diff --git a/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts b/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts new file mode 100644 index 000000000..e51ce634d --- /dev/null +++ b/packages/server/src/api/controllers/PaymentServices/PaymentServicesController.ts @@ -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); + } + } +} diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 012b7f041..429879837 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -258,6 +258,11 @@ export default class SaleInvoicesController extends BaseController { // Pdf template id. 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(), ]; } diff --git a/packages/server/src/api/controllers/StripeIntegration/CreateStripeAccountService.ts b/packages/server/src/api/controllers/StripeIntegration/CreateStripeAccountService.ts new file mode 100644 index 000000000..ac2803306 --- /dev/null +++ b/packages/server/src/api/controllers/StripeIntegration/CreateStripeAccountService.ts @@ -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} + */ + async createStripeAccount( + tenantId: number, + stripeAccountDTO?: CreateStripeAccountDTO + ): Promise { + 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; + } +} diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationApplication.ts b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationApplication.ts new file mode 100644 index 000000000..c10eb246b --- /dev/null +++ b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationApplication.ts @@ -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} The ID of the created Stripe account. + */ + public async createStripeAccount( + tenantId: number, + stripeAccountDTO?: CreateStripeAccountDTO + ): Promise { + return this.createStripeAccountService.createStripeAccount( + tenantId, + stripeAccountDTO + ); + } +} diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts index 345e6c062..1efb9d098 100644 --- a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts +++ b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts @@ -2,12 +2,16 @@ import { NextFunction, Request, Response, Router } from 'express'; import { Service, Inject } from 'typedi'; import { StripePaymentService } from '@/services/StripePayment/StripePaymentService'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; +import { StripeIntegrationApplication } from './StripeIntegrationApplication'; @Service() export class StripeIntegrationController { @Inject() private stripePaymentService: StripePaymentService; + @Inject() + private stripeIntegrationApp: StripeIntegrationApplication; + router() { const router = Router(); @@ -20,9 +24,19 @@ export class StripeIntegrationController { } public async createAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + try { - const accountId = await this.stripePaymentService.createAccount(); - res.status(201).json({ accountId }); + const accountId = await this.stripeIntegrationApp.createStripeAccount( + tenantId + ); + + res + .status(201) + .json({ + accountId, + message: 'The Stripe account has been created successfully.', + }); } catch (error) { next(error); } diff --git a/packages/server/src/api/controllers/StripeIntegration/types.ts b/packages/server/src/api/controllers/StripeIntegration/types.ts new file mode 100644 index 000000000..c6f303b9f --- /dev/null +++ b/packages/server/src/api/controllers/StripeIntegration/types.ts @@ -0,0 +1,6 @@ + + + +export interface CreateStripeAccountDTO { + name: string; +} diff --git a/packages/server/src/database/migrations/20240915163722_payment_integration.js b/packages/server/src/database/migrations/20240915155403_payment_integration.js similarity index 88% rename from packages/server/src/database/migrations/20240915163722_payment_integration.js rename to packages/server/src/database/migrations/20240915155403_payment_integration.js index bff641454..98cae0081 100644 --- a/packages/server/src/database/migrations/20240915163722_payment_integration.js +++ b/packages/server/src/database/migrations/20240915155403_payment_integration.js @@ -6,9 +6,9 @@ exports.up = function (knex) { return knex.schema.createTable('payment_integrations', (table) => { table.increments('id'); table.string('service'); - table.string('name'); + table.string('name'); table.string('slug'); - table.boolean('enable'); + table.boolean('enable').defaultTo(true); table.string('account_id'); table.json('options'); table.timestamps(); diff --git a/packages/server/src/database/migrations/20240915155403_creat_transaction_payment_service_table.js b/packages/server/src/database/migrations/20240915163722_creat_transaction_payment_service_table.js similarity index 67% rename from packages/server/src/database/migrations/20240915155403_creat_transaction_payment_service_table.js rename to packages/server/src/database/migrations/20240915163722_creat_transaction_payment_service_table.js index d56754965..17b7fceda 100644 --- a/packages/server/src/database/migrations/20240915155403_creat_transaction_payment_service_table.js +++ b/packages/server/src/database/migrations/20240915163722_creat_transaction_payment_service_table.js @@ -7,8 +7,14 @@ exports.up = function (knex) { table.increments('id'); table.integer('reference_id').unsigned(); table.string('reference_type'); - table.integer('integration_id'); - table.json('options'); + table + .integer('payment_integration_id') + .unsigned() + .index() + .references('id') + .inTable('payment_integrations'); + table.boolean('enable').defaultTo(false); + table.json('options').nullable(); }); }; diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 4493782b8..29521fefe 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -5,6 +5,24 @@ import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; 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 { id: number; amount: number; @@ -50,6 +68,8 @@ export interface ISaleInvoice { invoiceMessage: string; pdfTemplateId?: number; + + paymentMethods?: Array; } export interface ISaleInvoiceDTO { @@ -223,7 +243,6 @@ export interface ISaleInvoiceMailSent { messageOptions: SendInvoiceMailDTO; } - // Invoice Pdf Document export interface InvoicePdfLine { item: string; @@ -241,9 +260,9 @@ export interface InvoicePdfTax { export interface InvoicePdfTemplateAttributes { primaryColor: string; secondaryColor: string; - + companyName: string; - + showCompanyLogo: boolean; companyLogo: string; @@ -301,4 +320,4 @@ export interface InvoicePdfTemplateAttributes { billedToAddress: string[]; billedFromAddres: string[]; -} \ No newline at end of file +} diff --git a/packages/server/src/interfaces/StripePayment.ts b/packages/server/src/interfaces/StripePayment.ts new file mode 100644 index 000000000..98e934e11 --- /dev/null +++ b/packages/server/src/interfaces/StripePayment.ts @@ -0,0 +1,8 @@ + + +export interface StripePaymentLinkCreatedEventPayload { + tenantId: number; + paymentLinkId: string; + saleInvoiceId: number; + stripeIntegrationId: number; +} \ No newline at end of file diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 29851a084..d9e7ec54d 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -120,6 +120,7 @@ import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/ev import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange'; import { EventsTrackerListeners } from '@/services/EventsTracker/events/events'; import { CreatePaymentLinkOnInvoiceCreated } from '@/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated'; +import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber'; export default () => { return new EventPublisher(); @@ -293,7 +294,8 @@ export const susbcribers = () => { SeedInitialDemoAccountDataOnOrgBuild, // Stripe Payment - CreatePaymentLinkOnInvoiceCreated + CreatePaymentLinkOnInvoiceCreated, + InvoicePaymentIntegrationSubscriber, ...EventsTrackerListeners ]; diff --git a/packages/server/src/loaders/tenantModels.ts b/packages/server/src/loaders/tenantModels.ts index 3d349f81e..75de1cef1 100644 --- a/packages/server/src/loaders/tenantModels.ts +++ b/packages/server/src/loaders/tenantModels.ts @@ -69,6 +69,8 @@ import { BankRuleCondition } from '@/models/BankRuleCondition'; import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction'; import { MatchedBankTransaction } from '@/models/MatchedBankTransaction'; import { PdfTemplate } from '@/models/PdfTemplate'; +import { PaymentIntegration } from '@/models/PaymentIntegration'; +import { TransactionPaymentServiceEntry } from '@/models/TransactionPaymentServiceEntry'; export default (knex) => { const models = { @@ -140,7 +142,9 @@ export default (knex) => { BankRuleCondition, RecognizedBankTransaction, MatchedBankTransaction, - PdfTemplate + PdfTemplate, + PaymentIntegration, + TransactionPaymentServiceEntry, }; return mapValues(models, (model) => model.bindKnex(knex)); }; diff --git a/packages/server/src/models/PaymentIntegration.ts b/packages/server/src/models/PaymentIntegration.ts index a84a2d305..856e138c5 100644 --- a/packages/server/src/models/PaymentIntegration.ts +++ b/packages/server/src/models/PaymentIntegration.ts @@ -1,6 +1,7 @@ import { Model } from 'objection'; +import TenantModel from 'models/TenantModel'; -export class PaymentIntegration extends Model { +export class PaymentIntegration extends TenantModel { static get tableName() { return 'payment_integrations'; } @@ -12,7 +13,7 @@ export class PaymentIntegration extends Model { static get jsonSchema() { return { type: 'object', - required: ['service', 'enable'], + required: ['name', 'service', 'enable'], properties: { id: { type: 'integer' }, service: { type: 'string' }, diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 53803620e..025e7edb5 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -414,8 +414,8 @@ export default class SaleInvoice extends mixin(TenantModel, [ const Document = require('models/Document'); const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); const { - TransactionPaymentService, - } = require('models/TransactionPaymentService'); + TransactionPaymentServiceEntry, + } = require('models/TransactionPaymentServiceEntry'); 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: { relation: Model.HasManyRelation, - modelClass: TransactionPaymentService, + modelClass: TransactionPaymentServiceEntry, join: { from: 'sales_invoices.id', - to: 'transactions_payment_services.referenceId', + to: 'transactions_payment_methods.referenceId', + }, + beforeInsert: (model) => { + model.referenceType = 'SaleInvoice'; }, filter: (query) => { query.where('reference_type', 'SaleInvoice'); diff --git a/packages/server/src/models/TransactionPaymentService.ts b/packages/server/src/models/TransactionPaymentServiceEntry.ts similarity index 55% rename from packages/server/src/models/TransactionPaymentService.ts rename to packages/server/src/models/TransactionPaymentServiceEntry.ts index 5025d6683..5d1992634 100644 --- a/packages/server/src/models/TransactionPaymentService.ts +++ b/packages/server/src/models/TransactionPaymentServiceEntry.ts @@ -1,23 +1,25 @@ -import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; -export class TransactionPaymentService extends TenantModel { +export class TransactionPaymentServiceEntry extends TenantModel { /** * Table name */ static get tableName() { - return 'transactions_payment_services'; + return 'transactions_payment_methods'; } + /** + * Json schema of the model. + */ static get jsonSchema() { return { type: 'object', - required: ['service', 'enable'], + required: ['paymentIntegrationId'], properties: { id: { type: 'integer' }, - reference_id: { type: 'integer' }, - reference_type: { type: 'string' }, - service: { type: 'string' }, + referenceId: { type: 'integer' }, + referenceType: { type: 'string' }, + paymentIntegrationId: { type: 'integer' }, enable: { type: 'boolean' }, options: { type: 'object' }, }, diff --git a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts new file mode 100644 index 000000000..6358914a4 --- /dev/null +++ b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts @@ -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() + ); + } +} diff --git a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts new file mode 100644 index 000000000..2b3ea4318 --- /dev/null +++ b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts @@ -0,0 +1,11 @@ +import { Transformer } from '@/lib/Transformer/Transformer'; + +export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer { + /** + * Exclude attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['accountId']; + }; +} diff --git a/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts b/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts new file mode 100644 index 000000000..77ee56252 --- /dev/null +++ b/packages/server/src/services/PaymentServices/PaymentServicesApplication.ts @@ -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} The payment services for the specified invoice. + */ + async getPaymentServicesForInvoice(tenantId: number): Promise { + return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice( + tenantId + ); + } +} diff --git a/packages/server/src/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber.ts b/packages/server/src/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber.ts new file mode 100644 index 000000000..e6e8ede70 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber.ts @@ -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 + ); + } + ); + }; +} diff --git a/packages/server/src/services/StripePayment/SaleInvoiceStripePaymentLink.ts b/packages/server/src/services/StripePayment/SaleInvoiceStripePaymentLink.ts index bf3c875d8..6fa885919 100644 --- a/packages/server/src/services/StripePayment/SaleInvoiceStripePaymentLink.ts +++ b/packages/server/src/services/StripePayment/SaleInvoiceStripePaymentLink.ts @@ -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} */ - 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} + */ + 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} + */ + private async createStripePaymentLink( + priceId: string, + invoice: ISaleInvoice, + stripeAccountId: string, + metadata: Record = {} + ) { + 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, + }); } } diff --git a/packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.tsx b/packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.ts similarity index 68% rename from packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.tsx rename to packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.ts index 971e86d9f..a09be533a 100644 --- a/packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.tsx +++ b/packages/server/src/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated.ts @@ -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 ); - }; } diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 370be2fcc..1631a92c8 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -702,4 +702,17 @@ export default { onAssignedDefault: 'onPdfTemplateAssignedDefault', onAssigningDefault: 'onPdfTemplateAssigningDefault', }, + + // Payment methods integrations + paymentIntegrationLink: { + onPaymentIntegrationLink: 'onPaymentIntegrationLink' + }, + + // Stripe Payment Integration + stripeIntegration: { + onAccountCreated: 'onStripeIntegrationAccountCreated', + onAccountDeleted: 'onStripeIntegrationAccountDeleted', + + onPaymentLinkCreated: 'onStripePaymentLinkCreated', + } }; diff --git a/packages/webapp/src/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsBoot.tsx b/packages/webapp/src/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsBoot.tsx index 5d92657e6..80c5fcb68 100644 --- a/packages/webapp/src/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsBoot.tsx +++ b/packages/webapp/src/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsBoot.tsx @@ -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 {} @@ -25,10 +26,16 @@ interface SelectPaymentMethodsProviderProps { export const SelectPaymentMethodsBoot: React.FC< SelectPaymentMethodsProviderProps > = ({ children }) => { + const { isLoading: isPaymentServicesLoading, data: paymentServices } = + useGetPaymentServices(); + + const value = { + paymentServices, + isPaymentServicesLoading, + }; + return ( - + {children} ); diff --git a/packages/webapp/src/hooks/query/payment-services.ts b/packages/webapp/src/hooks/query/payment-services.ts new file mode 100644 index 000000000..5896172a1 --- /dev/null +++ b/packages/webapp/src/hooks/query/payment-services.ts @@ -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, +): UseQueryResult => { + const apiRequest = useApiRequest(); + + return useQuery( + [PaymentServicesQueryKey], + () => + apiRequest + .get('/payment-services') + .then( + (response) => + transformToCamelCase( + response.data?.paymentServices, + ) as GetPaymentServicesResponse, + ), + { + ...options, + }, + ); +};