feat: Stripe payment checkout session

This commit is contained in:
Ahmed Bouhuolia
2024-09-19 22:24:07 +02:00
parent 0ae7a25c27
commit f5a1d68c52
8 changed files with 226 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ import { Service, Inject } from 'typedi';
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { StripeIntegrationApplication } from './StripeIntegrationApplication';
import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication';
@Service()
export class StripeIntegrationController {
@@ -12,6 +13,9 @@ export class StripeIntegrationController {
@Inject()
private stripeIntegrationApp: StripeIntegrationApplication;
@Inject()
private stripePaymentApp: StripePaymentApplication;
router() {
const router = Router();
@@ -20,9 +24,41 @@ export class StripeIntegrationController {
'/account_session',
asyncMiddleware(this.createAccountSession.bind(this))
);
router.post(
'/:linkId/create_checkout_session',
this.createCheckoutSession.bind(this)
);
return router;
}
/**
* Creates a Stripe checkout session for the given payment link id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
public async createCheckoutSession(
req: Request<{ linkId: number }>,
res: Response,
next: NextFunction
) {
const { linkId } = req.params;
const { tenantId } = req;
try {
const session =
await this.stripePaymentApp.createSaleInvoiceCheckoutSession(
tenantId,
linkId
);
return res.status(200).send(session);
} catch (error) {
next(error);
}
}
/**
* Creates a new Stripe account.
* @param {Request} req - The Express request object.

View File

@@ -8,3 +8,10 @@ export interface StripePaymentLinkCreatedEventPayload {
export interface StripeCheckoutSessionCompletedEventPayload {
event: any;
}
export interface StripeInvoiceCheckoutSessionPOJO {
sessionId: string;
publishableKey: string;
redirectTo: string;
}

View File

@@ -0,0 +1,94 @@
import config from '@/config';
import { StripePaymentService } from './StripePaymentService';
import HasTenancyService from '../Tenancy/TenancyService';
import { ISaleInvoice } from '@/interfaces';
import { Inject, Service } from 'typedi';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { PaymentLink } from '@/system/models';
const origin = 'http://localhost';
@Service()
export class CreateInvoiceCheckoutSession {
@Inject()
private stripePaymentService: StripePaymentService;
@Inject()
private tenancy: HasTenancyService;
/**
* Creates a new Stripe checkout session from the given sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<StripeInvoiceCheckoutSessionPOJO>}
*/
async createInvoiceCheckoutSession(
tenantId: number,
publicPaymentLinkId: number
): Promise<StripeInvoiceCheckoutSessionPOJO> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieves the payment link from the given id.
const paymentLink = await PaymentLink.query()
.findOne('linkId', publicPaymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
// Retrieves the invoice from associated payment link.
const invoice = await SaleInvoice.query()
.findById(paymentLink.resourceId)
.withGraphFetched('paymentMethods')
.throwIfNotFound();
// It will be only one Stripe payment method associated to the invoice.
const stripePaymentMethod = invoice.paymentMethods?.find(
(method) => method.paymentIntegration?.service === 'Stripe'
);
const stripeAccountId = stripePaymentMethod?.paymentIntegration?.accountId;
const paymentIntegrationId = stripePaymentMethod?.paymentIntegration?.id;
const session = await this.createCheckoutSession(invoice, stripeAccountId);
return {
sessionId: session.id,
publishableKey: config.stripePayment.publishableKey,
redirectTo: session.url,
};
}
/**
* Creates a new Stripe checkout session for the given sale invoice.
* @param {ISaleInvoice} invoice - The sale invoice for which the checkout session is created.
* @param {string} stripeAccountId - The Stripe account ID associated with the payment method.
* @returns {Promise<any>} - The created Stripe checkout session.
*/
private createCheckoutSession(
invoice: ISaleInvoice,
stripeAccountId: string
) {
return this.stripePaymentService.stripe.checkout.sessions.create(
{
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: invoice.currencyCode,
product_data: {
name: invoice.invoiceNo,
},
unit_amount: invoice.total * 100, // Amount in cents
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
},
{
stripeAccount: stripeAccountId,
// stripeAccount: 'acct_1Q0nE7ESY7RfeebE',
}
);
}
}

View File

@@ -4,6 +4,8 @@ import { CreateStripeAccountService } from '@/api/controllers/StripeIntegration/
import { CreateStripeAccountDTO } from '@/api/controllers/StripeIntegration/types';
import { SaleInvoiceStripePaymentLink } from './SaleInvoiceStripePaymentLink';
import { DeleteStripePaymentLinkInvoice } from './DeleteStripePaymentLinkInvoice';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
export class StripePaymentApplication {
@Inject()
@@ -15,6 +17,9 @@ export class StripePaymentApplication {
@Inject()
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
@Inject()
private createSaleInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
/**
* Creates a new Stripe account for Bigcapital.
* @param {number} tenantId
@@ -49,6 +54,22 @@ export class StripePaymentApplication {
);
}
/**
* Creates the Stripe checkout session from the given sale invoice.
* @param {number} tenantId
* @param {string} paymentLinkId
* @returns {Promise<StripeInvoiceCheckoutSessionPOJO>}
*/
public createSaleInvoiceCheckoutSession(
tenantId: number,
paymentLinkId: number
): Promise<StripeInvoiceCheckoutSessionPOJO> {
return this.createSaleInvoiceCheckoutSessionService.createInvoiceCheckoutSession(
tenantId,
paymentLinkId
);
}
/**
* Deletes the Stripe payment link associated with the given sale invoice.
* @param {number} tenantId - Tenant id.

View File

@@ -1,18 +1,37 @@
import { Text, Classes, Button, Intent } from '@blueprintjs/core';
import clsx from 'classnames';
import { Box, Group, Stack } from '@/components';
import styles from './PaymentPortal.module.scss';
import { AppToaster, Box, Group, Stack } from '@/components';
import { usePaymentPortalBoot } from './PaymentPortalBoot';
import { useDrawerActions } from '@/hooks/state';
import { useCreateStripeCheckoutSession } from '@/hooks/query/stripe-integration';
import { DRAWERS } from '@/constants/drawers';
import styles from './PaymentPortal.module.scss';
export function PaymentPortal() {
const { openDrawer } = useDrawerActions();
const { sharableLinkMeta } = usePaymentPortalBoot();
const { sharableLinkMeta, linkId } = usePaymentPortalBoot();
const {
mutateAsync: createStripeCheckoutSession,
isLoading: isStripeCheckoutLoading,
} = useCreateStripeCheckoutSession();
// Handles invoice preview button click.
const handleInvoicePreviewBtnClick = () => {
openDrawer(DRAWERS.PAYMENT_INVOICE_PREVIEW);
};
// handles the pay button click.
const handlePayButtonClick = () => {
createStripeCheckoutSession({ linkId })
.then((session) => {
window.open(session.redirectTo);
})
.catch(() => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
});
});
};
return (
<Box className={styles.root}>
@@ -91,15 +110,19 @@ export function PaymentPortal() {
>
Download Invoice
</Button>
<Button
onClick={handleInvoicePreviewBtnClick}
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
>
View Invoice
</Button>
<Button
intent={Intent.PRIMARY}
className={clsx(styles.footerButton, styles.buyButton)}
loading={isStripeCheckoutLoading}
onClick={handlePayButtonClick}
>
Pay {sharableLinkMeta?.totalFormatted}
</Button>

View File

@@ -6,6 +6,7 @@ import {
import { Spinner } from '@blueprintjs/core';
interface PaymentPortalContextType {
linkId: string;
sharableLinkMeta: GetSharableLinkMetaResponse | undefined;
isSharableLinkMetaLoading: boolean;
}
@@ -27,6 +28,7 @@ export const PaymentPortalBoot: React.FC<PaymentPortalBootProps> = ({
useGetSharableLinkMeta(linkId);
const value = {
linkId,
sharableLinkMeta,
isSharableLinkMetaLoading,
};

View File

@@ -15,7 +15,6 @@ export function StripeIntegration() {
null,
);
const stripeConnectInstance = useStripeConnect(connectedAccountId || '');
const { mutateAsync: createAccount } = useCreateStripeAccount();
const handleSignupBtnClick = () => {

View File

@@ -5,6 +5,7 @@ import {
UseMutationResult,
} from 'react-query';
import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils';
interface AccountSessionValues {
connectedAccountId?: string;
@@ -57,3 +58,42 @@ export const useCreateStripeAccount = (
{ ...options },
);
};
interface CreateCheckoutSessionValues {
linkId: string;
}
interface CreateCheckoutSessionResponse {
sessionId: string;
publishableKey: string;
redirectTo: string;
}
export const useCreateStripeCheckoutSession = (
options?: UseMutationOptions<
CreateCheckoutSessionResponse,
Error,
CreateCheckoutSessionValues
>,
): UseMutationResult<
CreateCheckoutSessionResponse,
Error,
CreateCheckoutSessionValues
> => {
const apiRequest = useApiRequest();
return useMutation(
(values: CreateCheckoutSessionValues) => {
return apiRequest
.post(
`/stripe_integration/${values.linkId}/create_checkout_session`,
values,
)
.then(
(res) =>
transformToCamelCase(res.data) as CreateCheckoutSessionResponse,
);
},
{ ...options },
);
};