mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
feat: integrate Stripe payment to invoices
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface CreateStripeAccountDTO {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
8
packages/server/src/interfaces/StripePayment.ts
Normal file
8
packages/server/src/interfaces/StripePayment.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface StripePaymentLinkCreatedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
paymentLinkId: string;
|
||||||
|
saleInvoiceId: number;
|
||||||
|
stripeIntegrationId: number;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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' },
|
||||||
},
|
},
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Exclude attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['accountId'];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
32
packages/webapp/src/hooks/query/payment-services.ts
Normal file
32
packages/webapp/src/hooks/query/payment-services.ts
Normal 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user