feat: integrate Stripe payment to invoices

This commit is contained in:
Ahmed Bouhuolia
2024-09-18 19:24:01 +02:00
parent df706d2573
commit 4665f529e6
24 changed files with 540 additions and 80 deletions

View File

@@ -0,0 +1,33 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetPaymentServicesSpecificInvoiceTransformer } from './GetPaymentServicesSpecificInvoiceTransformer';
@Service()
export class GetPaymentServicesSpecificInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transform: TransformerInjectable;
/**
* Retrieves the payment services of the given invoice.
* @param {number} tenantId
* @param {number} invoiceId
* @returns
*/
async getPaymentServicesInvoice(tenantId: number) {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const paymentGateways = await PaymentIntegration.query()
.where('enable', true)
.orderBy('name', 'ASC');
return this.transform.transform(
tenantId,
paymentGateways,
new GetPaymentServicesSpecificInvoiceTransformer()
);
}
}

View File

@@ -0,0 +1,11 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer {
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['accountId'];
};
}

View File

@@ -0,0 +1,20 @@
import { Service, Inject } from 'typedi';
import { GetPaymentServicesSpecificInvoice } from './GetPaymentServicesSpecificInvoice';
@Service()
export class PaymentServicesApplication {
@Inject()
private getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice;
/**
* Retrieves the payment services for a specific invoice.
* @param {number} tenantId - The ID of the tenant.
* @param {number} invoiceId - The ID of the invoice.
* @returns {Promise<any>} The payment services for the specified invoice.
*/
async getPaymentServicesForInvoice(tenantId: number): Promise<any> {
return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice(
tenantId
);
}
}

View File

@@ -0,0 +1,57 @@
import { Service, Inject } from 'typedi';
import { omit } from 'lodash';
import events from '@/subscribers/events';
import {
ISaleInvoiceCreatedPayload,
PaymentIntegrationTransactionLink,
PaymentIntegrationTransactionLinkEventPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export class InvoicePaymentIntegrationSubscriber {
@Inject()
private eventPublisher: EventPublisher;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleCreatePaymentIntegrationEvents
);
return bus;
};
/**
* Handles the creation of payment integration events when a sale invoice is created.
* This method filters enabled payment methods from the invoice and emits a payment
* integration link event for each method.
* @param {ISaleInvoiceCreatedPayload} payload - The payload containing sale invoice creation details.
*/
private handleCreatePaymentIntegrationEvents = ({
tenantId,
saleInvoiceDTO,
saleInvoice,
trx,
}: ISaleInvoiceCreatedPayload) => {
const paymentMethods =
saleInvoice.paymentMethods?.filter((method) => method.enable) || [];
paymentMethods.map(
async (paymentMethod: PaymentIntegrationTransactionLink) => {
const payload = {
...omit(paymentMethod, ['id']),
tenantId,
saleInvoiceId: saleInvoice.id,
trx,
};
await this.eventPublisher.emitAsync(
events.paymentIntegrationLink.onPaymentIntegrationLink,
payload as PaymentIntegrationTransactionLinkEventPayload
);
}
);
};
}

View File

@@ -1,8 +1,11 @@
import { Inject, Service } from 'typedi';
import { ISaleInvoice } from '@/interfaces';
import { StripePaymentService } from './StripePaymentService';
import { Inject, Service } from 'typedi';
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 {
@@ -12,48 +15,131 @@ export class SaleInvoiceStripePaymentLink {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a Stripe payment link for the given sale invoice.
* @param {number} tenantId
* @param {ISaleInvoice} saleInvoice
* @param {number} tenantId - Tenant id.
* @param {number} stripeIntegrationId - Stripe integration id.
* @param {ISaleInvoice} saleInvoice - Sale invoice id.
* @returns {Promise<string>}
*/
async createPaymentLink(tenantId: number, saleInvoice: ISaleInvoice) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoiceId = saleInvoice.id;
async createPaymentLink(
tenantId: number,
stripeIntegrationId: number,
invoiceId: number
) {
const { SaleInvoice, PaymentIntegration } = this.tenancy.models(tenantId);
try {
const stripeAcocunt = { stripeAccount: 'acct_1Px3dSPjeOqFxnPw' };
const price = await this.stripePayment.stripe.prices.create(
{
unit_amount: saleInvoice.total * 100,
currency: 'usd',
product_data: {
name: saleInvoice.invoiceNo,
},
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,
},
stripeAcocunt
);
const paymentLinkInfo = {
line_items: [{ price: price.id, quantity: 1 }],
after_completion: {
type: 'redirect',
redirect: {
url: STRIPE_PAYMENT_LINK_REDIRECT,
},
},
{ 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, tenantId, resource: 'SaleInvoice' },
};
const paymentLink = await this.stripePayment.stripe.paymentLinks.create(
paymentLinkInfo,
stripeAcocunt
);
await SaleInvoice.query().findById(saleInvoiceId).patch({
stripePlinkId: paymentLink.id,
});
return paymentLink.id;
} catch (error) {
console.error('Error creating payment link:', error);
}
},
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,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
PaymentIntegrationTransactionLinkEventPayload,
} from '@/interfaces';
import { SaleInvoiceStripePaymentLink } from '../SaleInvoiceStripePaymentLink';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
@@ -22,29 +22,35 @@ export class CreatePaymentLinkOnInvoiceCreated extends EventSubscriber {
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleUpdateTransactionsOnItemCreated
);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleDeletePaymentLinkOnInvoiceDeleted
events.paymentIntegrationLink.onPaymentIntegrationLink,
this.handleCreatePaymentLinkOnIntegrationLink
);
// bus.subscribe(
// events.saleInvoice.onDeleted,
// this.handleDeletePaymentLinkOnInvoiceDeleted
// );
}
/**
* Updates the Plaid item transactions
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
*/
private handleUpdateTransactionsOnItemCreated = async ({
saleInvoice,
saleInvoiceId,
private handleCreatePaymentLinkOnIntegrationLink = async ({
tenantId,
paymentIntegrationId,
referenceId,
referenceType,
trx,
}: ISaleInvoiceCreatedPayload) => {
}: 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,
saleInvoice
paymentIntegrationId,
referenceId
);
});
};
@@ -61,6 +67,5 @@ export class CreatePaymentLinkOnInvoiceCreated extends EventSubscriber {
tenantId,
saleInvoiceId
);
};
}