Skip to content

Commit

Permalink
Coupon Code and Checkout Feature Implementation Added (#624)
Browse files Browse the repository at this point in the history
New Feature Coupon Code and Checkout Added
  • Loading branch information
chavda-bhavik authored Jun 27, 2024
2 parents f448990 + 79dcf58 commit 77d094a
Show file tree
Hide file tree
Showing 29 changed files with 662 additions and 175 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PaymentAPIService } from '@impler/shared';
import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';

@Injectable()
export class ApplyCoupon {
constructor(private paymentApiService: PaymentAPIService) {}

async execute(couponCode: string, userEmail: string, planCode: string) {
try {
return await this.paymentApiService.checkAppliedCoupon(couponCode, userEmail, planCode);
} catch (error) {
if (error) {
throw new BadRequestException(error);
} else throw new InternalServerErrorException();
}
}
}
26 changes: 26 additions & 0 deletions apps/api/src/app/user/usecases/checkout/checkout.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PaymentAPIService } from '@impler/shared';
import { Injectable } from '@nestjs/common';

@Injectable()
export class Checkout {
constructor(private paymentApiService: PaymentAPIService) {}

async execute({
externalId,
paymentMethodId,
planCode,
couponCode,
}: {
externalId: string;
planCode: string;
paymentMethodId: string;
couponCode?: string;
}) {
return this.paymentApiService.checkout({
externalId: externalId,
planCode: planCode,
paymentMethodId: paymentMethodId,
couponCode: couponCode,
});
}
}
6 changes: 6 additions & 0 deletions apps/api/src/app/user/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ConfirmIntentId } from './save-payment-intent-id/save-paymentintentid.u
import { RetrievePaymentMethods } from './retrive-payment-methods/retrive-payment-methods.usecase';
import { DeleteUserPaymentMethod } from './delete-user-payment-method/delete-user-payment-method.usecase';
import { GetTransactionHistory } from './get-transaction-history/get-transaction-history.usecase';
import { ApplyCoupon } from './apply-coupon/apply-coupon.usecase';
import { Checkout } from './checkout/checkout.usecase';

export const USE_CASES = [
GetImportCounts,
Expand All @@ -16,6 +18,8 @@ export const USE_CASES = [
RetrievePaymentMethods,
DeleteUserPaymentMethod,
GetTransactionHistory,
ApplyCoupon,
Checkout,
//
];

Expand All @@ -28,4 +32,6 @@ export {
RetrievePaymentMethods,
DeleteUserPaymentMethod,
GetTransactionHistory,
ApplyCoupon,
Checkout,
};
39 changes: 37 additions & 2 deletions apps/api/src/app/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ConfirmIntentId,
DeleteUserPaymentMethod,
GetTransactionHistory,
ApplyCoupon,
Checkout,
} from './usecases';
import { JwtAuthGuard } from '@shared/framework/auth.gaurd';
import { IJwtPayload, ACCESS_KEY_NAME } from '@impler/shared';
Expand All @@ -21,14 +23,16 @@ import { RetrievePaymentMethods } from './usecases/retrive-payment-methods/retri
@ApiSecurity(ACCESS_KEY_NAME)
export class UserController {
constructor(
private checkout: Checkout,
private applyCoupon: ApplyCoupon,
private getImportsCount: GetImportCounts,
private getActiveSubscription: GetActiveSubscription,
private cancelSubscription: CancelSubscription,
private updatePaymentMethod: UpdatePaymentMethod,
private confirmIntentId: ConfirmIntentId,
private getTransactionHistory: GetTransactionHistory,
private retrivePaymentMethods: RetrievePaymentMethods,
private deleteUserPaymentMethod: DeleteUserPaymentMethod,
private getTransactionHistory: GetTransactionHistory
private deleteUserPaymentMethod: DeleteUserPaymentMethod
) {}

@Get('/import-count')
Expand Down Expand Up @@ -102,4 +106,35 @@ export class UserController {
async getTransactionHistoryRoute(@UserSession() user: IJwtPayload) {
return this.getTransactionHistory.execute(user.email);
}

@Get('/coupons/:couponCode/apply/:planCode')
@ApiOperation({
summary:
'Check if a Particular coupon is available to apply for a particular plan and if the coupon is valid or not',
})
async applyCouponRoute(
@UserSession() user: IJwtPayload,
@Param('couponCode') couponCode: string,
@Param('planCode') planCode: string
) {
return this.applyCoupon.execute(couponCode, user.email, planCode);
}

@Get('/checkout')
@ApiOperation({
summary: 'Make successfull checkout once the coupon is successfully applied',
})
async checkoutRoute(
@Query('planCode') planCode: string,
@UserSession() user: IJwtPayload,
@Query('paymentMethodId') paymentMethodId: string,
@Query('couponCode') couponCode?: string
) {
return this.checkout.execute({
planCode: planCode,
externalId: user.email,
paymentMethodId: paymentMethodId,
couponCode: couponCode,
});
}
}
25 changes: 25 additions & 0 deletions apps/web/components/home/PlanDetails/PlanDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { numberFormatter } from '@impler/shared/dist/utils/helpers';

export function PlanDetails() {
const router = useRouter();
const status = router.query?.status;
const planName = router.query?.plan;

const { profile } = useApp();
const { [CONSTANTS.PLAN_CODE_QUERY_KEY]: selectedPlan } = router.query;

Expand All @@ -36,6 +39,28 @@ export function PlanDetails() {
withCloseButton: true,
});
};
useEffect(() => {
if (planName && status) {
modals.openConfirmModal({
id: MODAL_KEYS.PAYMENT_SUCCEED,
title: status === CONSTANTS.PAYMENT_SUCCCESS_CODE ? 'Subscription activated' : 'Payment failed',
centered: true,
children: (
<Text fw="bolder" align="center">
{status === CONSTANTS.PAYMENT_SUCCCESS_CODE
? CONSTANTS.PAYMENT_SUCCESS_MESSAGE
: CONSTANTS.PAYMENT_FAILED_MESSAGE}
</Text>
),
labels: { confirm: 'Ok', cancel: false },
confirmProps: { color: status === CONSTANTS.PAYMENT_SUCCCESS_CODE ? 'green' : 'red' },

onConfirm: () => {
router.push(ROUTES.HOME, {}, { shallow: true });
},
});
}
}, [planName, status, router]);

useEffect(() => {
if (selectedPlan && profile) {
Expand Down
51 changes: 51 additions & 0 deletions apps/web/components/settings/AddCard/Coupon/Coupon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { Alert, Group, TextInput } from '@mantine/core';
import { CheckIcon } from '@assets/icons/Check.icon';
import { useCoupon } from '@hooks/useCoupon';
import { Button } from '@ui/button';

interface CouponProps {
planCode: string;
couponCode: string | undefined;
setAppliedCouponCode: (couponCode?: string) => void;
}

const Coupon = ({ planCode, couponCode, setAppliedCouponCode }: CouponProps) => {
const { register, errors, applyCouponSubmit, handleSubmit, isApplyCouponLoading } = useCoupon({
planCode,
setAppliedCouponCode,
});

return (
<div>
{couponCode ? (
<Alert
fw="bolder"
color="green"
variant="outline"
withCloseButton
onClose={() => setAppliedCouponCode(undefined)}
icon={<CheckIcon color="white" size="md" />}
>
{`Coupon ${couponCode} is applied!`}
</Alert>
) : (
<form onSubmit={handleSubmit(applyCouponSubmit)}>
<Group spacing={0} align="flex-start">
<TextInput
placeholder="Enter coupon code"
style={{ flexGrow: 1 }}
{...register('couponCode')}
error={errors.couponCode?.message}
/>
<Button type="submit" compact loading={isApplyCouponLoading}>
Apply
</Button>
</Group>
</form>
)}
</div>
);
};

export default Coupon;
1 change: 1 addition & 0 deletions apps/web/components/settings/AddCard/Coupon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Coupon';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Image from 'next/image';
import { Flex, Radio, Stack, Text, useMantineTheme } from '@mantine/core';
import { capitalizeFirstLetter } from '@shared/utils';
import { colors } from '@config';
import React from 'react';

interface PaymentMethodOptionProps {
method: {
paymentMethodId: string;
brand: string;
last4Digits: string;
expMonth: number;
expYear: number;
};
selected: boolean;
onChange: (methodId: string) => void;
}

export default function PaymentMethodOption({ method, selected, onChange }: PaymentMethodOptionProps) {
const theme = useMantineTheme();
const cardBrandsSrc = method.brand.toLowerCase().replaceAll(' ', '_') || 'default';

const handleClick = () => {
onChange(method.paymentMethodId);
};

return (
<div style={{ marginBottom: '20px', cursor: 'pointer' }} onClick={handleClick}>
<Flex
justify="space-between"
align="center"
p="xl"
style={{
border: `1px solid ${selected ? colors.blue : 'transparent'}`,
backgroundColor: selected
? theme.colorScheme === 'dark'
? colors.BGSecondaryDark
: colors.BGSecondaryLight
: 'transparent',
padding: '8px',
}}
>
<Flex align="center" gap="xl">
<Radio value={method.paymentMethodId} id={method.paymentMethodId} checked={selected} />
<Stack spacing={4}>
<Text fw={700} size="md">
{capitalizeFirstLetter(method.brand)} **** {method.last4Digits}
</Text>
<Text size="sm">Expires {`${method.expMonth}/${method.expYear}`}</Text>
</Stack>
</Flex>
<Image width={50} height={30} src={`/images/cards/${cardBrandsSrc}.png`} alt="Card Company" />
</Flex>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Radio } from '@mantine/core';
import { useRouter } from 'next/router';
import { modals } from '@mantine/modals';
import { MODAL_KEYS, ROUTES } from '@config';
import { ICardData } from '@impler/shared';
import { Button } from '@ui/button';

import PaymentMethodOption from './PaymentMethodOption';

interface PaymentMethodsProps {
paymentMethods: ICardData[] | undefined;
selectedPaymentMethod: string | undefined;
handlePaymentMethodChange: (methodId: string) => void;
}

export function PaymentMethods({
paymentMethods,
selectedPaymentMethod,
handlePaymentMethodChange,
}: PaymentMethodsProps) {
const router = useRouter();

const handleAddMoreCard = () => {
modals.close(MODAL_KEYS.SELECT_CARD);
modals.close(MODAL_KEYS.PAYMENT_PLANS);
router.push(ROUTES.ADD_CARD);
};

return (
<>
<Radio.Group
w={480}
name="paymentMethod"
value={selectedPaymentMethod || undefined}
onChange={(event) => handlePaymentMethodChange(event)}
>
{paymentMethods?.map((method) => (
<PaymentMethodOption
key={method.paymentMethodId}
method={method}
selected={selectedPaymentMethod === method.paymentMethodId}
onChange={() => handlePaymentMethodChange(method.paymentMethodId)}
/>
))}
</Radio.Group>

<Button variant="outline" color="yellow" fullWidth onClick={handleAddMoreCard}>
+ Add Card
</Button>
</>
);
}
2 changes: 2 additions & 0 deletions apps/web/components/settings/AddCard/PaymentMethods/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './PaymentMethods';
export * from './PaymentMethodOption';
Loading

0 comments on commit 77d094a

Please sign in to comment.