From 46c80e9d86d4b9478b7d9dd48dac25459000b40a Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Fri, 31 Oct 2025 02:48:51 +0530 Subject: [PATCH 1/3] Create coupons by admin added --- .../api/controllers/coupon_controller.go | 11 ++- services/web/src/actions/shopActions.ts | 15 ++++ services/web/src/components/shop/shop.tsx | 77 ++++++++++++++++++- services/web/src/constants/APIConstant.ts | 1 + services/web/src/constants/actionTypes.ts | 1 + services/web/src/constants/messages.ts | 2 + services/web/src/containers/shop/shop.js | 32 ++++++++ services/web/src/sagas/shopSaga.ts | 41 ++++++++++ 8 files changed, 176 insertions(+), 4 deletions(-) diff --git a/services/community/api/controllers/coupon_controller.go b/services/community/api/controllers/coupon_controller.go index a1f1f2e6..b83d8a6c 100644 --- a/services/community/api/controllers/coupon_controller.go +++ b/services/community/api/controllers/coupon_controller.go @@ -16,6 +16,7 @@ package controllers import ( "encoding/json" + "fmt" "io" "log" "net/http" @@ -46,12 +47,20 @@ func (s *Server) AddNewCoupon(w http.ResponseWriter, r *http.Request) { return } coupon.Prepare() + + existingCoupon, err := models.ValidateCode(s.Client, s.DB, bson.M{"coupon_code": coupon.CouponCode}) + if err == nil && existingCoupon.CouponCode != "" { + responses.ERROR(w, http.StatusConflict, fmt.Errorf("Coupon code already exists")) + return + } + savedCoupon, er := models.SaveCoupon(s.Client, coupon) if er != nil { responses.ERROR(w, http.StatusInternalServerError, er) + return } if savedCoupon.CouponCode != "" { - responses.JSON(w, http.StatusOK, "Coupon Added in database") + responses.JSON(w, http.StatusOK, "Coupon added in database!") } } diff --git a/services/web/src/actions/shopActions.ts b/services/web/src/actions/shopActions.ts index b4d39de3..51277119 100644 --- a/services/web/src/actions/shopActions.ts +++ b/services/web/src/actions/shopActions.ts @@ -114,3 +114,18 @@ export const applyCouponAction = ({ }, }; }; + +export const newCouponAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { + return { + type: actionTypes.NEW_COUPON, + payload: { + accessToken, + ...data, + callback, + }, + }; +}; \ No newline at end of file diff --git a/services/web/src/components/shop/shop.tsx b/services/web/src/components/shop/shop.tsx index 5155c574..07bdaa74 100644 --- a/services/web/src/components/shop/shop.tsx +++ b/services/web/src/components/shop/shop.tsx @@ -33,9 +33,11 @@ import { PlusOutlined, OrderedListOutlined, ShoppingCartOutlined, + GiftOutlined, } from "@ant-design/icons"; -import { COUPON_CODE_REQUIRED } from "../../constants/messages"; +import { COUPON_CODE_REQUIRED, COUPON_AMOUNT_REQUIRED } from "../../constants/messages"; import { useNavigate } from "react-router-dom"; +import roleTypes from "../../constants/roleTypes"; const { Content } = Layout; const { Meta } = Card; @@ -59,6 +61,12 @@ interface ShopProps extends PropsFromRedux { nextOffset: number | null; onOffsetChange: (offset: number | null) => void; onBuyProduct: (product: Product) => void; + isNewCouponFormOpen: boolean; + setIsNewCouponFormOpen: (isOpen: boolean) => void; + newCouponHasErrored: boolean; + newCouponErrorMessage: string; + onNewCouponFinish: (values: any) => void; + role: string; } const ProductAvatar: React.FC<{ image_url: string }> = ({ image_url }) => ( @@ -105,6 +113,12 @@ const Shop: React.FC = (props) => { nextOffset, onOffsetChange, onBuyProduct, + isNewCouponFormOpen, + setIsNewCouponFormOpen, + newCouponHasErrored, + newCouponErrorMessage, + onNewCouponFinish, + role, } = props; return ( @@ -114,6 +128,18 @@ const Shop: React.FC = (props) => { title="Shop" onBack={() => navigate("/dashboard")} extra={[ + role === roleTypes.ROLE_ADMIN && ( + + ), , - ]} + ].filter(Boolean)} /> @@ -211,6 +237,47 @@ const Shop: React.FC = (props) => { + setIsNewCouponFormOpen(false)} + > +
+ + + + + + + + {newCouponHasErrored && ( +
{newCouponErrorMessage}
+ )} + +
+
+
); }; @@ -223,12 +290,16 @@ interface RootState { prevOffset: number | null; nextOffset: number | null; }; + userReducer: { + role: string; + }; } const mapStateToProps = (state: RootState) => { const { accessToken, availableCredit, products, prevOffset, nextOffset } = state.shopReducer; - return { accessToken, availableCredit, products, prevOffset, nextOffset }; + const { role } = state.userReducer; + return { accessToken, availableCredit, products, prevOffset, nextOffset, role }; }; const connector = connect(mapStateToProps); diff --git a/services/web/src/constants/APIConstant.ts b/services/web/src/constants/APIConstant.ts index 63d3fc2b..f4036cb8 100644 --- a/services/web/src/constants/APIConstant.ts +++ b/services/web/src/constants/APIConstant.ts @@ -76,5 +76,6 @@ export const requestURLS: RequestURLSType = { GET_POST_BY_ID: "api/v2/community/posts/", ADD_COMMENT: "api/v2/community/posts//comment", VALIDATE_COUPON: "api/v2/coupon/validate-coupon", + NEW_COUPON: "api/v2/coupon/new-coupon", VALIDATE_TOKEN: "api/auth/verify", }; diff --git a/services/web/src/constants/actionTypes.ts b/services/web/src/constants/actionTypes.ts index 861a3b60..b2adfa91 100644 --- a/services/web/src/constants/actionTypes.ts +++ b/services/web/src/constants/actionTypes.ts @@ -70,6 +70,7 @@ const actionTypes = { RETURN_ORDER: "RETURN_ORDER", ORDER_RETURNED: "ORDER_RETURNED", APPLY_COUPON: "APPLY_COUPON", + NEW_COUPON: "NEW_COUPON", GET_POSTS: "GET_POSTS", FETCHED_POSTS: "FETCHED_POSTS", diff --git a/services/web/src/constants/messages.ts b/services/web/src/constants/messages.ts index 4a03de0e..98bdaee3 100644 --- a/services/web/src/constants/messages.ts +++ b/services/web/src/constants/messages.ts @@ -53,6 +53,7 @@ export const POST_TITLE_REQUIRED: string = "Please enter title for post!"; export const POST_DESC_REQUIRED: string = "Please enter description for Post!"; export const COMMENT_REQUIRED: string = "Please enter a comment!"; export const COUPON_CODE_REQUIRED: string = "Please enter a coupon code!"; +export const COUPON_AMOUNT_REQUIRED: string = "Please enter a coupon amount!"; export const NO_VEHICLE_DESC_1: string = "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"; export const INVALID_COUPON_CODE: string = "Invalid Coupon Code"; export const COUPON_APPLIED: string = "Coupon applied"; export const COUPON_NOT_APPLIED: string = "Could not validate coupon"; +export const COUPON_NOT_CREATED: string = "Could not create coupon"; export const INVALID_CREDS: string = "Invalid Username or Password"; export const INVALID_CODE_CREDS: string = "Invalid Email or Code"; export const SIGN_UP_SUCCESS: string = "User Registered Successfully!"; diff --git a/services/web/src/containers/shop/shop.js b/services/web/src/containers/shop/shop.js index c7fc7ebf..d1cfe20c 100644 --- a/services/web/src/containers/shop/shop.js +++ b/services/web/src/containers/shop/shop.js @@ -22,6 +22,7 @@ import { getProductsAction, buyProductAction, applyCouponAction, + newCouponAction, } from "../../actions/shopActions"; import Shop from "../../components/shop/shop"; import { useNavigate } from "react-router-dom"; @@ -36,6 +37,10 @@ const ShopContainer = (props) => { const [errorMessage, setErrorMessage] = React.useState(""); const [isCouponFormOpen, setIsCouponFormOpen] = useState(false); + const [newCouponHasErrored, setNewCouponHasErrored] = React.useState(false); + const [newCouponErrorMessage, setNewCouponErrorMessage] = React.useState(""); + const [isNewCouponFormOpen, setIsNewCouponFormOpen] = useState(false); + useEffect(() => { const callback = (res, data) => { if (res !== responseTypes.SUCCESS) { @@ -98,6 +103,26 @@ const ShopContainer = (props) => { }); }; + const handleNewCouponFormFinish = (values) => { + const callback = (res, data) => { + if (res === responseTypes.SUCCESS) { + setIsNewCouponFormOpen(false); + Modal.success({ + title: SUCCESS_MESSAGE, + content: data, + }); + } else { + setNewCouponHasErrored(true); + setNewCouponErrorMessage(data); + } + }; + props.newCoupon({ + callback, + accessToken, + ...values, + }); + }; + return ( { errorMessage={errorMessage} onFinish={handleFormFinish} onOffsetChange={handleOffsetChange} + isNewCouponFormOpen={isNewCouponFormOpen} + setIsNewCouponFormOpen={setIsNewCouponFormOpen} + newCouponHasErrored={newCouponHasErrored} + newCouponErrorMessage={newCouponErrorMessage} + onNewCouponFinish={handleNewCouponFormFinish} {...props} /> ); @@ -122,6 +152,7 @@ const mapDispatchToProps = { getProducts: getProductsAction, buyProduct: buyProductAction, applyCoupon: applyCouponAction, + newCoupon: newCouponAction, }; ShopContainer.propTypes = { @@ -129,6 +160,7 @@ ShopContainer.propTypes = { getProducts: PropTypes.func, buyProduct: PropTypes.func, applyCoupon: PropTypes.func, + newCoupon: PropTypes.func, nextOffset: PropTypes.number, prevOffset: PropTypes.number, onOffsetChange: PropTypes.func, diff --git a/services/web/src/sagas/shopSaga.ts b/services/web/src/sagas/shopSaga.ts index 69689d34..8f5eeb67 100644 --- a/services/web/src/sagas/shopSaga.ts +++ b/services/web/src/sagas/shopSaga.ts @@ -27,6 +27,7 @@ import { INVALID_COUPON_CODE, COUPON_APPLIED, COUPON_NOT_APPLIED, + COUPON_NOT_CREATED, } from "../constants/messages"; interface ReceivedResponse extends Response { @@ -342,6 +343,45 @@ export function* applyCoupon(action: MyAction): Generator { } } +/** + * create a new coupon (admin only) + * @payload { accessToken, couponCode, amount, callback} payload + * accessToken: access token of the user + * couponCode: coupon code to create + * amount: amount for the coupon + * callback : callback method + */ +export function* newCoupon(action: MyAction): Generator { + const { accessToken, couponCode, amount, callback } = action.payload; + let recievedResponse: ReceivedResponse = {} as ReceivedResponse; + try { + yield put({ type: actionTypes.FETCHING_DATA }); + let postUrl = APIService.COMMUNITY_SERVICE + requestURLS.NEW_COUPON; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; + const responseJson = yield fetch(postUrl, { + headers, + method: "POST", + body: JSON.stringify({ coupon_code: couponCode, amount: amount }), + }).then((response: Response) => { + recievedResponse = response as ReceivedResponse; + return response.json(); + }); + + yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse }); + if (recievedResponse.ok) { + callback(responseTypes.SUCCESS, responseJson); + } else { + callback(responseTypes.FAILURE, COUPON_NOT_CREATED); + } + } catch (e) { + yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse }); + callback(responseTypes.FAILURE, COUPON_NOT_CREATED); + } +} + export function* shopActionWatcher(): Generator { yield takeLatest(actionTypes.GET_PRODUCTS, getProducts); yield takeLatest(actionTypes.BUY_PRODUCT, buyProduct); @@ -349,4 +389,5 @@ export function* shopActionWatcher(): Generator { yield takeLatest(actionTypes.GET_ORDER_BY_ID, getOrderById); yield takeLatest(actionTypes.RETURN_ORDER, returnOrder); yield takeLatest(actionTypes.APPLY_COUPON, applyCoupon); + yield takeLatest(actionTypes.NEW_COUPON, newCoupon); } From a57de36a62925012880644b14f4147a0ccbb0f57 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Fri, 31 Oct 2025 03:23:21 +0530 Subject: [PATCH 2/3] go lint fix --- services/community/api/controllers/coupon_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/community/api/controllers/coupon_controller.go b/services/community/api/controllers/coupon_controller.go index b83d8a6c..66e8c9fb 100644 --- a/services/community/api/controllers/coupon_controller.go +++ b/services/community/api/controllers/coupon_controller.go @@ -50,7 +50,7 @@ func (s *Server) AddNewCoupon(w http.ResponseWriter, r *http.Request) { existingCoupon, err := models.ValidateCode(s.Client, s.DB, bson.M{"coupon_code": coupon.CouponCode}) if err == nil && existingCoupon.CouponCode != "" { - responses.ERROR(w, http.StatusConflict, fmt.Errorf("Coupon code already exists")) + responses.ERROR(w, http.StatusConflict, fmt.Errorf("coupon code already exists")) return } From 6f13f401ab2d01a5d6ea8c8bf094a465eae35471 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Fri, 31 Oct 2025 08:20:37 +0530 Subject: [PATCH 3/3] lint fix --- services/web/src/actions/shopActions.ts | 2 +- services/web/src/components/shop/shop.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/services/web/src/actions/shopActions.ts b/services/web/src/actions/shopActions.ts index 51277119..064c4460 100644 --- a/services/web/src/actions/shopActions.ts +++ b/services/web/src/actions/shopActions.ts @@ -128,4 +128,4 @@ export const newCouponAction = ({ callback, }, }; -}; \ No newline at end of file +}; diff --git a/services/web/src/components/shop/shop.tsx b/services/web/src/components/shop/shop.tsx index 07bdaa74..21514513 100644 --- a/services/web/src/components/shop/shop.tsx +++ b/services/web/src/components/shop/shop.tsx @@ -35,7 +35,10 @@ import { ShoppingCartOutlined, GiftOutlined, } from "@ant-design/icons"; -import { COUPON_CODE_REQUIRED, COUPON_AMOUNT_REQUIRED } from "../../constants/messages"; +import { + COUPON_CODE_REQUIRED, + COUPON_AMOUNT_REQUIRED, +} from "../../constants/messages"; import { useNavigate } from "react-router-dom"; import roleTypes from "../../constants/roleTypes"; @@ -299,7 +302,14 @@ const mapStateToProps = (state: RootState) => { const { accessToken, availableCredit, products, prevOffset, nextOffset } = state.shopReducer; const { role } = state.userReducer; - return { accessToken, availableCredit, products, prevOffset, nextOffset, role }; + return { + accessToken, + availableCredit, + products, + prevOffset, + nextOffset, + role, + }; }; const connector = connect(mapStateToProps);