feat: clean up the stripe payment integration

This commit is contained in:
Ahmed Bouhuolia
2024-09-21 09:18:39 +02:00
parent f5a1d68c52
commit 11c56c75a4
18 changed files with 3144 additions and 484 deletions

View File

@@ -47,8 +47,11 @@ export class CreateInvoiceCheckoutSession {
const stripeAccountId = stripePaymentMethod?.paymentIntegration?.accountId;
const paymentIntegrationId = stripePaymentMethod?.paymentIntegration?.id;
const session = await this.createCheckoutSession(invoice, stripeAccountId);
// Creates checkout session for the given invoice.
const session = await this.createCheckoutSession(invoice, stripeAccountId, {
tenantId,
paymentLinkId: paymentLink.id,
});
return {
sessionId: session.id,
publishableKey: config.stripePayment.publishableKey,
@@ -64,7 +67,8 @@ export class CreateInvoiceCheckoutSession {
*/
private createCheckoutSession(
invoice: ISaleInvoice,
stripeAccountId: string
stripeAccountId: string,
metadata?: Record<string, any>
) {
return this.stripePaymentService.stripe.checkout.sessions.create(
{
@@ -84,11 +88,13 @@ export class CreateInvoiceCheckoutSession {
mode: 'payment',
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
metadata: {
saleInvoiceId: invoice.id,
resource: 'SaleInvoice',
...metadata,
},
},
{
stripeAccount: stripeAccountId,
// stripeAccount: 'acct_1Q0nE7ESY7RfeebE',
}
{ stripeAccount: stripeAccountId }
);
}
}

View File

@@ -1,11 +1,9 @@
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { snakeCase } from 'lodash';
import { StripePaymentService } from './StripePaymentService';
import HasTenancyService from '../Tenancy/TenancyService';
interface CreateStripeAccountDTO {
name: string;
}
import { CreateStripeAccountDTO } from './types';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreateStripeAccountService {
@@ -15,29 +13,43 @@ export class CreateStripeAccountService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new Stripe account for Bigcapital.
* @param {number} tenantId
* @param {number} createStripeAccountDTO
* Creates a new Stripe account.
* @param {number} tenantId
* @param {CreateStripeAccountDTO} stripeAccountDTO
* @returns {Promise<string>}
*/
async createAccount(
async createStripeAccount(
tenantId: number,
createStripeAccountDTO: CreateStripeAccountDTO
) {
stripeAccountDTO?: CreateStripeAccountDTO
): Promise<string> {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const stripeAccount = await this.stripePaymentService.createAccount();
const stripeAccountId = stripeAccount.id;
// Creates a new Stripe account.
const account = await this.stripePaymentService.createAccount();
const slug = snakeCase(createStripeAccountDTO.name);
// Store the Stripe account on tenant store.
const parsedStripeAccountDTO = {
...stripeAccountDTO,
name: 'Stripe',
};
// Stores the details of the Stripe account.
await PaymentIntegration.query().insert({
service: 'stripe',
name: createStripeAccountDTO.name,
slug,
enable: true,
accountId: account.id,
name: parsedStripeAccountDTO.name,
accountId: stripeAccountId,
enable: false,
service: 'Stripe',
});
// Triggers `onStripeIntegrationAccountCreated` event.
await this.eventPublisher.emitAsync(
events.stripeIntegration.onAccountCreated,
{
tenantId,
stripeAccountDTO,
stripeAccountId,
}
);
return stripeAccountId;
}
}

View File

@@ -1,65 +0,0 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { StripePaymentService } from './StripePaymentService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class DeleteStripePaymentLinkInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private stripePayment: StripePaymentService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the Stripe payment link associates to the given sale invoice.
* @param {number} tenantId -
* @param {number} invoiceId -
* @param {Knex.Transaction} knex -
*/
async deleteInvoicePaymentLink(
tenantId: number,
invoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoice = await SaleInvoice.query(trx)
.findById(invoiceId)
.withGraphFetched('paymentMethods.paymentIntegration')
.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;
if (invoice.stripePlinkId && stripeAccountId) {
const stripeAcocunt = { stripeAccount: stripeAccountId };
const stripePlinkId = invoice.stripePlinkId;
await this.stripePayment.stripe.paymentLinks.update(
stripePlinkId,
{ active: false },
stripeAcocunt
);
// Triggers `onStripePaymentLinkInactivated` event.
await this.eventPublisher.emitAsync(
events.stripeIntegration.onPaymentLinkInactivated,
{
tenantId,
saleInvoiceId: invoiceId,
paymentIntegrationId,
stripePlinkId,
}
);
}
}
}

View File

@@ -1,145 +0,0 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice } from '@/interfaces';
import { StripePaymentService } from './StripePaymentService';
import HasTenancyService from '../Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { StripePaymentLinkCreatedEventPayload } from '@/interfaces/StripePayment';
import { STRIPE_PAYMENT_LINK_REDIRECT } from './constants';
import events from '@/subscribers/events';
@Service()
export class SaleInvoiceStripePaymentLink {
@Inject()
private stripePayment: StripePaymentService;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a Stripe payment link for the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} stripeIntegrationId - Stripe integration id.
* @param {ISaleInvoice} saleInvoice - Sale invoice id.
* @returns {Promise<string>}
*/
async createPaymentLink(
tenantId: number,
stripeIntegrationId: number,
invoiceId: number
) {
const { SaleInvoice, PaymentIntegration } = this.tenancy.models(tenantId);
const stripeIntegration = await PaymentIntegration.query()
.findById(stripeIntegrationId)
.throwIfNotFound();
const stripeAccountId = stripeIntegration.accountId;
const invoice = await SaleInvoice.query()
.findById(invoiceId)
.throwIfNotFound();
// Creates Stripe price.
const price = await this.createStripePrice(invoice, stripeAccountId);
// Creates Stripe payment link.
const paymentLink = await this.createStripePaymentLink(
price.id,
invoice,
stripeAccountId,
{ tenantId }
);
// Associate the payment link id to the invoice.
await this.updateInvoiceWithPaymentLink(
tenantId,
invoiceId,
paymentLink.id
);
// Triggers `onStripePaymentLinkCreated` event.
await this.eventPublisher.emitAsync(
events.stripeIntegration.onPaymentLinkCreated,
{
tenantId,
stripeIntegrationId,
saleInvoiceId: invoiceId,
paymentLinkId: paymentLink.id,
} as StripePaymentLinkCreatedEventPayload
);
return paymentLink.id;
}
/**
* Creates a Stripe price for the invoice.
* @param {ISaleInvoice} invoice - Sale invoice.
* @param {string} stripeAccountId - Stripe account id.
* @returns {Promise<Stripe.Price>}
*/
private async createStripePrice(
invoice: ISaleInvoice,
stripeAccountId: string
) {
return this.stripePayment.stripe.prices.create(
{
unit_amount: invoice.total * 100,
currency: 'usd',
product_data: {
name: invoice.invoiceNo,
},
},
{ stripeAccount: stripeAccountId }
);
}
/**
* Creates a Stripe payment link.
* @param {string} priceId - Stripe price id.
* @param {ISaleInvoice} invoice - Sale invoice.
* @param {number} tenantId - Tenant id.
* @param {string} stripeAccountId - Stripe account id.
* @returns {Promise<Stripe.PaymentLink>}
*/
private async createStripePaymentLink(
priceId: string,
invoice: ISaleInvoice,
stripeAccountId: string,
metadata: Record<string, any> = {}
) {
const paymentLinkInfo = {
line_items: [{ price: priceId, quantity: 1 }],
after_completion: {
type: 'redirect',
redirect: {
url: STRIPE_PAYMENT_LINK_REDIRECT,
},
},
metadata: {
saleInvoiceId: invoice.id,
resource: 'SaleInvoice',
...metadata,
},
};
return this.stripePayment.stripe.paymentLinks.create(paymentLinkInfo, {
stripeAccount: stripeAccountId,
});
}
/**
* Updates the sale invoice with the Stripe payment link id.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Sale invoice id.
* @param {string} paymentLinkId - Stripe payment link id.
*/
private async updateInvoiceWithPaymentLink(
tenantId: number,
invoiceId: number,
paymentLinkId: string
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
await SaleInvoice.query().findById(invoiceId).patch({
stripePlinkId: paymentLinkId,
});
}
}

View File

@@ -1,22 +1,13 @@
import { Knex } from 'knex';
import { Inject } from 'typedi';
import { CreateStripeAccountService } from '@/api/controllers/StripeIntegration/CreateStripeAccountService';
import { CreateStripeAccountDTO } from '@/api/controllers/StripeIntegration/types';
import { SaleInvoiceStripePaymentLink } from './SaleInvoiceStripePaymentLink';
import { DeleteStripePaymentLinkInvoice } from './DeleteStripePaymentLinkInvoice';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { CreateStripeAccountService } from './CreateStripeAccountService';
import { CreateStripeAccountDTO } from './types';
export class StripePaymentApplication {
@Inject()
private createStripeAccountService: CreateStripeAccountService;
@Inject()
private saleInvoiceStripePaymentLinkService: SaleInvoiceStripePaymentLink;
@Inject()
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
@Inject()
private createSaleInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
@@ -35,25 +26,6 @@ export class StripePaymentApplication {
);
}
/**
* Creates a Stripe payment link for the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} stripeIntegrationId - Stripe integration id.
* @param {ISaleInvoice} saleInvoice - Sale invoice id.
* @returns {Promise<string>}
*/
public createSaleInvoicePaymentLink(
tenantId: number,
stripeIntegrationId: number,
invoiceId: number
) {
return this.saleInvoiceStripePaymentLinkService.createPaymentLink(
tenantId,
stripeIntegrationId,
invoiceId
);
}
/**
* Creates the Stripe checkout session from the given sale invoice.
* @param {number} tenantId
@@ -69,22 +41,4 @@ export class StripePaymentApplication {
paymentLinkId
);
}
/**
* Deletes the Stripe payment link associated with the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Sale invoice id.
* @returns {Promise<void>}
*/
public deleteInvoicePaymentLink(
tenantId: number,
invoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
return this.deleteStripePaymentLinkInvoice.deleteInvoicePaymentLink(
tenantId,
invoiceId,
trx
);
}
}

View File

@@ -8,7 +8,7 @@ export class StripePaymentService {
constructor() {
this.stripe = new stripe(config.stripePayment.secretKey, {
apiVersion: '2023-10-16',
apiVersion: '2024-06-20',
});
}

View File

@@ -1,73 +0,0 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
PaymentIntegrationTransactionLinkDeleteEventPayload,
PaymentIntegrationTransactionLinkEventPayload,
} from '@/interfaces';
import { SaleInvoiceStripePaymentLink } from '../SaleInvoiceStripePaymentLink';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
import events from '@/subscribers/events';
import { DeleteStripePaymentLinkInvoice } from '../DeleteStripePaymentLinkInvoice';
@Service()
export class CreatePaymentLinkOnInvoiceCreated extends EventSubscriber {
@Inject()
private invoiceStripePaymentLink: SaleInvoiceStripePaymentLink;
@Inject()
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentIntegrationLink.onPaymentIntegrationLink,
this.handleCreatePaymentLinkOnIntegrationLink
);
bus.subscribe(
events.paymentIntegrationLink.onPaymentIntegrationDeleteLink,
this.handleDeletePaymentLinkOnIntegrationLinkDelete
);
}
/**
* Updates the Plaid item transactions
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
*/
private handleCreatePaymentLinkOnIntegrationLink = async ({
tenantId,
paymentIntegrationId,
referenceId,
referenceType,
trx,
}: PaymentIntegrationTransactionLinkEventPayload) => {
// Can't continue if the link request is not coming from the invoice transaction.
if ('SaleInvoice' !== referenceType) {
return;
}
runAfterTransaction(trx, async () => {
await this.invoiceStripePaymentLink.createPaymentLink(
tenantId,
paymentIntegrationId,
referenceId
);
});
};
/**
* Deletes the Stripe payment link once the associated invoice deleted.
* @param {ISaleInvoiceDeletedPayload}
*/
private handleDeletePaymentLinkOnIntegrationLinkDelete = async ({
oldSaleInvoiceId,
tenantId,
trx,
}: PaymentIntegrationTransactionLinkDeleteEventPayload) => {
await this.deleteStripePaymentLinkInvoice.deleteInvoicePaymentLink(
tenantId,
oldSaleInvoiceId,
trx
);
};
}

View File

@@ -0,0 +1,6 @@
export interface CreateStripeAccountDTO {
name: string;
}