diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 8990a2020..41d5863a5 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -15,7 +15,7 @@ global.__views_dirname = path.join(global.__static_dirname, '/views'); global.__images_dirname = path.join(global.__static_dirname, '/images'); async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { rawBody: true }); app.setGlobalPrefix('/api'); // create and mount the middleware manually here diff --git a/packages/server/src/modules/PaymentLinks/PaymentLinks.controller.ts b/packages/server/src/modules/PaymentLinks/PaymentLinks.controller.ts index 66686356d..6be5bfb0b 100644 --- a/packages/server/src/modules/PaymentLinks/PaymentLinks.controller.ts +++ b/packages/server/src/modules/PaymentLinks/PaymentLinks.controller.ts @@ -1,5 +1,5 @@ import { Response } from 'express'; -import { Controller, Get, Param, Res } from '@nestjs/common'; +import { Controller, Get, Param, Post, Res } from '@nestjs/common'; import { PaymentLinksApplication } from './PaymentLinksApplication'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @@ -40,7 +40,7 @@ export class PaymentLinksController { return { data }; } - @Get('/:paymentLinkId/stripe_checkout_session') + @Post('/:paymentLinkId/stripe_checkout_session') @ApiOperation({ summary: 'Create Stripe checkout session', description: diff --git a/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts b/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts index 98200fd5c..9af65ee8d 100644 --- a/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts +++ b/packages/server/src/modules/PaymentServices/queries/GetPaymentMethodsState.ts @@ -15,7 +15,7 @@ export class GetPaymentMethodsStateService { private readonly paymentIntegrationModel: TenantModelProxy< typeof PaymentIntegration >, - ) {} + ) { } /** * Retrieves the payment state provising state. diff --git a/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts b/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts index d621f2191..4bfcfaa94 100644 --- a/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts +++ b/packages/server/src/modules/SaleInvoices/queries/GetSaleInvoice.service.ts @@ -1,5 +1,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { SaleInvoice } from '../models/SaleInvoice'; import { SaleInvoiceTransformer } from './SaleInvoice.transformer'; @@ -17,7 +18,7 @@ export class GetSaleInvoice { @Inject(SaleInvoice.name) private saleInvoiceModel: TenantModelProxy, - ) {} + ) { } /** * Retrieve sale invoice with associated entries. @@ -27,9 +28,10 @@ export class GetSaleInvoice { */ public async getSaleInvoice( saleInvoiceId: number, + trx?: Knex.Transaction, ): Promise { const saleInvoice = await this.saleInvoiceModel() - .query() + .query(trx) .findById(saleInvoiceId) .withGraphFetched('entries.item') .withGraphFetched('entries.tax') diff --git a/packages/server/src/modules/StripePayment/CreatePaymentReceivedStripePayment.ts b/packages/server/src/modules/StripePayment/CreatePaymentReceivedStripePayment.ts index 9c18d059c..ccab8c45a 100644 --- a/packages/server/src/modules/StripePayment/CreatePaymentReceivedStripePayment.ts +++ b/packages/server/src/modules/StripePayment/CreatePaymentReceivedStripePayment.ts @@ -12,7 +12,7 @@ export class CreatePaymentReceiveStripePayment { private readonly createPaymentReceivedService: CreatePaymentReceivedService, private readonly uow: UnitOfWork, private readonly accountRepository: AccountRepository, - ) {} + ) { } /** * Creates a payment received transaction associated to the given invoice. @@ -28,7 +28,7 @@ export class CreatePaymentReceiveStripePayment { // Retrieves the given invoice to create payment transaction associated to it. const invoice = - await this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId); + await this.getSaleInvoiceService.getSaleInvoice(saleInvoiceId, trx); const paymentReceivedDTO = { customerId: invoice.customerId, @@ -38,6 +38,7 @@ export class CreatePaymentReceiveStripePayment { referenceNo: '', statement: '', depositAccountId: stripeClearingAccount.id, + branchId: invoice.branchId, entries: [{ invoiceId: saleInvoiceId, paymentAmount: paidAmount }], }; // Create a payment received transaction associated to the given invoice. diff --git a/packages/server/src/modules/StripePayment/GetStripeAuthorizationLink.ts b/packages/server/src/modules/StripePayment/GetStripeAuthorizationLink.ts index 56b34adb8..88a9efe21 100644 --- a/packages/server/src/modules/StripePayment/GetStripeAuthorizationLink.ts +++ b/packages/server/src/modules/StripePayment/GetStripeAuthorizationLink.ts @@ -7,7 +7,7 @@ export class GetStripeAuthorizationLinkService { public getStripeAuthLink() { const clientId = this.config.get('stripePayment.clientId'); - const redirectUrl = this.config.get('stripePayment.redirectTo'); + const redirectUrl = this.config.get('stripePayment.redirectUrl'); const authorizationUri = `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write&redirect_uri=${redirectUrl}`; diff --git a/packages/server/src/modules/StripePayment/StripePayment.module.ts b/packages/server/src/modules/StripePayment/StripePayment.module.ts index 9ff83af2a..503949970 100644 --- a/packages/server/src/modules/StripePayment/StripePayment.module.ts +++ b/packages/server/src/modules/StripePayment/StripePayment.module.ts @@ -3,12 +3,11 @@ import { CreateStripeAccountLinkService } from './CreateStripeAccountLink'; import { CreateStripeAccountService } from './CreateStripeAccountService'; import { StripePaymentApplication } from './StripePaymentApplication'; import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken'; -import { PaymentIntegration } from './models/PaymentIntegration.model'; import { SeedStripeAccountsOnOAuthGrantedSubscriber } from './subscribers/SeedStripeAccounts'; import { StripeWebhooksSubscriber } from './subscribers/StripeWebhooksSubscriber'; import { StripeIntegrationController } from './StripePayment.controller'; +import { StripePaymentWebhooksController } from './StripePaymentWebhooks.controller'; import { StripePaymentService } from './StripePaymentService'; -import { InjectSystemModel } from '../System/SystemModels/SystemModels.module'; import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink'; import { AccountsModule } from '../Accounts/Accounts.module'; import { CreatePaymentReceiveStripePayment } from './CreatePaymentReceivedStripePayment'; @@ -16,8 +15,6 @@ import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module'; import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module'; import { TenancyContext } from '../Tenancy/TenancyContext.service'; -const models = [InjectSystemModel(PaymentIntegration)]; - @Module({ imports: [ AccountsModule, @@ -25,7 +22,6 @@ const models = [InjectSystemModel(PaymentIntegration)]; forwardRef(() => SaleInvoicesModule), ], providers: [ - ...models, StripePaymentService, GetStripeAuthorizationLinkService, CreateStripeAccountLinkService, @@ -38,6 +34,6 @@ const models = [InjectSystemModel(PaymentIntegration)]; TenancyContext, ], exports: [StripePaymentService, GetStripeAuthorizationLinkService], - controllers: [StripeIntegrationController], + controllers: [StripeIntegrationController, StripePaymentWebhooksController], }) -export class StripePaymentModule {} +export class StripePaymentModule { } diff --git a/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts b/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts new file mode 100644 index 000000000..a8bbde9a5 --- /dev/null +++ b/packages/server/src/modules/StripePayment/StripePaymentWebhooks.controller.ts @@ -0,0 +1,118 @@ +import { + Controller, + Headers, + HttpCode, + HttpException, + HttpStatus, + Post, + RawBodyRequest, + Req, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ConfigService } from '@nestjs/config'; +import { StripePaymentService } from './StripePaymentService'; +import { events } from '@/common/events/events'; +import { + StripeCheckoutSessionCompletedEventPayload, + StripeWebhookEventPayload, +} from './StripePayment.types'; +import { PublicRoute } from '../Auth/guards/jwt.guard'; + +@Controller('/webhooks/stripe') +@ApiTags('stripe') +@PublicRoute() +export class StripePaymentWebhooksController { + constructor( + private readonly stripePaymentService: StripePaymentService, + private readonly eventEmitter: EventEmitter2, + private readonly configService: ConfigService, + ) { } + + /** + * Handles incoming Stripe webhook events. + * Verifies the webhook signature, processes the event based on its type, + * and triggers appropriate actions or events in the system. + * @param {Request} req - The Express request object containing the webhook payload. + * @param {Response} res - The Express response object. + * @returns {Promise} + */ + @Post('/') + @HttpCode(200) + @ApiOperation({ summary: 'Listen to Stripe webhooks' }) + async handleWebhook( + @Req() req: RawBodyRequest, + @Headers('stripe-signature') signature: string, + ) { + try { + const rawBody = req.rawBody || req.body; + const webhooksSecret = this.configService.get( + 'stripePayment.webhooksSecret', + ); + if (!webhooksSecret) { + throw new HttpException( + 'Stripe webhook secret is not configured', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + if (!signature) { + throw new HttpException( + 'Stripe signature header is missing', + HttpStatus.BAD_REQUEST, + ); + } + let event; + + // Verify webhook signature and extract the event. + // See https://stripe.com/docs/webhooks#verify-events for more information. + try { + event = this.stripePaymentService.stripe.webhooks.constructEvent( + rawBody, + signature, + webhooksSecret, + ); + } catch (err) { + throw new HttpException( + `Webhook Error: ${err.message}`, + HttpStatus.BAD_REQUEST, + ); + } + // Handle the event based on its type + switch (event.type) { + case 'checkout.session.completed': + // Triggers `onStripeCheckoutSessionCompleted` event. + await this.eventEmitter.emitAsync( + events.stripeWebhooks.onCheckoutSessionCompleted, + { + event, + } as StripeCheckoutSessionCompletedEventPayload, + ); + break; + case 'account.updated': + // Triggers `onStripeAccountUpdated` event. + await this.eventEmitter.emitAsync( + events.stripeWebhooks.onAccountUpdated, + { + event, + } as StripeWebhookEventPayload, + ); + break; + + // Add more cases as needed + default: + console.log(`Unhandled event type ${event.type}`); + } + return { received: true }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Internal server error', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/packages/server/src/modules/StripePayment/subscribers/StripeWebhooksSubscriber.ts b/packages/server/src/modules/StripePayment/subscribers/StripeWebhooksSubscriber.ts index 1250c7d61..012c7359b 100644 --- a/packages/server/src/modules/StripePayment/subscribers/StripeWebhooksSubscriber.ts +++ b/packages/server/src/modules/StripePayment/subscribers/StripeWebhooksSubscriber.ts @@ -1,21 +1,25 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { ClsService } from 'nestjs-cls'; import { CreatePaymentReceiveStripePayment } from '../CreatePaymentReceivedStripePayment'; import { StripeCheckoutSessionCompletedEventPayload, StripeWebhookEventPayload, } from '../StripePayment.types'; -// import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection'; -// import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware'; -import { OnEvent } from '@nestjs/event-emitter'; -import { Inject, Injectable } from '@nestjs/common'; import { events } from '@/common/events/events'; import { PaymentIntegration } from '../models/PaymentIntegration.model'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModel } from '@/modules/System/models/TenantModel'; +import { SystemUser } from '@/modules/System/models/SystemUser'; @Injectable() export class StripeWebhooksSubscriber { constructor( private readonly createPaymentReceiveStripePayment: CreatePaymentReceiveStripePayment, + private readonly clsService: ClsService, + + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, @Inject(PaymentIntegration.name) private readonly paymentIntegrationModel: TenantModelProxy< @@ -23,8 +27,8 @@ export class StripeWebhooksSubscriber { >, @Inject(TenantModel.name) - private readonly tenantModel: typeof TenantModel - ) {} + private readonly tenantModel: typeof TenantModel, + ) { } /** * Handles the checkout session completed webhook event. @@ -38,10 +42,22 @@ export class StripeWebhooksSubscriber { const tenantId = parseInt(metadata.tenantId, 10); const saleInvoiceId = parseInt(metadata.saleInvoiceId, 10); + const tenant = await this.tenantModel + .query() + .findOne({ id: tenantId }) + .throwIfNotFound(); + const user = await this.systemUserModel + .query() + .findOne({ + tenantId: tenant.id, + }) + .modify('active') + .throwIfNotFound(); - // await initalizeTenantServices(tenantId); - // await initializeTenantSettings(tenantId); + this.clsService.set('organizationId', tenant.organizationId); + this.clsService.set('userId', user.id); + this.clsService.set('tenantId', tenant.id); // Get the amount from the event const amount = event.data.object.amount_total; diff --git a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index fdfd9df1a..e16537b95 100644 --- a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -26,6 +26,7 @@ import { BillLandedCostEntry } from '@/modules/BillLandedCosts/models/BillLanded import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost'; import { VendorCreditAppliedBill } from '@/modules/VendorCreditsApplyBills/models/VendorCreditAppliedBill'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; +import { PaymentIntegration } from '@/modules/StripePayment/models/PaymentIntegration.model'; import { PaymentReceivedEntry } from '@/modules/PaymentReceived/models/PaymentReceivedEntry'; import { CreditNoteAppliedInvoice } from '@/modules/CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice'; import { CreditNote } from '@/modules/CreditNotes/models/CreditNote'; @@ -75,6 +76,7 @@ const models = [ VendorCredit, VendorCreditAppliedBill, RefundVendorCredit, + PaymentIntegration, PaymentReceived, PaymentReceivedEntry, TenantUser, diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index c069fd903..1e9fb0537 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -243,7 +243,7 @@ const transformPaymentMethodsToRequest = ( paymentMethods: Record, ): Array<{ payment_integration_id: string; enable: boolean }> => { return Object.entries(paymentMethods).map(([paymentMethodId, method]) => ({ - payment_integration_id: paymentMethodId, + payment_integration_id: +paymentMethodId, enable: method.enable, })); }; diff --git a/packages/webapp/src/hooks/query/stripe-integration.ts b/packages/webapp/src/hooks/query/stripe-integration.ts index b488c1afd..60ffc708b 100644 --- a/packages/webapp/src/hooks/query/stripe-integration.ts +++ b/packages/webapp/src/hooks/query/stripe-integration.ts @@ -37,7 +37,7 @@ export const useCreateStripeAccountLink = ( return useMutation( (values: StripeAccountLinkValues) => { return apiRequest - .post('/stripe_integration/account_link', { + .post('/stripe/account_link', { stripe_account_id: values?.stripeAccountId, }) .then((res) => transformToCamelCase(res.data)); @@ -72,7 +72,7 @@ export const useCreateStripeAccountSession = ( return useMutation( (values: AccountSessionValues) => { return apiRequest - .post('/stripe_integration/account_session', { + .post('/stripe/account_session', { account: values?.connectedAccountId, }) .then((res) => res.data); @@ -100,7 +100,7 @@ export const useCreateStripeAccount = ( return useMutation( (values: CreateStripeAccountValues) => { return apiRequest - .post('/stripe_integration/account') + .post('/stripe/account') .then((res) => res.data); }, { ...options }, @@ -131,7 +131,7 @@ export const useGetStripeAccountLink = ( 'getStripeAccountLink', () => { return apiRequest - .get('/stripe_integration/link') + .get('/stripe/link') .then((res) => transformToCamelCase(res.data)); }, { ...options }, @@ -163,7 +163,7 @@ export const useSetStripeAccountCallback = ( return useMutation( (values: StripeAccountCallbackMutationValues) => { return apiRequest - .post(`/stripe_integration/callback`, values) + .post(`/stripe/callback`, values) .then( (res) => transformToCamelCase(