diff --git a/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts b/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts index 91590d6a9..fe68f14a1 100644 --- a/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts +++ b/packages/server/src/api/controllers/ShareLink/PublicSharableLinkController.ts @@ -1,13 +1,13 @@ import { Inject, Service } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; -import { body, param } from 'express-validator'; +import { param } from 'express-validator'; import BaseController from '@/api/controllers/BaseController'; -import { GetInvoicePaymentLinkMetadata } from '@/services/Sales/Invoices/GetInvoicePaymentLinkMetadata'; +import { PaymentLinksApplication } from '@/services/PaymentLinks/PaymentLinksApplication'; @Service() export class PublicSharableLinkController extends BaseController { @Inject() - private getSharableLinkMetaService: GetInvoicePaymentLinkMetadata; + private paymentLinkApp: PaymentLinksApplication; /** * Router constructor. @@ -16,12 +16,18 @@ export class PublicSharableLinkController extends BaseController { const router = Router(); router.get( - '/sharable-links/meta/invoice/:linkId', - [param('linkId').exists()], + '/:paymentLinkId/invoice', + [param('paymentLinkId').exists()], this.validationResult, this.getPaymentLinkPublicMeta.bind(this), this.validationResult ); + router.post( + '/:paymentLinkId/stripe_checkout_session', + [param('paymentLinkId').exists()], + this.validationResult, + this.createInvoicePaymentLinkCheckoutSession.bind(this) + ); return router; } @@ -33,19 +39,45 @@ export class PublicSharableLinkController extends BaseController { * @returns */ public async getPaymentLinkPublicMeta( - req: Request, + req: Request<{ paymentLinkId: string }>, res: Response, next: NextFunction ) { - const { linkId } = req.params; + const { paymentLinkId } = req.params; try { - const data = - await this.getSharableLinkMetaService.getInvoicePaymentLinkMeta(linkId); + const data = await this.paymentLinkApp.getInvoicePaymentLink( + paymentLinkId + ); return res.status(200).send({ data }); } catch (error) { next(error); } } + + /** + * Creates a Stripe checkout session for the given payment link id. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + public async createInvoicePaymentLinkCheckoutSession( + req: Request<{ paymentLinkId: string }>, + res: Response, + next: NextFunction + ) { + const { paymentLinkId } = req.params; + + try { + const session = + await this.paymentLinkApp.createInvoicePaymentCheckoutSession( + paymentLinkId + ); + return res.status(200).send(session); + } catch (error) { + next(error); + } + } } diff --git a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts index 58940e5ec..bd992a77e 100644 --- a/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts +++ b/packages/server/src/api/controllers/StripeIntegration/StripeIntegrationController.ts @@ -27,10 +27,6 @@ export class StripeIntegrationController extends BaseController { this.validationResult, asyncMiddleware(this.createAccountLink.bind(this)) ); - router.post( - '/:linkId/create_checkout_session', - this.createCheckoutSession.bind(this) - ); return router; } @@ -75,33 +71,6 @@ export class StripeIntegrationController extends BaseController { } } - /** - * Creates a Stripe checkout session for the given payment link id. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - * @returns {Promise} - */ - public async createCheckoutSession( - req: Request<{ linkId: number }>, - res: Response, - next: NextFunction - ) { - const { linkId } = req.params; - const { tenantId } = req; - - try { - const session = - await this.stripePaymentApp.createSaleInvoiceCheckoutSession( - tenantId, - linkId - ); - return res.status(200).send(session); - } catch (error) { - next(error); - } - } - /** * Creates a new Stripe account. * @param {Request} req - The Express request object. diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 7de8d9398..c1a890587 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -87,7 +87,10 @@ export default () => { app.use('/account', Container.get(Account).router()); app.use('/webhooks', Container.get(Webhooks).router()); app.use('/demo', Container.get(OneClickDemoController).router()); - app.use(Container.get(PublicSharableLinkController).router()); + app.use( + '/payment-links', + Container.get(PublicSharableLinkController).router() + ); // - Dashboard routes. // --------------------------- diff --git a/packages/server/src/api/middleware/TenantDependencyInjection.ts b/packages/server/src/api/middleware/TenantDependencyInjection.ts index 56c38cf43..8447db780 100644 --- a/packages/server/src/api/middleware/TenantDependencyInjection.ts +++ b/packages/server/src/api/middleware/TenantDependencyInjection.ts @@ -50,7 +50,8 @@ export const injectI18nUtils = (req) => { export const initalizeTenantServices = async (tenantId: number) => { const tenant = await Tenant.query() .findById(tenantId) - .withGraphFetched('metadata'); + .withGraphFetched('metadata') + .throwIfNotFound(); const tenantServices = Container.get(TenancyService); const tenantsManager = Container.get(TenantsManagerService); diff --git a/packages/server/src/database/seeds/data/accounts.js b/packages/server/src/database/seeds/data/accounts.js index 6e171ae0b..5d21686c3 100644 --- a/packages/server/src/database/seeds/data/accounts.js +++ b/packages/server/src/database/seeds/data/accounts.js @@ -34,9 +34,9 @@ export const PrepardExpenses = { export const StripeClearingAccount = { name: 'Stripe Clearing', slug: 'stripe-clearing', - account_type: 'other-current-liability', + account_type: 'other-current-asset', parent_account_id: null, - code: '50006', + code: '100020', active: true, index: 1, predefined: true, diff --git a/packages/server/src/services/StripePayment/CreateInvoiceCheckoutSession.ts b/packages/server/src/services/PaymentLinks/CreateInvoiceCheckoutSession.ts similarity index 92% rename from packages/server/src/services/StripePayment/CreateInvoiceCheckoutSession.ts rename to packages/server/src/services/PaymentLinks/CreateInvoiceCheckoutSession.ts index 34eedc402..e46919e47 100644 --- a/packages/server/src/services/StripePayment/CreateInvoiceCheckoutSession.ts +++ b/packages/server/src/services/PaymentLinks/CreateInvoiceCheckoutSession.ts @@ -1,20 +1,21 @@ -import config from '@/config'; -import { StripePaymentService } from './StripePaymentService'; +import { Inject, Service } from 'typedi'; +import { StripePaymentService } from '../StripePayment/StripePaymentService'; import HasTenancyService from '../Tenancy/TenancyService'; import { ISaleInvoice } from '@/interfaces'; -import { Inject, Service } from 'typedi'; import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { PaymentLink } from '@/system/models'; +import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware'; +import config from '@/config'; const origin = 'http://localhost'; @Service() export class CreateInvoiceCheckoutSession { @Inject() - private stripePaymentService: StripePaymentService; + private tenancy: HasTenancyService; @Inject() - private tenancy: HasTenancyService; + private stripePaymentService: StripePaymentService; /** * Creates a new Stripe checkout session from the given sale invoice. @@ -23,17 +24,18 @@ export class CreateInvoiceCheckoutSession { * @returns {Promise} */ async createInvoiceCheckoutSession( - tenantId: number, - publicPaymentLinkId: number + publicPaymentLinkId: string ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); - // Retrieves the payment link from the given id. const paymentLink = await PaymentLink.query() .findOne('linkId', publicPaymentLinkId) .where('resourceType', 'SaleInvoice') .throwIfNotFound(); + const tenantId = paymentLink.tenantId; + await initializeTenantSettings(tenantId); + + const { SaleInvoice } = this.tenancy.models(tenantId); // Retrieves the invoice from associated payment link. const invoice = await SaleInvoice.query() .findById(paymentLink.resourceId) diff --git a/packages/server/src/services/Sales/Invoices/GetInvoicePaymentLinkMetadata.ts b/packages/server/src/services/PaymentLinks/GetInvoicePaymentLinkMetadata.ts similarity index 94% rename from packages/server/src/services/Sales/Invoices/GetInvoicePaymentLinkMetadata.ts rename to packages/server/src/services/PaymentLinks/GetInvoicePaymentLinkMetadata.ts index 532082d08..b63b67901 100644 --- a/packages/server/src/services/Sales/Invoices/GetInvoicePaymentLinkMetadata.ts +++ b/packages/server/src/services/PaymentLinks/GetInvoicePaymentLinkMetadata.ts @@ -4,7 +4,7 @@ import { ServiceError } from '@/exceptions'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { PaymentLink } from '@/system/models'; -import { GetInvoicePaymentLinkMetaTransformer } from './GetInvoicePaymentLinkTransformer'; +import { GetInvoicePaymentLinkMetaTransformer } from '../Sales/Invoices/GetInvoicePaymentLinkTransformer'; import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection'; @Service() diff --git a/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts b/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts new file mode 100644 index 000000000..641521558 --- /dev/null +++ b/packages/server/src/services/PaymentLinks/PaymentLinksApplication.ts @@ -0,0 +1,37 @@ +import { Inject, Service } from 'typedi'; +import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata'; +import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; +import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; + +@Service() +export class PaymentLinksApplication { + @Inject() + private getInvoicePaymentLinkMetadataService: GetInvoicePaymentLinkMetadata; + + @Inject() + private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; + + /** + * Retrieves the invoice payment link. + * @param {string} paymentLinkId + * @returns {} + */ + public getInvoicePaymentLink(paymentLinkId: string) { + return this.getInvoicePaymentLinkMetadataService.getInvoicePaymentLinkMeta( + paymentLinkId + ); + } + + /** + * Create the invoice payment checkout session from the given payment link id. + * @param {string} paymentLinkId - Payment link id. + * @returns {Promise} + */ + public createInvoicePaymentCheckoutSession( + paymentLinkId: string + ): Promise { + return this.createInvoiceCheckoutSessionService.createInvoiceCheckoutSession( + paymentLinkId + ); + } +} diff --git a/packages/server/src/services/StripePayment/CreatePaymentReceivedStripePayment.ts b/packages/server/src/services/StripePayment/CreatePaymentReceivedStripePayment.ts index 28158ebb1..b0a3017c1 100644 --- a/packages/server/src/services/StripePayment/CreatePaymentReceivedStripePayment.ts +++ b/packages/server/src/services/StripePayment/CreatePaymentReceivedStripePayment.ts @@ -1,6 +1,9 @@ import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; import { GetSaleInvoice } from '../Sales/Invoices/GetSaleInvoice'; import { CreatePaymentReceived } from '../Sales/PaymentReceived/CreatePaymentReceived'; +import HasTenancyService from '../Tenancy/TenancyService'; +import UnitOfWork from '../UnitOfWork'; @Service() export class CreatePaymentReceiveStripePayment { @@ -10,28 +13,52 @@ export class CreatePaymentReceiveStripePayment { @Inject() private createPaymentReceivedService: CreatePaymentReceived; + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + /** * - * @param {number} tenantId - * @param {number} saleInvoiceId - * @param {number} paidAmount + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {number} paidAmount */ async createPaymentReceived( tenantId: number, saleInvoiceId: number, paidAmount: number ) { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( - tenantId, - saleInvoiceId - ); - await this.createPaymentReceivedService.createPaymentReceived(tenantId, { - customerId: invoice.customerId, - paymentDate: new Date(), - amount: paidAmount, - depositAccountId: 1002, - statement: '', - entries: [{ invoiceId: saleInvoiceId, paymentAmount: paidAmount }], + const { accountRepository } = this.tenancy.repositories(tenantId); + + // Create a payment received transaction under UOW envirement. + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + // Finds or creates a new stripe payment clearing account (current asset). + const stripeClearingAccount = + accountRepository.findOrCreateStripeClearing({}, trx); + + // Retrieves the given invoice to create payment transaction associated to it. + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + saleInvoiceId + ); + // Create a payment received transaction associated to the given invoice. + await this.createPaymentReceivedService.createPaymentReceived( + tenantId, + { + customerId: invoice.customerId, + paymentDate: new Date(), + amount: paidAmount, + exchangeRate: 1, + referenceNo: '', + statement: '', + depositAccountId: stripeClearingAccount.id, + entries: [{ invoiceId: saleInvoiceId, paymentAmount: paidAmount }], + }, + {}, + trx + ); }); } } diff --git a/packages/server/src/services/StripePayment/StripePaymentApplication.ts b/packages/server/src/services/StripePayment/StripePaymentApplication.ts index 876616ef7..23aa73546 100644 --- a/packages/server/src/services/StripePayment/StripePaymentApplication.ts +++ b/packages/server/src/services/StripePayment/StripePaymentApplication.ts @@ -1,6 +1,4 @@ import { Inject } from 'typedi'; -import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; -import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { CreateStripeAccountService } from './CreateStripeAccountService'; import { CreateStripeAccountLinkService } from './CreateStripeAccountLink'; import { CreateStripeAccountDTO } from './types'; @@ -14,9 +12,6 @@ export class StripePaymentApplication { @Inject() private createStripeAccountLinkService: CreateStripeAccountLinkService; - @Inject() - private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; - @Inject() private exchangeStripeOAuthTokenService: ExchangeStripeOAuthTokenService; @@ -51,22 +46,6 @@ export class StripePaymentApplication { ); } - /** - * Creates the Stripe checkout session from the given sale invoice. - * @param {number} tenantId - * @param {string} paymentLinkId - * @returns {Promise} - */ - public createSaleInvoiceCheckoutSession( - tenantId: number, - paymentLinkId: number - ): Promise { - return this.createInvoiceCheckoutSessionService.createInvoiceCheckoutSession( - tenantId, - paymentLinkId - ); - } - /** * Retrieves Stripe OAuth2 connect link. * @returns {string} diff --git a/packages/server/src/services/StripePayment/events/StripeWebhooksSubscriber.ts b/packages/server/src/services/StripePayment/events/StripeWebhooksSubscriber.ts index 87cfc157a..9f2e157d7 100644 --- a/packages/server/src/services/StripePayment/events/StripeWebhooksSubscriber.ts +++ b/packages/server/src/services/StripePayment/events/StripeWebhooksSubscriber.ts @@ -7,6 +7,7 @@ import { } from '@/interfaces/StripePayment'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Tenant } from '@/system/models'; +import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection'; @Service() export class StripeWebhooksSubscriber { @@ -41,6 +42,8 @@ export class StripeWebhooksSubscriber { const tenantId = parseInt(metadata.tenantId, 10); const saleInvoiceId = parseInt(metadata.saleInvoiceId, 10); + await initalizeTenantServices(tenantId); + // Get the amount from the event const amount = event.data.object.amount_total; diff --git a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx index 3f3e2c98d..e83a7e318 100644 --- a/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx +++ b/packages/webapp/src/containers/PaymentPortal/PaymentPortal.tsx @@ -3,7 +3,7 @@ import clsx from 'classnames'; import { AppToaster, Box, Group, Stack } from '@/components'; import { usePaymentPortalBoot } from './PaymentPortalBoot'; import { useDrawerActions } from '@/hooks/state'; -import { useCreateStripeCheckoutSession } from '@/hooks/query/stripe-integration'; +import { useCreateStripeCheckoutSession } from '@/hooks/query/payment-link'; import { DRAWERS } from '@/constants/drawers'; import styles from './PaymentPortal.module.scss'; diff --git a/packages/webapp/src/hooks/query/payment-link.ts b/packages/webapp/src/hooks/query/payment-link.ts index 8857cad81..630abf437 100644 --- a/packages/webapp/src/hooks/query/payment-link.ts +++ b/packages/webapp/src/hooks/query/payment-link.ts @@ -10,6 +10,8 @@ import { import useApiRequest from '../useRequest'; import { transformToCamelCase, transfromToSnakeCase } from '@/utils'; +const GetPaymentLinkInvoice = 'GetPaymentLinkInvoice'; + // Create Payment Link // ------------------------------------ interface CreatePaymentLinkValues { @@ -101,13 +103,49 @@ export function useGetInvoicePaymentLink( const apiRequest = useApiRequest(); return useQuery( - ['sharable-link-meta', linkId], + [GetPaymentLinkInvoice, linkId], () => apiRequest - .get(`/sharable-links/meta/invoice/${linkId}`) + .get(`/payment-links/${linkId}/invoice`) .then((res) => transformToCamelCase(res.data?.data)), { ...options, }, ); } + +// Create Stripe Checkout Session. +// ------------------------------------ +interface CreateCheckoutSessionValues { + linkId: string; +} +interface CreateCheckoutSessionResponse { + sessionId: string; + publishableKey: string; + redirectTo: string; +} +export const useCreateStripeCheckoutSession = ( + options?: UseMutationOptions< + CreateCheckoutSessionResponse, + Error, + CreateCheckoutSessionValues + >, +): UseMutationResult< + CreateCheckoutSessionResponse, + Error, + CreateCheckoutSessionValues +> => { + const apiRequest = useApiRequest(); + + return useMutation( + (values: CreateCheckoutSessionValues) => { + return apiRequest + .post(`/payment-links/${values.linkId}/stripe_checkout_session`, values) + .then( + (res) => + transformToCamelCase(res.data) as CreateCheckoutSessionResponse, + ); + }, + { ...options }, + ); +}; diff --git a/packages/webapp/src/hooks/query/stripe-integration.ts b/packages/webapp/src/hooks/query/stripe-integration.ts index 216f57192..b488c1afd 100644 --- a/packages/webapp/src/hooks/query/stripe-integration.ts +++ b/packages/webapp/src/hooks/query/stripe-integration.ts @@ -107,46 +107,6 @@ export const useCreateStripeAccount = ( ); }; -// Create Stripe Checkout Session. -// ------------------------------------ -interface CreateCheckoutSessionValues { - linkId: string; -} - -interface CreateCheckoutSessionResponse { - sessionId: string; - publishableKey: string; - redirectTo: string; -} - -export const useCreateStripeCheckoutSession = ( - options?: UseMutationOptions< - CreateCheckoutSessionResponse, - Error, - CreateCheckoutSessionValues - >, -): UseMutationResult< - CreateCheckoutSessionResponse, - Error, - CreateCheckoutSessionValues -> => { - const apiRequest = useApiRequest(); - - return useMutation( - (values: CreateCheckoutSessionValues) => { - return apiRequest - .post( - `/stripe_integration/${values.linkId}/create_checkout_session`, - values, - ) - .then( - (res) => - transformToCamelCase(res.data) as CreateCheckoutSessionResponse, - ); - }, - { ...options }, - ); -}; // Create Stripe Account OAuth Link. // ------------------------------------