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,47 @@
import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication';
@Service()
export class PaymentServicesController extends BaseController {
@Inject()
private paymentServicesApp: PaymentServicesApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
asyncMiddleware(this.getPaymentServicesSpecificInvoice.bind(this))
);
return router;
}
/**
* Retrieve accounts types list.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Response}
*/
private async getPaymentServicesSpecificInvoice(
req: Request<{ invoiceId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const paymentServices =
await this.paymentServicesApp.getPaymentServicesForInvoice(tenantId);
return res.status(200).send({ paymentServices });
} catch (error) {
next(error);
}
}
}

View File

@@ -258,6 +258,11 @@ export default class SaleInvoicesController extends BaseController {
// Pdf template id. // Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// Payment methods.
check('payment_methods').optional({ nullable: true }).isArray({ min: 1 }),
check('payment_methods.*.payment_integration_id').exists(),
check('payment_methods.*.enable').exists().isBoolean(),
]; ];
} }

View File

@@ -0,0 +1,55 @@
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { CreateStripeAccountDTO } from './types';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreateStripeAccountService {
@Inject()
private stripePaymentService: StripePaymentService;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new Stripe account.
* @param {number} tenantId
* @param {CreateStripeAccountDTO} stripeAccountDTO
* @returns {Promise<string>}
*/
async createStripeAccount(
tenantId: number,
stripeAccountDTO?: CreateStripeAccountDTO
): Promise<string> {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const stripeAccount = await this.stripePaymentService.createAccount();
const stripeAccountId = stripeAccount.id;
const parsedStripeAccountDTO = {
...stripeAccountDTO,
name: 'Stripe',
};
// Stores the details of the Stripe account.
await PaymentIntegration.query().insert({
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

@@ -0,0 +1,24 @@
import { Service, Inject } from 'typedi';
import { CreateStripeAccountService } from './CreateStripeAccountService';
import { CreateStripeAccountDTO } from './types';
@Service()
export class StripeIntegrationApplication {
@Inject()
private createStripeAccountService: CreateStripeAccountService;
/**
* Creates a new Stripe account for the tenant.
* @param {TenantContext} tenantContext - The tenant context.
* @param {string} label - The label for the Stripe account.
* @returns {Promise<string>} The ID of the created Stripe account.
*/
public async createStripeAccount(
tenantId: number,
stripeAccountDTO?: CreateStripeAccountDTO
): Promise<string> {
return this.createStripeAccountService.createStripeAccount(
tenantId,
stripeAccountDTO
);
}
}

View File

@@ -2,12 +2,16 @@ import { NextFunction, Request, Response, Router } from 'express';
import { Service, Inject } from 'typedi'; 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';
@Service() @Service()
export class StripeIntegrationController { export class StripeIntegrationController {
@Inject() @Inject()
private stripePaymentService: StripePaymentService; private stripePaymentService: StripePaymentService;
@Inject()
private stripeIntegrationApp: StripeIntegrationApplication;
router() { router() {
const router = Router(); const router = Router();
@@ -20,9 +24,19 @@ export class StripeIntegrationController {
} }
public async createAccount(req: Request, res: Response, next: NextFunction) { public async createAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try { try {
const accountId = await this.stripePaymentService.createAccount(); const accountId = await this.stripeIntegrationApp.createStripeAccount(
res.status(201).json({ accountId }); tenantId
);
res
.status(201)
.json({
accountId,
message: 'The Stripe account has been created successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

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

View File

@@ -8,7 +8,7 @@ exports.up = function (knex) {
table.string('service'); table.string('service');
table.string('name'); table.string('name');
table.string('slug'); table.string('slug');
table.boolean('enable'); table.boolean('enable').defaultTo(true);
table.string('account_id'); table.string('account_id');
table.json('options'); table.json('options');
table.timestamps(); table.timestamps();

View File

@@ -7,8 +7,14 @@ exports.up = function (knex) {
table.increments('id'); table.increments('id');
table.integer('reference_id').unsigned(); table.integer('reference_id').unsigned();
table.string('reference_type'); table.string('reference_type');
table.integer('integration_id'); table
table.json('options'); .integer('payment_integration_id')
.unsigned()
.index()
.references('id')
.inTable('payment_integrations');
table.boolean('enable').defaultTo(false);
table.json('options').nullable();
}); });
}; };

View File

@@ -5,6 +5,24 @@ import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
export interface PaymentIntegrationTransactionLink {
id: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
}
export interface PaymentIntegrationTransactionLinkEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
saleInvoiceId: number;
trx?: Knex.Transaction
}
export interface ISaleInvoice { export interface ISaleInvoice {
id: number; id: number;
amount: number; amount: number;
@@ -50,6 +68,8 @@ export interface ISaleInvoice {
invoiceMessage: string; invoiceMessage: string;
pdfTemplateId?: number; pdfTemplateId?: number;
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
} }
export interface ISaleInvoiceDTO { export interface ISaleInvoiceDTO {
@@ -223,7 +243,6 @@ export interface ISaleInvoiceMailSent {
messageOptions: SendInvoiceMailDTO; messageOptions: SendInvoiceMailDTO;
} }
// Invoice Pdf Document // Invoice Pdf Document
export interface InvoicePdfLine { export interface InvoicePdfLine {
item: string; item: string;

View File

@@ -0,0 +1,8 @@
export interface StripePaymentLinkCreatedEventPayload {
tenantId: number;
paymentLinkId: string;
saleInvoiceId: number;
stripeIntegrationId: number;
}

View File

@@ -120,6 +120,7 @@ import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/ev
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange'; import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events'; import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
import { CreatePaymentLinkOnInvoiceCreated } from '@/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated'; import { CreatePaymentLinkOnInvoiceCreated } from '@/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated';
import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -293,7 +294,8 @@ export const susbcribers = () => {
SeedInitialDemoAccountDataOnOrgBuild, SeedInitialDemoAccountDataOnOrgBuild,
// Stripe Payment // Stripe Payment
CreatePaymentLinkOnInvoiceCreated CreatePaymentLinkOnInvoiceCreated,
InvoicePaymentIntegrationSubscriber,
...EventsTrackerListeners ...EventsTrackerListeners
]; ];

View File

@@ -69,6 +69,8 @@ import { BankRuleCondition } from '@/models/BankRuleCondition';
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction'; import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction'; import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
import { PdfTemplate } from '@/models/PdfTemplate'; import { PdfTemplate } from '@/models/PdfTemplate';
import { PaymentIntegration } from '@/models/PaymentIntegration';
import { TransactionPaymentServiceEntry } from '@/models/TransactionPaymentServiceEntry';
export default (knex) => { export default (knex) => {
const models = { const models = {
@@ -140,7 +142,9 @@ export default (knex) => {
BankRuleCondition, BankRuleCondition,
RecognizedBankTransaction, RecognizedBankTransaction,
MatchedBankTransaction, MatchedBankTransaction,
PdfTemplate PdfTemplate,
PaymentIntegration,
TransactionPaymentServiceEntry,
}; };
return mapValues(models, (model) => model.bindKnex(knex)); return mapValues(models, (model) => model.bindKnex(knex));
}; };

View File

@@ -1,6 +1,7 @@
import { Model } from 'objection'; import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export class PaymentIntegration extends Model { export class PaymentIntegration extends TenantModel {
static get tableName() { static get tableName() {
return 'payment_integrations'; return 'payment_integrations';
} }
@@ -12,7 +13,7 @@ export class PaymentIntegration extends Model {
static get jsonSchema() { static get jsonSchema() {
return { return {
type: 'object', type: 'object',
required: ['service', 'enable'], required: ['name', 'service', 'enable'],
properties: { properties: {
id: { type: 'integer' }, id: { type: 'integer' },
service: { type: 'string' }, service: { type: 'string' },

View File

@@ -414,8 +414,8 @@ export default class SaleInvoice extends mixin(TenantModel, [
const Document = require('models/Document'); const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const { const {
TransactionPaymentService, TransactionPaymentServiceEntry,
} = require('models/TransactionPaymentService'); } = require('models/TransactionPaymentServiceEntry');
return { return {
/** /**
@@ -577,14 +577,17 @@ export default class SaleInvoice extends mixin(TenantModel, [
}, },
/** /**
* Sale invoice may belongs to payment methods. * Sale invoice may belongs to payment methods entries.
*/ */
paymentMethods: { paymentMethods: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: TransactionPaymentService, modelClass: TransactionPaymentServiceEntry,
join: { join: {
from: 'sales_invoices.id', from: 'sales_invoices.id',
to: 'transactions_payment_services.referenceId', to: 'transactions_payment_methods.referenceId',
},
beforeInsert: (model) => {
model.referenceType = 'SaleInvoice';
}, },
filter: (query) => { filter: (query) => {
query.where('reference_type', 'SaleInvoice'); query.where('reference_type', 'SaleInvoice');

View File

@@ -1,23 +1,25 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
export class TransactionPaymentService extends TenantModel { export class TransactionPaymentServiceEntry extends TenantModel {
/** /**
* Table name * Table name
*/ */
static get tableName() { static get tableName() {
return 'transactions_payment_services'; return 'transactions_payment_methods';
} }
/**
* Json schema of the model.
*/
static get jsonSchema() { static get jsonSchema() {
return { return {
type: 'object', type: 'object',
required: ['service', 'enable'], required: ['paymentIntegrationId'],
properties: { properties: {
id: { type: 'integer' }, id: { type: 'integer' },
reference_id: { type: 'integer' }, referenceId: { type: 'integer' },
reference_type: { type: 'string' }, referenceType: { type: 'string' },
service: { type: 'string' }, paymentIntegrationId: { type: 'integer' },
enable: { type: 'boolean' }, enable: { type: 'boolean' },
options: { type: 'object' }, options: { type: 'object' },
}, },

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

View File

@@ -702,4 +702,17 @@ export default {
onAssignedDefault: 'onPdfTemplateAssignedDefault', onAssignedDefault: 'onPdfTemplateAssignedDefault',
onAssigningDefault: 'onPdfTemplateAssigningDefault', onAssigningDefault: 'onPdfTemplateAssigningDefault',
}, },
// Payment methods integrations
paymentIntegrationLink: {
onPaymentIntegrationLink: 'onPaymentIntegrationLink'
},
// Stripe Payment Integration
stripeIntegration: {
onAccountCreated: 'onStripeIntegrationAccountCreated',
onAccountDeleted: 'onStripeIntegrationAccountDeleted',
onPaymentLinkCreated: 'onStripePaymentLinkCreated',
}
}; };

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'; import { useGetPaymentServices } from '@/hooks/query/payment-services';
import React, { createContext, useContext, ReactNode } from 'react';
interface SelectPaymentMethodsContextType {} interface SelectPaymentMethodsContextType {}
@@ -25,10 +26,16 @@ interface SelectPaymentMethodsProviderProps {
export const SelectPaymentMethodsBoot: React.FC< export const SelectPaymentMethodsBoot: React.FC<
SelectPaymentMethodsProviderProps SelectPaymentMethodsProviderProps
> = ({ children }) => { > = ({ children }) => {
const { isLoading: isPaymentServicesLoading, data: paymentServices } =
useGetPaymentServices();
const value = {
paymentServices,
isPaymentServicesLoading,
};
return ( return (
<SelectPaymentMethodsContext.Provider <SelectPaymentMethodsContext.Provider value={value}>
value={{ }}
>
{children} {children}
</SelectPaymentMethodsContext.Provider> </SelectPaymentMethodsContext.Provider>
); );

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import useApiRequest from '../useRequest';
import { transformToCamelCase } from '@/utils';
const PaymentServicesQueryKey = 'PaymentServices';
export interface GetPaymentServicesResponse {
}
export const useGetPaymentServices = (
options?: UseQueryOptions<GetPaymentServicesResponse, Error>,
): UseQueryResult<GetPaymentServicesResponse, Error> => {
const apiRequest = useApiRequest();
return useQuery<GetPaymentServicesResponse, Error>(
[PaymentServicesQueryKey],
() =>
apiRequest
.get('/payment-services')
.then(
(response) =>
transformToCamelCase(
response.data?.paymentServices,
) as GetPaymentServicesResponse,
),
{
...options,
},
);
};