Skip to content

Commit ce152a9

Browse files
authored
Create new coupons by admin added (#337)
* Create coupons by admin added * go lint fix * lint fix
1 parent f3f4d53 commit ce152a9

File tree

8 files changed

+169
-2
lines changed

8 files changed

+169
-2
lines changed

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
@@ -129,3 +129,18 @@ export const newProductAction = ({
129129
},
130130
};
131131
};
132+
133+
export const newCouponAction = ({
134+
accessToken,
135+
callback,
136+
...data
137+
}: ActionPayload) => {
138+
return {
139+
type: actionTypes.NEW_COUPON,
140+
payload: {
141+
accessToken,
142+
...data,
143+
callback,
144+
},
145+
};
146+
};

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

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import {
3333
PlusOutlined,
3434
OrderedListOutlined,
3535
ShoppingCartOutlined,
36+
GiftOutlined,
3637
} from "@ant-design/icons";
3738
import {
3839
COUPON_CODE_REQUIRED,
3940
PRODUCT_DETAILS_REQUIRED,
41+
COUPON_AMOUNT_REQUIRED,
4042
} from "../../constants/messages";
4143
import { useNavigate } from "react-router-dom";
4244
import roleTypes from "../../constants/roleTypes";
@@ -68,6 +70,11 @@ interface ShopProps extends PropsFromRedux {
6870
newProductHasErrored: boolean;
6971
newProductErrorMessage: string;
7072
onNewProductFinish: (values: any) => void;
73+
isNewCouponFormOpen: boolean;
74+
setIsNewCouponFormOpen: (isOpen: boolean) => void;
75+
newCouponHasErrored: boolean;
76+
newCouponErrorMessage: string;
77+
onNewCouponFinish: (values: any) => void;
7178
role: string;
7279
}
7380

@@ -120,6 +127,11 @@ const Shop: React.FC<ShopProps> = (props) => {
120127
newProductHasErrored,
121128
newProductErrorMessage,
122129
onNewProductFinish,
130+
isNewCouponFormOpen,
131+
setIsNewCouponFormOpen,
132+
newCouponHasErrored,
133+
newCouponErrorMessage,
134+
onNewCouponFinish,
123135
role,
124136
} = props;
125137

@@ -130,6 +142,18 @@ const Shop: React.FC<ShopProps> = (props) => {
130142
title="Shop"
131143
onBack={() => navigate("/dashboard")}
132144
extra={[
145+
role === roleTypes.ROLE_ADMIN && (
146+
<Button
147+
type="primary"
148+
shape="round"
149+
icon={<GiftOutlined />}
150+
size="large"
151+
key="new-coupon"
152+
onClick={() => setIsNewCouponFormOpen(true)}
153+
>
154+
Create Coupon
155+
</Button>
156+
),
133157
<Button
134158
type="primary"
135159
shape="round"
@@ -150,7 +174,7 @@ const Shop: React.FC<ShopProps> = (props) => {
150174
>
151175
Past Orders
152176
</Button>,
153-
]}
177+
].filter(Boolean)}
154178
/>
155179
<Descriptions column={1} className="balance-desc">
156180
<Descriptions.Item label="Available Balance">
@@ -291,6 +315,47 @@ const Shop: React.FC<ShopProps> = (props) => {
291315
</Form.Item>
292316
</Form>
293317
</Modal>
318+
<Modal
319+
title="Create New Coupon"
320+
open={isNewCouponFormOpen}
321+
footer={null}
322+
onCancel={() => setIsNewCouponFormOpen(false)}
323+
>
324+
<Form
325+
name="basic"
326+
initialValues={{
327+
remember: true,
328+
}}
329+
onFinish={onNewCouponFinish}
330+
>
331+
<Form.Item
332+
name="couponCode"
333+
rules={[{ required: true, message: COUPON_CODE_REQUIRED }]}
334+
>
335+
<Input placeholder="Coupon Code" />
336+
</Form.Item>
337+
<Form.Item
338+
name="amount"
339+
rules={[
340+
{ required: true, message: COUPON_AMOUNT_REQUIRED },
341+
{
342+
pattern: /^\d+$/,
343+
message: "Please enter a valid amount!",
344+
},
345+
]}
346+
>
347+
<Input placeholder="Amount" type="number" step="1" />
348+
</Form.Item>
349+
<Form.Item>
350+
{newCouponHasErrored && (
351+
<div className="error-message">{newCouponErrorMessage}</div>
352+
)}
353+
<Button type="primary" htmlType="submit" className="form-button">
354+
Create
355+
</Button>
356+
</Form.Item>
357+
</Form>
358+
</Modal>
294359
</Layout>
295360
);
296361
};

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
@@ -71,6 +71,7 @@ const actionTypes = {
7171
ORDER_RETURNED: "ORDER_RETURNED",
7272
APPLY_COUPON: "APPLY_COUPON",
7373
NEW_PRODUCT: "NEW_PRODUCT",
74+
NEW_COUPON: "NEW_COUPON",
7475

7576
GET_POSTS: "GET_POSTS",
7677
FETCHED_POSTS: "FETCHED_POSTS",

services/web/src/constants/messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export const COMMENT_REQUIRED: string = "Please enter a comment!";
5555
export const COUPON_CODE_REQUIRED: string = "Please enter a coupon code!";
5656
export const PRODUCT_DETAILS_REQUIRED: string =
5757
"Please enter all product details!";
58+
export const COUPON_AMOUNT_REQUIRED: string = "Please enter a coupon amount!";
59+
5860
export const NO_VEHICLE_DESC_1: string =
5961
"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.";
6062
export const NO_VEHICLE_DESC_2: string = " Click here ";
@@ -84,6 +86,7 @@ export const ORDER_NOT_RETURNED: string = "Could not return order";
8486
export const INVALID_COUPON_CODE: string = "Invalid Coupon Code";
8587
export const COUPON_APPLIED: string = "Coupon applied";
8688
export const COUPON_NOT_APPLIED: string = "Could not validate coupon";
89+
export const COUPON_NOT_CREATED: string = "Could not create coupon";
8790
export const PRODUCT_NOT_ADDED: string = "Could not add product";
8891
export const NEW_PRODUCT_ADDED: string = "Product added!";
8992
export const INVALID_CREDS: string = "Invalid Username or Password";

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
buyProductAction,
2424
applyCouponAction,
2525
newProductAction,
26+
newCouponAction,
2627
} from "../../actions/shopActions";
2728
import Shop from "../../components/shop/shop";
2829
import { useNavigate } from "react-router-dom";
@@ -41,6 +42,10 @@ const ShopContainer = (props) => {
4142
const [newProductHasErrored, setNewProductHasErrored] = useState(false);
4243
const [newProductErrorMessage, setNewProductErrorMessage] = useState("");
4344

45+
const [newCouponHasErrored, setNewCouponHasErrored] = React.useState(false);
46+
const [newCouponErrorMessage, setNewCouponErrorMessage] = React.useState("");
47+
const [isNewCouponFormOpen, setIsNewCouponFormOpen] = useState(false);
48+
4449
useEffect(() => {
4550
const callback = (res, data) => {
4651
if (res !== responseTypes.SUCCESS) {
@@ -124,6 +129,26 @@ const ShopContainer = (props) => {
124129
});
125130
};
126131

132+
const handleNewCouponFormFinish = (values) => {
133+
const callback = (res, data) => {
134+
if (res === responseTypes.SUCCESS) {
135+
setIsNewCouponFormOpen(false);
136+
Modal.success({
137+
title: SUCCESS_MESSAGE,
138+
content: data,
139+
});
140+
} else {
141+
setNewCouponHasErrored(true);
142+
setNewCouponErrorMessage(data);
143+
}
144+
};
145+
props.newCoupon({
146+
callback,
147+
accessToken,
148+
...values,
149+
});
150+
};
151+
127152
return (
128153
<Shop
129154
onBuyProduct={handleBuyProduct}
@@ -138,6 +163,11 @@ const ShopContainer = (props) => {
138163
newProductHasErrored={newProductHasErrored}
139164
newProductErrorMessage={newProductErrorMessage}
140165
onNewProductFinish={handleNewProductFormFinish}
166+
isNewCouponFormOpen={isNewCouponFormOpen}
167+
setIsNewCouponFormOpen={setIsNewCouponFormOpen}
168+
newCouponHasErrored={newCouponHasErrored}
169+
newCouponErrorMessage={newCouponErrorMessage}
170+
onNewCouponFinish={handleNewCouponFormFinish}
141171
{...props}
142172
/>
143173
);
@@ -154,6 +184,7 @@ const mapDispatchToProps = {
154184
buyProduct: buyProductAction,
155185
applyCoupon: applyCouponAction,
156186
newProduct: newProductAction,
187+
newCoupon: newCouponAction,
157188
};
158189

159190
ShopContainer.propTypes = {
@@ -162,6 +193,7 @@ ShopContainer.propTypes = {
162193
buyProduct: PropTypes.func,
163194
applyCoupon: PropTypes.func,
164195
newProduct: PropTypes.func,
196+
newCoupon: PropTypes.func,
165197
nextOffset: PropTypes.number,
166198
prevOffset: PropTypes.number,
167199
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
NEW_PRODUCT_ADDED,
3132
PRODUCT_NOT_ADDED,
3233
} from "../constants/messages";
@@ -384,6 +385,45 @@ export function* newProduct(action: MyAction): Generator<any, void, any> {
384385
}
385386
}
386387

388+
/**
389+
* create a new coupon (admin only)
390+
* @payload { accessToken, couponCode, amount, callback} payload
391+
* accessToken: access token of the user
392+
* couponCode: coupon code to create
393+
* amount: amount for the coupon
394+
* callback : callback method
395+
*/
396+
export function* newCoupon(action: MyAction): Generator<any, void, any> {
397+
const { accessToken, couponCode, amount, callback } = action.payload;
398+
let recievedResponse: ReceivedResponse = {} as ReceivedResponse;
399+
try {
400+
yield put({ type: actionTypes.FETCHING_DATA });
401+
let postUrl = APIService.COMMUNITY_SERVICE + requestURLS.NEW_COUPON;
402+
const headers = {
403+
"Content-Type": "application/json",
404+
Authorization: `Bearer ${accessToken}`,
405+
};
406+
const responseJson = yield fetch(postUrl, {
407+
headers,
408+
method: "POST",
409+
body: JSON.stringify({ coupon_code: couponCode, amount: amount }),
410+
}).then((response: Response) => {
411+
recievedResponse = response as ReceivedResponse;
412+
return response.json();
413+
});
414+
415+
yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse });
416+
if (recievedResponse.ok) {
417+
callback(responseTypes.SUCCESS, responseJson);
418+
} else {
419+
callback(responseTypes.FAILURE, COUPON_NOT_CREATED);
420+
}
421+
} catch (e) {
422+
yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse });
423+
callback(responseTypes.FAILURE, COUPON_NOT_CREATED);
424+
}
425+
}
426+
387427
export function* shopActionWatcher(): Generator<any, void, any> {
388428
yield takeLatest(actionTypes.GET_PRODUCTS, getProducts);
389429
yield takeLatest(actionTypes.BUY_PRODUCT, buyProduct);
@@ -392,4 +432,5 @@ export function* shopActionWatcher(): Generator<any, void, any> {
392432
yield takeLatest(actionTypes.RETURN_ORDER, returnOrder);
393433
yield takeLatest(actionTypes.APPLY_COUPON, applyCoupon);
394434
yield takeLatest(actionTypes.NEW_PRODUCT, newProduct);
435+
yield takeLatest(actionTypes.NEW_COUPON, newCoupon);
395436
}

0 commit comments

Comments
 (0)