refactor: payment services to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-28 06:00:58 +02:00
parent 6251831741
commit f20f07a42f
15 changed files with 574 additions and 10 deletions

View File

@@ -78,6 +78,7 @@ import { RolesModule } from '../Roles/Roles.module';
import { SubscriptionModule } from '../Subscription/Subscription.module';
import { OrganizationModule } from '../Organization/Organization.module';
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module';
@Module({
imports: [
@@ -191,6 +192,7 @@ import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module
SubscriptionModule,
OrganizationModule,
TenantDBManagerModule,
PaymentServicesModule,
],
controllers: [AppController],
providers: [

View File

@@ -1,4 +1,5 @@
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsArray,
@@ -28,47 +29,77 @@ class AttachmentDto {
export class CommandCreditNoteDto {
@IsInt()
@ApiProperty({ example: 1, description: 'The customer ID' })
customerId: number;
@IsOptional()
@IsPositive()
@ApiProperty({ example: 3.43, description: 'The exchange rate' })
exchangeRate?: number;
@IsDate()
@Type(() => Date)
@ApiProperty({ example: '2021-09-01', description: 'The credit note date' })
creditNoteDate: Date;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The reference number' })
referenceNo?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The credit note number' })
creditNoteNumber?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The note' })
note?: string;
@IsOptional()
@IsString()
@ApiProperty({ example: '123', description: 'The terms and conditions' })
termsConditions?: string;
@IsBoolean()
@ApiProperty({
example: false,
description: 'The credit note is open',
})
open: boolean = false;
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The warehouse ID',
})
warehouseId?: number;
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The branch ID',
})
branchId?: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreditNoteEntryDto)
@Min(1)
@ApiProperty({
example: [
{
itemId: 1,
quantity: 1,
rate: 10,
taxRateId: 1,
},
],
description: 'The credit note entries',
})
entries: CreditNoteEntryDto[];
@IsOptional()
@@ -79,14 +110,27 @@ export class CommandCreditNoteDto {
@IsOptional()
@IsInt()
@ApiProperty({
example: 1,
description: 'The pdf template ID',
})
pdfTemplateId?: number;
@IsOptional()
@IsNumber()
@ApiProperty({
example: 10,
description: 'The discount amount',
})
discount?: number;
@IsOptional()
@IsEnum(DiscountType)
@ApiProperty({
example: 'percentage',
description: 'The discount type',
enum: DiscountType,
})
discountType?: DiscountType;
@IsOptional()

View File

@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import { IsNotEmpty } from 'class-validator';
@@ -5,30 +6,43 @@ import { IsNotEmpty } from 'class-validator';
class CommandItemCategoryDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Category name', description: 'The category name' })
name: string;
@IsString()
@IsOptional()
@ApiProperty({
example: 'Category description',
description: 'The category description',
})
description?: string;
@IsNumber()
@IsNotEmpty()
@ApiProperty({
example: 1,
description: 'The user ID',
})
userId: number;
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The cost account ID' })
costAccountId?: number;
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The sell account ID' })
sellAccountId?: number;
@IsNumber()
@IsOptional()
@ApiProperty({ example: 1, description: 'The inventory account ID' })
inventoryAccountId?: number;
@IsString()
@IsOptional()
@ApiProperty({ example: 'FIFO', description: 'The cost method' })
costMethod?: string;
}

View File

@@ -0,0 +1,86 @@
import {
Controller,
Get,
Post,
Delete,
Param,
Body,
Req,
Res,
Next,
UsePipes,
ValidationPipe,
HttpStatus,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { PaymentServicesApplication } from './PaymentServicesApplication';
@ApiTags('PaymentServices')
@Controller('payment-services')
export class PaymentServicesController {
constructor(
private readonly paymentServicesApp: PaymentServicesApplication,
) {}
@Get('/')
async getPaymentServicesSpecificInvoice(@Res() res: Response) {
const paymentServices =
await this.paymentServicesApp.getPaymentServicesForInvoice();
return res.status(HttpStatus.OK).send({ paymentServices });
}
@Get('/state')
async getPaymentMethodsState(@Res() res: Response) {
const paymentMethodsState =
await this.paymentServicesApp.getPaymentMethodsState();
return res.status(HttpStatus.OK).send({ data: paymentMethodsState });
}
@Get('/:paymentServiceId')
async getPaymentService(
@Param('paymentServiceId') paymentServiceId: number,
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
) {
const paymentService =
await this.paymentServicesApp.getPaymentService(paymentServiceId);
return res.status(HttpStatus.OK).send({ data: paymentService });
}
@Post('/:paymentMethodId')
@UsePipes(new ValidationPipe({ whitelist: true }))
async updatePaymentMethod(
@Param('paymentMethodId') paymentMethodId: number,
@Body() updatePaymentMethodDTO: any,
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
) {
await this.paymentServicesApp.editPaymentMethod(
paymentMethodId,
updatePaymentMethodDTO,
);
return res.status(HttpStatus.OK).send({
id: paymentMethodId,
message: 'The given payment method has been updated.',
});
}
@Delete('/:paymentMethodId')
async deletePaymentMethod(
@Param('paymentMethodId') paymentMethodId: number,
@Res() res: Response,
) {
await this.paymentServicesApp.deletePaymentMethod(paymentMethodId);
return res.status(HttpStatus.NO_CONTENT).send({
id: paymentMethodId,
message: 'The payment method has been deleted.',
});
}
}

View File

@@ -1,7 +1,21 @@
import { Module } from '@nestjs/common';
import { DeletePaymentMethodService } from './commands/DeletePaymentMethodService';
import { EditPaymentMethodService } from './commands/EditPaymentMethodService';
import { GetPaymentMethodService } from './queries/GetPaymentService';
import { GetPaymentServicesSpecificInvoice } from './queries/GetPaymentServicesSpecificInvoice';
import { GetPaymentMethodsStateService } from './queries/GetPaymentMethodsState';
import { PaymentServicesApplication } from './PaymentServicesApplication';
import { PaymentServicesController } from './PaymentServices.controller';
export class PaymentServicesModule {}
@Module({
providers: [
DeletePaymentMethodService,
EditPaymentMethodService,
GetPaymentMethodService,
GetPaymentMethodsStateService,
GetPaymentServicesSpecificInvoice,
PaymentServicesApplication,
],
controllers: [PaymentServicesController],
})
export class PaymentServicesModule {}

View File

@@ -0,0 +1,75 @@
import { Service, Inject } from 'typedi';
import { GetPaymentServicesSpecificInvoice } from './queries/GetPaymentServicesSpecificInvoice';
import { DeletePaymentMethodService } from './commands/DeletePaymentMethodService';
import { EditPaymentMethodService } from './commands/EditPaymentMethodService';
import { EditPaymentMethodDTO, GetPaymentMethodsPOJO } from './types';
import { GetPaymentMethodsStateService } from './queries/GetPaymentMethodsState';
import { GetPaymentMethodService } from './queries/GetPaymentService';
@Service()
export class PaymentServicesApplication {
constructor(
private readonly getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice,
private readonly deletePaymentMethodService: DeletePaymentMethodService,
private readonly editPaymentMethodService: EditPaymentMethodService,
private readonly getPaymentMethodsStateService: GetPaymentMethodsStateService,
private readonly getPaymentMethodService: GetPaymentMethodService,
) {}
/**
* Retrieves the payment services for a specific invoice.
* @param {number} invoiceId - The ID of the invoice.
* @returns {Promise<any>} The payment services for the specified invoice.
*/
public async getPaymentServicesForInvoice(): Promise<any> {
return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice(
tenantId,
);
}
/**
* Retrieves specific payment service details.
* @param {number} paymentServiceId - Payment service id.
*/
public async getPaymentService(paymentServiceId: number) {
return this.getPaymentMethodService.getPaymentMethod(paymentServiceId);
}
/**
* Deletes the given payment method.
* @param {number} paymentIntegrationId
* @returns {Promise<void>}
*/
public async deletePaymentMethod(
paymentIntegrationId: number,
): Promise<void> {
return this.deletePaymentMethodService.deletePaymentMethod(
paymentIntegrationId,
);
}
/**
* Edits the given payment method.
* @param {number} paymentIntegrationId
* @param {EditPaymentMethodDTO} editPaymentMethodDTO
* @returns {Promise<void>}
*/
public async editPaymentMethod(
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO,
): Promise<void> {
return this.editPaymentMethodService.editPaymentMethod(
paymentIntegrationId,
editPaymentMethodDTO,
);
}
/**
* Retrieves the payment state providing state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(): Promise<GetPaymentMethodsPOJO> {
return this.getPaymentMethodsStateService.getPaymentMethodsState();
}
}

View File

@@ -0,0 +1,59 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PaymentIntegration } from '../models/PaymentIntegration.model';
import { TransactionPaymentServiceEntry } from '../models/TransactionPaymentServiceEntry.model';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
@Injectable()
export class DeletePaymentMethodService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventEmitter: EventEmitter2,
@Inject(PaymentIntegration.name)
private readonly paymentIntegrationModel: TenantModelProxy<
typeof PaymentIntegration
>,
@Inject(TransactionPaymentServiceEntry.name)
private readonly transactionPaymentServiceEntryModel: TenantModelProxy<
typeof TransactionPaymentServiceEntry
>,
) {}
/**
* Deletes the given payment integration.
* @param {number} paymentIntegrationId
* @returns {Promise<void>}
*/
public async deletePaymentMethod(
paymentIntegrationId: number,
): Promise<void> {
const paymentIntegration = await this.paymentIntegrationModel()
.query()
.findById(paymentIntegrationId)
.throwIfNotFound();
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Delete payment methods links.
await this.transactionPaymentServiceEntryModel()
.query(trx)
.where('paymentIntegrationId', paymentIntegrationId)
.delete();
// Delete the payment integration.
await this.paymentIntegrationModel()
.query(trx)
.findById(paymentIntegrationId)
.delete();
// Triggers `onPaymentMethodDeleted` event.
await this.eventEmitter.emitAsync(events.paymentMethod.onDeleted, {
paymentIntegrationId,
});
});
}
}

View File

@@ -0,0 +1,64 @@
import { Knex } from 'knex';
import { EditPaymentMethodDTO } from '../types';
import { TransactionPaymentServiceEntry } from '../models/TransactionPaymentServiceEntry.model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PaymentIntegration } from '../models/PaymentIntegration.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
@Injectable()
export class EditPaymentMethodService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventEmitter: EventEmitter2,
@Inject(PaymentIntegration.name)
private readonly paymentIntegrationModel: TenantModelProxy<
typeof PaymentIntegration
>,
@Inject(TransactionPaymentServiceEntry.name)
private readonly transactionPaymentServiceEntryModel: TenantModelProxy<
typeof TransactionPaymentServiceEntry
>,
) {}
/**
* Edits the given payment method.
* @param {number} paymentIntegrationId - The ID of the payment method.
* @param {EditPaymentMethodDTO} editPaymentMethodDTO
* @returns {Promise<void>}
*/
async editPaymentMethod(
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO,
): Promise<void> {
const paymentMethod = await this.paymentIntegrationModel()
.query()
.findById(paymentIntegrationId)
.throwIfNotFound();
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onPaymentMethodEditing` event.
await this.eventEmitter.emitAsync(events.paymentMethod.onEditing, {
paymentIntegrationId,
editPaymentMethodDTO,
trx,
});
await this.paymentIntegrationModel()
.query(trx)
.findById(paymentIntegrationId)
.patch({
...editPaymentMethodDTO,
});
// Triggers `onPaymentMethodEdited` event.
await this.eventEmitter.emitAsync(events.paymentMethod.onEdited, {
paymentIntegrationId,
editPaymentMethodDTO,
trx,
});
});
}
}

View File

@@ -0,0 +1,67 @@
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GetPaymentMethodsPOJO } from '../types';
import { isStripePaymentConfigured } from '../utils';
import { GetStripeAuthorizationLinkService } from '../../StripePayment/GetStripeAuthorizationLink';
import { PaymentIntegration } from '../models/PaymentIntegration.model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetPaymentMethodsStateService {
constructor(
private readonly getStripeAuthorizationLinkService: GetStripeAuthorizationLinkService,
private readonly configService: ConfigService,
@Inject(PaymentIntegration.name)
private readonly paymentIntegrationModel: TenantModelProxy<
typeof PaymentIntegration
>,
) {}
/**
* Retrieves the payment state provising state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(
): Promise<GetPaymentMethodsPOJO> {
const stripePayment = await this.paymentIntegrationModel()
.query()
.orderBy('createdAt', 'ASC')
.findOne({
service: 'Stripe',
});
const isStripeAccountCreated = !!stripePayment;
const isStripePaymentEnabled = stripePayment?.paymentEnabled;
const isStripePayoutEnabled = stripePayment?.payoutEnabled;
const isStripeEnabled = stripePayment?.fullEnabled;
const stripePaymentMethodId = stripePayment?.id || null;
const stripeAccountId = stripePayment?.accountId || null;
const stripePublishableKey = this.configService.get(
'stripePayment.publishableKey',
);
const stripeCurrencies = ['USD', 'EUR'];
const stripeRedirectUrl = 'https://your-stripe-redirect-url.com';
const isStripeServerConfigured = isStripePaymentConfigured();
const stripeAuthLink =
this.getStripeAuthorizationLinkService.getStripeAuthLink();
const paymentMethodPOJO: GetPaymentMethodsPOJO = {
stripe: {
isStripeAccountCreated,
isStripePaymentEnabled,
isStripePayoutEnabled,
isStripeEnabled,
isStripeServerConfigured,
stripeAccountId,
stripePaymentMethodId,
stripePublishableKey,
stripeCurrencies,
stripeAuthLink,
stripeRedirectUrl,
},
};
return paymentMethodPOJO;
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetPaymentMethodsPOJO } from '../types';
import { PaymentIntegration } from '../models/PaymentIntegration.model';
@Injectable()
export class GetPaymentMethodService {
constructor(
@Inject(PaymentIntegration.name)
private readonly paymentIntegrationModel: TenantModelProxy<
typeof PaymentIntegration
>,
) {}
/**
* Retrieves the payment state provising state.
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethod(
paymentServiceId: number,
): Promise<GetPaymentMethodsPOJO> {
const stripePayment = await this.paymentIntegrationModel()
.query()
.findById(paymentServiceId)
.throwIfNotFound();
return stripePayment;
}
}

View File

@@ -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()
.modify('fullEnabled')
.orderBy('name', 'ASC');
return this.transform.transform(
tenantId,
paymentGateways,
new GetPaymentServicesSpecificInvoiceTransformer()
);
}
}

View File

@@ -0,0 +1,19 @@
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer {
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['accountId'];
};
public includeAttributes = (): string[] => {
return ['serviceFormatted'];
};
public serviceFormatted(method) {
return 'Stripe';
}
}

View File

@@ -0,0 +1,33 @@
export interface EditPaymentMethodDTO {
name?: string;
options?: {
bankAccountId?: number; // bank account.
clearningAccountId?: number; // current liability.
showVisa?: boolean;
showMasterCard?: boolean;
showDiscover?: boolean;
showAmer?: boolean;
showJcb?: boolean;
showDiners?: boolean;
};
}
export interface GetPaymentMethodsPOJO {
stripe: {
isStripeAccountCreated: boolean;
isStripePaymentEnabled: boolean;
isStripePayoutEnabled: boolean;
isStripeEnabled: boolean;
isStripeServerConfigured: boolean;
stripeAccountId: string | null;
stripePaymentMethodId: number | null;
stripePublishableKey: string | null;
stripeAuthLink: string;
stripeCurrencies: Array<string>;
stripeRedirectUrl: string | null;
};
}

View File

@@ -0,0 +1,9 @@
import config from '@/config';
export const isStripePaymentConfigured = () => {
return (
config.stripePayment.secretKey &&
config.stripePayment.publishableKey &&
config.stripePayment.webhooksSecret
);
};

View File

@@ -1,11 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsNotEmpty,
IsNumber,
IsString,
Length,
MinLength,
ValidateNested,
} from 'class-validator';
@@ -34,7 +36,10 @@ export class CommandRolePermissionDto {
description: 'The value of the permission',
})
value: boolean;
}
export class CreateRolePermissionDto extends CommandRolePermissionDto {}
export class EditRolePermissionDto extends CommandRolePermissionDto {
@IsNumber()
@IsNotEmpty()
@ApiProperty({
@@ -60,17 +65,28 @@ class CommandRoleDto {
description: 'The description of the role',
})
roleDescription: string;
}
export class CreateRoleDto extends CommandRoleDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CommandRolePermissionDto)
@MinLength(1)
@ApiProperty({
type: [CommandRolePermissionDto],
description: 'The permissions of the role',
})
permissions: Array<CommandRolePermissionDto>;
permissions: Array<CreateRolePermissionDto>;
}
export class CreateRoleDto extends CommandRoleDto {}
export class EditRoleDto extends CommandRoleDto {}
export class EditRoleDto extends CommandRoleDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CommandRolePermissionDto)
@ApiProperty({
type: [CommandRolePermissionDto],
description: 'The permissions of the role',
})
permissions: Array<EditRolePermissionDto>;
}