Skip to content

Commit 46c80e9

Browse files
committed
Create coupons by admin added
1 parent 6094546 commit 46c80e9

8 files changed

Lines changed: 176 additions & 4 deletions

File tree

services/community/api/controllers/coupon_controller.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package controllers
1616

1717
import (
1818
"encoding/json"
19+
"fmt"
1920
"io"
2021
"log"
2122
"net/http"
@@ -46,12 +47,20 @@ func (s *Server) AddNewCoupon(w http.ResponseWriter, r *http.Request) {
4647
return
4748
}
4849
coupon.Prepare()
50+
51+
existingCoupon, err := models.ValidateCode(s.Client, s.DB, bson.M{"coupon_code": coupon.CouponCode})
52+
if err == nil && existingCoupon.CouponCode != "" {
53+
responses.ERROR(w, http.StatusConflict, fmt.Errorf("Coupon code already exists"))
54+
return
55+
}
56+
4957
savedCoupon, er := models.SaveCoupon(s.Client, coupon)
5058
if er != nil {
5159
responses.ERROR(w, http.StatusInternalServerError, er)
60+
return
5261
}
5362
if savedCoupon.CouponCode != "" {
54-
responses.JSON(w, http.StatusOK, "Coupon Added in database")
63+
responses.JSON(w, http.StatusOK, "Coupon added in database!")
5564
}
5665

5766
}

services/web/src/actions/shopActions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,18 @@ export const applyCouponAction = ({
114114
},
115115
};
116116
};
117+
118+
export const newCouponAction = ({
119+
accessToken,
120+
callback,
121+
...data
122+
}: ActionPayload) => {
123+
return {
124+
type: actionTypes.NEW_COUPON,
125+
payload: {
126+
accessToken,
127+
...data,
128+
callback,
129+
},
130+
};
131+
};

services/web/src/components/shop/shop.tsx

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ import {
3333
PlusOutlined,
3434
OrderedListOutlined,
3535
ShoppingCartOutlined,
36+
GiftOutlined,
3637
} from "@ant-design/icons";
37-
import { COUPON_CODE_REQUIRED } from "../../constants/messages";
38+
import { COUPON_CODE_REQUIRED, COUPON_AMOUNT_REQUIRED } from "../../constants/messages";
3839
import { useNavigate } from "react-router-dom";
40+
import roleTypes from "../../constants/roleTypes";
3941

4042
const { Content } = Layout;
4143
const { Meta } = Card;
@@ -59,6 +61,12 @@ interface ShopProps extends PropsFromRedux {
5961
nextOffset: number | null;
6062
onOffsetChange: (offset: number | null) => void;
6163
onBuyProduct: (product: Product) => void;
64+
isNewCouponFormOpen: boolean;
65+
setIsNewCouponFormOpen: (isOpen: boolean) => void;
66+
newCouponHasErrored: boolean;
67+
newCouponErrorMessage: string;
68+
onNewCouponFinish: (values: any) => void;
69+
role: string;
6270
}
6371

6472
const ProductAvatar: React.FC<{ image_url: string }> = ({ image_url }) => (
@@ -105,6 +113,12 @@ const Shop: React.FC<ShopProps> = (props) => {
105113
nextOffset,
106114
onOffsetChange,
107115
onBuyProduct,
116+
isNewCouponFormOpen,
117+
setIsNewCouponFormOpen,
118+
newCouponHasErrored,
119+
newCouponErrorMessage,
120+
onNewCouponFinish,
121+
role,
108122
} = props;
109123

110124
return (
@@ -114,6 +128,18 @@ const Shop: React.FC<ShopProps> = (props) => {
114128
title="Shop"
115129
onBack={() => navigate("/dashboard")}
116130
extra={[
131+
role === roleTypes.ROLE_ADMIN && (
132+
<Button
133+
type="primary"
134+
shape="round"
135+
icon={<GiftOutlined />}
136+
size="large"
137+
key="new-coupon"
138+
onClick={() => setIsNewCouponFormOpen(true)}
139+
>
140+
Create Coupon
141+
</Button>
142+
),
117143
<Button
118144
type="primary"
119145
shape="round"
@@ -134,7 +160,7 @@ const Shop: React.FC<ShopProps> = (props) => {
134160
>
135161
Past Orders
136162
</Button>,
137-
]}
163+
].filter(Boolean)}
138164
/>
139165
<Descriptions column={1} className="balance-desc">
140166
<Descriptions.Item label="Available Balance">
@@ -211,6 +237,47 @@ const Shop: React.FC<ShopProps> = (props) => {
211237
</Form.Item>
212238
</Form>
213239
</Modal>
240+
<Modal
241+
title="Create New Coupon"
242+
open={isNewCouponFormOpen}
243+
footer={null}
244+
onCancel={() => setIsNewCouponFormOpen(false)}
245+
>
246+
<Form
247+
name="basic"
248+
initialValues={{
249+
remember: true,
250+
}}
251+
onFinish={onNewCouponFinish}
252+
>
253+
<Form.Item
254+
name="couponCode"
255+
rules={[{ required: true, message: COUPON_CODE_REQUIRED }]}
256+
>
257+
<Input placeholder="Coupon Code" />
258+
</Form.Item>
259+
<Form.Item
260+
name="amount"
261+
rules={[
262+
{ required: true, message: COUPON_AMOUNT_REQUIRED },
263+
{
264+
pattern: /^\d+$/,
265+
message: "Please enter a valid amount!",
266+
},
267+
]}
268+
>
269+
<Input placeholder="Amount" type="number" step="1" />
270+
</Form.Item>
271+
<Form.Item>
272+
{newCouponHasErrored && (
273+
<div className="error-message">{newCouponErrorMessage}</div>
274+
)}
275+
<Button type="primary" htmlType="submit" className="form-button">
276+
Create
277+
</Button>
278+
</Form.Item>
279+
</Form>
280+
</Modal>
214281
</Layout>
215282
);
216283
};
@@ -223,12 +290,16 @@ interface RootState {
223290
prevOffset: number | null;
224291
nextOffset: number | null;
225292
};
293+
userReducer: {
294+
role: string;
295+
};
226296
}
227297

228298
const mapStateToProps = (state: RootState) => {
229299
const { accessToken, availableCredit, products, prevOffset, nextOffset } =
230300
state.shopReducer;
231-
return { accessToken, availableCredit, products, prevOffset, nextOffset };
301+
const { role } = state.userReducer;
302+
return { accessToken, availableCredit, products, prevOffset, nextOffset, role };
232303
};
233304

234305
const connector = connect(mapStateToProps);

services/web/src/constants/APIConstant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ export const requestURLS: RequestURLSType = {
7676
GET_POST_BY_ID: "api/v2/community/posts/<postId>",
7777
ADD_COMMENT: "api/v2/community/posts/<postId>/comment",
7878
VALIDATE_COUPON: "api/v2/coupon/validate-coupon",
79+
NEW_COUPON: "api/v2/coupon/new-coupon",
7980
VALIDATE_TOKEN: "api/auth/verify",
8081
};

services/web/src/constants/actionTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const actionTypes = {
7070
RETURN_ORDER: "RETURN_ORDER",
7171
ORDER_RETURNED: "ORDER_RETURNED",
7272
APPLY_COUPON: "APPLY_COUPON",
73+
NEW_COUPON: "NEW_COUPON",
7374

7475
GET_POSTS: "GET_POSTS",
7576
FETCHED_POSTS: "FETCHED_POSTS",

services/web/src/constants/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const POST_TITLE_REQUIRED: string = "Please enter title for post!";
5353
export const POST_DESC_REQUIRED: string = "Please enter description for Post!";
5454
export const COMMENT_REQUIRED: string = "Please enter a comment!";
5555
export const COUPON_CODE_REQUIRED: string = "Please enter a coupon code!";
56+
export const COUPON_AMOUNT_REQUIRED: string = "Please enter a coupon amount!";
5657

5758
export const NO_VEHICLE_DESC_1: string =
5859
"Your newly purchased Vehicle Details have been sent to you email address. Please check your email for the VIN and PIN code of your vehicle using the MailHog web portal.";
@@ -83,6 +84,7 @@ export const ORDER_NOT_RETURNED: string = "Could not return order";
8384
export const INVALID_COUPON_CODE: string = "Invalid Coupon Code";
8485
export const COUPON_APPLIED: string = "Coupon applied";
8586
export const COUPON_NOT_APPLIED: string = "Could not validate coupon";
87+
export const COUPON_NOT_CREATED: string = "Could not create coupon";
8688
export const INVALID_CREDS: string = "Invalid Username or Password";
8789
export const INVALID_CODE_CREDS: string = "Invalid Email or Code";
8890
export const SIGN_UP_SUCCESS: string = "User Registered Successfully!";

services/web/src/containers/shop/shop.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getProductsAction,
2323
buyProductAction,
2424
applyCouponAction,
25+
newCouponAction,
2526
} from "../../actions/shopActions";
2627
import Shop from "../../components/shop/shop";
2728
import { useNavigate } from "react-router-dom";
@@ -36,6 +37,10 @@ const ShopContainer = (props) => {
3637
const [errorMessage, setErrorMessage] = React.useState("");
3738
const [isCouponFormOpen, setIsCouponFormOpen] = useState(false);
3839

40+
const [newCouponHasErrored, setNewCouponHasErrored] = React.useState(false);
41+
const [newCouponErrorMessage, setNewCouponErrorMessage] = React.useState("");
42+
const [isNewCouponFormOpen, setIsNewCouponFormOpen] = useState(false);
43+
3944
useEffect(() => {
4045
const callback = (res, data) => {
4146
if (res !== responseTypes.SUCCESS) {
@@ -98,6 +103,26 @@ const ShopContainer = (props) => {
98103
});
99104
};
100105

106+
const handleNewCouponFormFinish = (values) => {
107+
const callback = (res, data) => {
108+
if (res === responseTypes.SUCCESS) {
109+
setIsNewCouponFormOpen(false);
110+
Modal.success({
111+
title: SUCCESS_MESSAGE,
112+
content: data,
113+
});
114+
} else {
115+
setNewCouponHasErrored(true);
116+
setNewCouponErrorMessage(data);
117+
}
118+
};
119+
props.newCoupon({
120+
callback,
121+
accessToken,
122+
...values,
123+
});
124+
};
125+
101126
return (
102127
<Shop
103128
onBuyProduct={handleBuyProduct}
@@ -107,6 +132,11 @@ const ShopContainer = (props) => {
107132
errorMessage={errorMessage}
108133
onFinish={handleFormFinish}
109134
onOffsetChange={handleOffsetChange}
135+
isNewCouponFormOpen={isNewCouponFormOpen}
136+
setIsNewCouponFormOpen={setIsNewCouponFormOpen}
137+
newCouponHasErrored={newCouponHasErrored}
138+
newCouponErrorMessage={newCouponErrorMessage}
139+
onNewCouponFinish={handleNewCouponFormFinish}
110140
{...props}
111141
/>
112142
);
@@ -122,13 +152,15 @@ const mapDispatchToProps = {
122152
getProducts: getProductsAction,
123153
buyProduct: buyProductAction,
124154
applyCoupon: applyCouponAction,
155+
newCoupon: newCouponAction,
125156
};
126157

127158
ShopContainer.propTypes = {
128159
accessToken: PropTypes.string,
129160
getProducts: PropTypes.func,
130161
buyProduct: PropTypes.func,
131162
applyCoupon: PropTypes.func,
163+
newCoupon: PropTypes.func,
132164
nextOffset: PropTypes.number,
133165
prevOffset: PropTypes.number,
134166
onOffsetChange: PropTypes.func,

services/web/src/sagas/shopSaga.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
INVALID_COUPON_CODE,
2828
COUPON_APPLIED,
2929
COUPON_NOT_APPLIED,
30+
COUPON_NOT_CREATED,
3031
} from "../constants/messages";
3132

3233
interface ReceivedResponse extends Response {
@@ -342,11 +343,51 @@ export function* applyCoupon(action: MyAction): Generator<any, void, any> {
342343
}
343344
}
344345

346+
/**
347+
* create a new coupon (admin only)
348+
* @payload { accessToken, couponCode, amount, callback} payload
349+
* accessToken: access token of the user
350+
* couponCode: coupon code to create
351+
* amount: amount for the coupon
352+
* callback : callback method
353+
*/
354+
export function* newCoupon(action: MyAction): Generator<any, void, any> {
355+
const { accessToken, couponCode, amount, callback } = action.payload;
356+
let recievedResponse: ReceivedResponse = {} as ReceivedResponse;
357+
try {
358+
yield put({ type: actionTypes.FETCHING_DATA });
359+
let postUrl = APIService.COMMUNITY_SERVICE + requestURLS.NEW_COUPON;
360+
const headers = {
361+
"Content-Type": "application/json",
362+
Authorization: `Bearer ${accessToken}`,
363+
};
364+
const responseJson = yield fetch(postUrl, {
365+
headers,
366+
method: "POST",
367+
body: JSON.stringify({ coupon_code: couponCode, amount: amount }),
368+
}).then((response: Response) => {
369+
recievedResponse = response as ReceivedResponse;
370+
return response.json();
371+
});
372+
373+
yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse });
374+
if (recievedResponse.ok) {
375+
callback(responseTypes.SUCCESS, responseJson);
376+
} else {
377+
callback(responseTypes.FAILURE, COUPON_NOT_CREATED);
378+
}
379+
} catch (e) {
380+
yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse });
381+
callback(responseTypes.FAILURE, COUPON_NOT_CREATED);
382+
}
383+
}
384+
345385
export function* shopActionWatcher(): Generator<any, void, any> {
346386
yield takeLatest(actionTypes.GET_PRODUCTS, getProducts);
347387
yield takeLatest(actionTypes.BUY_PRODUCT, buyProduct);
348388
yield takeLatest(actionTypes.GET_ORDERS, getOrders);
349389
yield takeLatest(actionTypes.GET_ORDER_BY_ID, getOrderById);
350390
yield takeLatest(actionTypes.RETURN_ORDER, returnOrder);
351391
yield takeLatest(actionTypes.APPLY_COUPON, applyCoupon);
392+
yield takeLatest(actionTypes.NEW_COUPON, newCoupon);
352393
}

0 commit comments

Comments
 (0)