feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,104 @@
import { StripePaymentService } from '../StripePayment/StripePaymentService';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice';
import { PaymentLink } from './models/PaymentLink';
import { StripeInvoiceCheckoutSessionPOJO } from '../StripePayment/StripePayment.types';
import { ModelObject } from 'objection';
import { ConfigService } from '@nestjs/config';
const origin = 'http://localhost';
@Injectable()
export class CreateInvoiceCheckoutSession {
constructor(
private readonly stripePaymentService: StripePaymentService,
private readonly configService: ConfigService,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PaymentLink.name)
private readonly paymentLinkModel: typeof PaymentLink,
) {}
/**
* Creates a new Stripe checkout session from the given sale invoice.
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<StripeInvoiceCheckoutSessionPOJO>}
*/
async createInvoiceCheckoutSession(
publicPaymentLinkId: string,
): Promise<StripeInvoiceCheckoutSessionPOJO> {
// Retrieves the payment link from the given id.
const paymentLink = await this.paymentLinkModel
.query()
.findOne('linkId', publicPaymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
// Retrieves the invoice from associated payment link.
const invoice = await this.saleInvoiceModel()
.query()
.findById(paymentLink.resourceId)
.withGraphFetched('paymentMethods')
.throwIfNotFound();
// It will be only one Stripe payment method associated to the invoice.
const stripePaymentMethod = invoice.paymentMethods?.find(
(method) => method.paymentIntegration?.service === 'Stripe',
);
const stripeAccountId = stripePaymentMethod?.paymentIntegration?.accountId;
const paymentIntegrationId = stripePaymentMethod?.paymentIntegration?.id;
// Creates checkout session for the given invoice.
const session = await this.createCheckoutSession(invoice, stripeAccountId, {
tenantId: paymentLink.tenantId,
paymentLinkId: paymentLink.id,
});
return {
sessionId: session.id,
publishableKey: this.configService.get('stripePayment.publishableKey'),
redirectTo: session.url,
};
}
/**
* Creates a new Stripe checkout session for the given sale invoice.
* @param {ISaleInvoice} invoice - The sale invoice for which the checkout session is created.
* @param {string} stripeAccountId - The Stripe account ID associated with the payment method.
* @returns {Promise<any>} - The created Stripe checkout session.
*/
private createCheckoutSession(
invoice: ModelObject<SaleInvoice>,
stripeAccountId?: string,
metadata?: Record<string, any>,
) {
return this.stripePaymentService.stripe.checkout.sessions.create(
{
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: invoice.currencyCode,
product_data: {
name: invoice.invoiceNo,
},
unit_amount: invoice.total * 100, // Amount in cents
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
metadata: {
saleInvoiceId: invoice.id,
resource: 'SaleInvoice',
...metadata,
},
},
{ stripeAccount: stripeAccountId },
);
}
}

View File

@@ -0,0 +1,72 @@
import moment from 'moment';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { SaleInvoice } from '../SaleInvoices/models/SaleInvoice';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { PaymentLink } from './models/PaymentLink';
import { ServiceError } from '../Items/ServiceError';
import { GetInvoicePaymentLinkMetaTransformer } from '../SaleInvoices/queries/GetInvoicePaymentLink.transformer';
import { ClsService } from 'nestjs-cls';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenantModel } from '../System/models/TenantModel';
@Injectable()
export class GetInvoicePaymentLinkMetadata {
constructor(
private readonly transformer: TransformerInjectable,
private readonly clsService: ClsService,
private readonly tenancyContext: TenancyContext,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(PaymentLink.name)
private readonly paymentLinkModel: typeof PaymentLink,
@Inject(TenantModel.name)
private readonly systemTenantModel: typeof TenantModel,
) {}
/**
* Retrieves the invoice sharable link meta of the link id.
* @param {string} linkId - Link id.
*/
async getInvoicePaymentLinkMeta(linkId: string) {
const paymentLink = await this.paymentLinkModel
.query()
.findOne('linkId', linkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
// Validate the expiry at date.
if (paymentLink.expiryAt) {
const currentDate = moment();
const expiryDate = moment(paymentLink.expiryAt);
if (expiryDate.isBefore(currentDate)) {
throw new ServiceError('PAYMENT_LINK_EXPIRED');
}
}
const tenant = await this.systemTenantModel
.query()
.findById(paymentLink.tenantId);
this.clsService.set('organizationId', tenant.organizationId);
// this.clsService.set('userId', paymentLink.userId);
const invoice = await this.saleInvoiceModel()
.query()
.findById(paymentLink.resourceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('taxes.taxRate')
.withGraphFetched('paymentMethods.paymentIntegration')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
return this.transformer.transform(
invoice,
new GetInvoicePaymentLinkMetaTransformer(),
);
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Injectable } from '@nestjs/common';
import { SaleInvoicePdf } from '../SaleInvoices/queries/SaleInvoicePdf.service';
import { PaymentLink } from './models/PaymentLink';
import { ClsService } from 'nestjs-cls';
import { TenantModel } from '../System/models/TenantModel';
@Injectable()
export class GetPaymentLinkInvoicePdf {
constructor(
private readonly getSaleInvoicePdfService: SaleInvoicePdf,
private readonly clsService: ClsService,
@Inject(PaymentLink.name)
private readonly paymentLinkModel: typeof PaymentLink,
@Inject(TenantModel.name)
private readonly systemTenantModel: typeof TenantModel,
) {}
/**
* Retrieves the sale invoice PDF of the given payment link id.
* @param {number} paymentLinkId
* @returns {Promise<Buffer, string>}
*/
async getPaymentLinkInvoicePdf(
paymentLinkId: string,
): Promise<[Buffer, string]> {
const paymentLink = await this.paymentLinkModel
.query()
.findOne('linkId', paymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
const saleInvoiceId = paymentLink.resourceId;
const tenant = await this.systemTenantModel
.query()
.findById(paymentLink.tenantId);
this.clsService.set('organizationId', tenant.organizationId);
return this.getSaleInvoicePdfService.getSaleInvoicePdf(saleInvoiceId);
}
}

View File

@@ -0,0 +1,121 @@
import { Response } from 'express';
import { Controller, Get, Param, Res } from '@nestjs/common';
import { PaymentLinksApplication } from './PaymentLinksApplication';
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('payment-links')
@ApiTags('payment-links')
export class PaymentLinksController {
constructor(private readonly paymentLinkApp: PaymentLinksApplication) {}
@Get('/:paymentLinkId/invoice')
@ApiOperation({
summary: 'Get payment link public metadata',
description: 'Retrieves public metadata for an invoice payment link',
})
@ApiParam({
name: 'paymentLinkId',
description: 'The ID of the payment link',
type: 'string',
required: true,
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved payment link metadata',
schema: {
type: 'object',
properties: {
data: {
type: 'object',
description: 'Payment link metadata',
},
},
},
})
@ApiResponse({ status: 404, description: 'Payment link not found' })
public async getPaymentLinkPublicMeta(
@Param('paymentLinkId') paymentLinkId: string,
) {
const data = await this.paymentLinkApp.getInvoicePaymentLink(paymentLinkId);
return { data };
}
@Get('/:paymentLinkId/stripe_checkout_session')
@ApiOperation({
summary: 'Create Stripe checkout session',
description: 'Creates a Stripe checkout session for an invoice payment link',
})
@ApiParam({
name: 'paymentLinkId',
description: 'The ID of the payment link',
type: 'string',
required: true,
})
@ApiResponse({
status: 200,
description: 'Successfully created Stripe checkout session',
schema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Stripe checkout session ID',
},
url: {
type: 'string',
description: 'Stripe checkout session URL',
},
},
},
})
@ApiResponse({ status: 404, description: 'Payment link not found' })
public async createInvoicePaymentLinkCheckoutSession(
@Param('paymentLinkId') paymentLinkId: string,
) {
const session =
await this.paymentLinkApp.createInvoicePaymentCheckoutSession(
paymentLinkId,
);
return session;
}
@Get('/:paymentLinkId/invoice/pdf')
@ApiOperation({
summary: 'Get payment link invoice PDF',
description: 'Retrieves the PDF of the invoice associated with a payment link',
})
@ApiParam({
name: 'paymentLinkId',
description: 'The ID of the payment link',
type: 'string',
required: true,
})
@ApiResponse({
status: 200,
description: 'Successfully retrieved invoice PDF',
content: {
'application/pdf': {
schema: {
type: 'string',
format: 'binary',
},
},
},
})
@ApiResponse({ status: 404, description: 'Payment link or invoice not found' })
public async getPaymentLinkInvoicePdf(
@Param('paymentLinkId') paymentLinkId: string,
@Res() res: Response,
) {
const [pdfContent, filename] =
await this.paymentLinkApp.getPaymentLinkInvoicePdf(paymentLinkId);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf';
import { PaymentLinksApplication } from './PaymentLinksApplication';
import { PaymentLinksController } from './PaymentLinks.controller';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { PaymentLink } from './models/PaymentLink';
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
const models = [InjectSystemModel(PaymentLink)];
@Module({
imports: [StripePaymentModule, SaleInvoicesModule],
providers: [
...models,
TenancyContext,
CreateInvoiceCheckoutSession,
GetPaymentLinkInvoicePdf,
PaymentLinksApplication,
GetInvoicePaymentLinkMetadata,
],
controllers: [PaymentLinksController],
exports: [...models, PaymentLinksApplication],
})
export class PaymentLinksModule {}

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf';
import { StripeInvoiceCheckoutSessionPOJO } from '../StripePayment/StripePayment.types';
@Injectable()
export class PaymentLinksApplication {
constructor(
private readonly getInvoicePaymentLinkMetadataService: GetInvoicePaymentLinkMetadata,
private readonly createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession,
private readonly getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf,
) {}
/**
* 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<StripeInvoiceCheckoutSessionPOJO>}
*/
public createInvoicePaymentCheckoutSession(
paymentLinkId: string,
): Promise<StripeInvoiceCheckoutSessionPOJO> {
return this.createInvoiceCheckoutSessionService.createInvoiceCheckoutSession(
paymentLinkId,
);
}
/**
* Retrieves the sale invoice pdf of the given payment link id.
* @param {number} paymentLinkId
* @returns {Promise<Buffer> }
*/
public getPaymentLinkInvoicePdf(
paymentLinkId: string,
): Promise<[Buffer, string]> {
return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf(
paymentLinkId,
);
}
}

View File

@@ -0,0 +1,32 @@
import { SystemModel } from '@/modules/System/models/SystemModel';
import { Model } from 'objection';
export class PaymentLink extends SystemModel {
public id!: number;
public tenantId!: number;
public resourceId!: number;
public resourceType!: string;
public linkId!: string;
public publicity!: string;
public expiryAt!: Date;
// Timestamps
public createdAt!: Date;
public updatedAt!: Date;
/**
* Table name.
* @returns {string}
*/
static get tableName() {
return 'payment_links';
}
/**
* Timestamps columns.
* @returns {string[]}
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
}