mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +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 { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import { StripeIntegrationApplication } from './StripeIntegrationApplication';
|
import { StripeIntegrationApplication } from './StripeIntegrationApplication';
|
||||||
|
import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class StripeIntegrationController {
|
export class StripeIntegrationController {
|
||||||
@@ -12,6 +13,9 @@ export class StripeIntegrationController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private stripeIntegrationApp: StripeIntegrationApplication;
|
private stripeIntegrationApp: StripeIntegrationApplication;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private stripePaymentApp: StripePaymentApplication;
|
||||||
|
|
||||||
router() {
|
router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -20,9 +24,41 @@ export class StripeIntegrationController {
|
|||||||
'/account_session',
|
'/account_session',
|
||||||
asyncMiddleware(this.createAccountSession.bind(this))
|
asyncMiddleware(this.createAccountSession.bind(this))
|
||||||
);
|
);
|
||||||
|
router.post(
|
||||||
|
'/:linkId/create_checkout_session',
|
||||||
|
this.createCheckoutSession.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
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.
|
* Creates a new Stripe account.
|
||||||
* @param {Request} req - The Express request object.
|
* @param {Request} req - The Express request object.
|
||||||
|
|||||||
@@ -8,3 +8,10 @@ export interface StripePaymentLinkCreatedEventPayload {
|
|||||||
export interface StripeCheckoutSessionCompletedEventPayload {
|
export interface StripeCheckoutSessionCompletedEventPayload {
|
||||||
event: any;
|
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 { CreateStripeAccountDTO } from '@/api/controllers/StripeIntegration/types';
|
||||||
import { SaleInvoiceStripePaymentLink } from './SaleInvoiceStripePaymentLink';
|
import { SaleInvoiceStripePaymentLink } from './SaleInvoiceStripePaymentLink';
|
||||||
import { DeleteStripePaymentLinkInvoice } from './DeleteStripePaymentLinkInvoice';
|
import { DeleteStripePaymentLinkInvoice } from './DeleteStripePaymentLinkInvoice';
|
||||||
|
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
|
||||||
|
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
|
||||||
|
|
||||||
export class StripePaymentApplication {
|
export class StripePaymentApplication {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -15,6 +17,9 @@ export class StripePaymentApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
|
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private createSaleInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Stripe account for Bigcapital.
|
* Creates a new Stripe account for Bigcapital.
|
||||||
* @param {number} tenantId
|
* @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.
|
* Deletes the Stripe payment link associated with the given sale invoice.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
import { Text, Classes, Button, Intent } from '@blueprintjs/core';
|
import { Text, Classes, Button, Intent } from '@blueprintjs/core';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { Box, Group, Stack } from '@/components';
|
import { AppToaster, Box, Group, Stack } from '@/components';
|
||||||
import styles from './PaymentPortal.module.scss';
|
|
||||||
import { usePaymentPortalBoot } from './PaymentPortalBoot';
|
import { usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||||
import { useDrawerActions } from '@/hooks/state';
|
import { useDrawerActions } from '@/hooks/state';
|
||||||
|
import { useCreateStripeCheckoutSession } from '@/hooks/query/stripe-integration';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import styles from './PaymentPortal.module.scss';
|
||||||
|
|
||||||
export function PaymentPortal() {
|
export function PaymentPortal() {
|
||||||
const { openDrawer } = useDrawerActions();
|
const { openDrawer } = useDrawerActions();
|
||||||
const { sharableLinkMeta } = usePaymentPortalBoot();
|
const { sharableLinkMeta, linkId } = usePaymentPortalBoot();
|
||||||
|
const {
|
||||||
|
mutateAsync: createStripeCheckoutSession,
|
||||||
|
isLoading: isStripeCheckoutLoading,
|
||||||
|
} = useCreateStripeCheckoutSession();
|
||||||
|
|
||||||
|
// Handles invoice preview button click.
|
||||||
const handleInvoicePreviewBtnClick = () => {
|
const handleInvoicePreviewBtnClick = () => {
|
||||||
openDrawer(DRAWERS.PAYMENT_INVOICE_PREVIEW);
|
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 (
|
return (
|
||||||
<Box className={styles.root}>
|
<Box className={styles.root}>
|
||||||
@@ -91,15 +110,19 @@ export function PaymentPortal() {
|
|||||||
>
|
>
|
||||||
Download Invoice
|
Download Invoice
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleInvoicePreviewBtnClick}
|
onClick={handleInvoicePreviewBtnClick}
|
||||||
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
|
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
|
||||||
>
|
>
|
||||||
View Invoice
|
View Invoice
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
className={clsx(styles.footerButton, styles.buyButton)}
|
className={clsx(styles.footerButton, styles.buyButton)}
|
||||||
|
loading={isStripeCheckoutLoading}
|
||||||
|
onClick={handlePayButtonClick}
|
||||||
>
|
>
|
||||||
Pay {sharableLinkMeta?.totalFormatted}
|
Pay {sharableLinkMeta?.totalFormatted}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { Spinner } from '@blueprintjs/core';
|
import { Spinner } from '@blueprintjs/core';
|
||||||
|
|
||||||
interface PaymentPortalContextType {
|
interface PaymentPortalContextType {
|
||||||
|
linkId: string;
|
||||||
sharableLinkMeta: GetSharableLinkMetaResponse | undefined;
|
sharableLinkMeta: GetSharableLinkMetaResponse | undefined;
|
||||||
isSharableLinkMetaLoading: boolean;
|
isSharableLinkMetaLoading: boolean;
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ export const PaymentPortalBoot: React.FC<PaymentPortalBootProps> = ({
|
|||||||
useGetSharableLinkMeta(linkId);
|
useGetSharableLinkMeta(linkId);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
|
linkId,
|
||||||
sharableLinkMeta,
|
sharableLinkMeta,
|
||||||
isSharableLinkMetaLoading,
|
isSharableLinkMetaLoading,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export function StripeIntegration() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const stripeConnectInstance = useStripeConnect(connectedAccountId || '');
|
const stripeConnectInstance = useStripeConnect(connectedAccountId || '');
|
||||||
|
|
||||||
const { mutateAsync: createAccount } = useCreateStripeAccount();
|
const { mutateAsync: createAccount } = useCreateStripeAccount();
|
||||||
|
|
||||||
const handleSignupBtnClick = () => {
|
const handleSignupBtnClick = () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
UseMutationResult,
|
UseMutationResult,
|
||||||
} from 'react-query';
|
} from 'react-query';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
interface AccountSessionValues {
|
interface AccountSessionValues {
|
||||||
connectedAccountId?: string;
|
connectedAccountId?: string;
|
||||||
@@ -57,3 +58,42 @@ export const useCreateStripeAccount = (
|
|||||||
{ ...options },
|
{ ...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