mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: Stripe payment checkout session
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -8,3 +8,10 @@ export interface StripePaymentLinkCreatedEventPayload {
|
||||
export interface StripeCheckoutSessionCompletedEventPayload {
|
||||
event: any;
|
||||
}
|
||||
|
||||
|
||||
export interface StripeInvoiceCheckoutSessionPOJO {
|
||||
sessionId: string;
|
||||
publishableKey: string;
|
||||
redirectTo: string;
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ export function StripeIntegration() {
|
||||
null,
|
||||
);
|
||||
const stripeConnectInstance = useStripeConnect(connectedAccountId || '');
|
||||
|
||||
const { mutateAsync: createAccount } = useCreateStripeAccount();
|
||||
|
||||
const handleSignupBtnClick = () => {
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user