This commit is contained in:
Ahmed Bouhuolia
2026-01-16 00:23:16 +02:00
parent 2bbc154f18
commit c21301061f
15 changed files with 87 additions and 45 deletions

View File

@@ -35,10 +35,11 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
const password = configService.get<string>('redis.password'); const password = configService.get<string>('redis.password');
const db = configService.get<number>('redis.db'); const db = configService.get<number>('redis.db');
const globalTtl = configService.get<number>('throttle.global.ttl'); // Ensure we always have valid numbers with fallback defaults
const globalLimit = configService.get<number>('throttle.global.limit'); const globalTtl = configService.get<number>('throttle.global.ttl') ?? 60000;
const authTtl = configService.get<number>('throttle.auth.ttl'); const globalLimit = configService.get<number>('throttle.global.limit') ?? 100;
const authLimit = configService.get<number>('throttle.auth.limit'); const authTtl = configService.get<number>('throttle.auth.ttl') ?? 60000;
const authLimit = configService.get<number>('throttle.auth.limit') ?? 10;
return { return {
throttlers: [ throttlers: [

View File

@@ -4,8 +4,11 @@ import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepo
import { TransactionsByReferenceService } from './TransactionsByReference.service'; import { TransactionsByReferenceService } from './TransactionsByReference.service';
import { TransactionsByReferenceController } from './TransactionsByReference.controller'; import { TransactionsByReferenceController } from './TransactionsByReference.controller';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
@Module({ @Module({
imports: [FinancialSheetCommonModule, AccountsModule],
providers: [ providers: [
TransactionsByReferenceRepository, TransactionsByReferenceRepository,
TransactionsByReferenceApplication, TransactionsByReferenceApplication,
@@ -14,4 +17,4 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
], ],
controllers: [TransactionsByReferenceController], controllers: [TransactionsByReferenceController],
}) })
export class TransactionsByReferenceModule {} export class TransactionsByReferenceModule { }

View File

@@ -1,6 +1,6 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication'; import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types'; import { TransactionsByReferenceQueryDto } from './TransactionsByReferenceQuery.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('reports/transactions-by-reference') @Controller('reports/transactions-by-reference')
@@ -8,13 +8,13 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
export class TransactionsByReferenceController { export class TransactionsByReferenceController {
constructor( constructor(
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication, private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,
) {} ) { }
@Get() @Get()
@ApiResponse({ status: 200, description: 'Transactions by reference' }) @ApiResponse({ status: 200, description: 'Transactions by reference' })
@ApiOperation({ summary: 'Get transactions by reference' }) @ApiOperation({ summary: 'Get transactions by reference' })
async getTransactionsByReference( async getTransactionsByReference(
@Query() query: ITransactionsByReferenceQuery, @Query() query: TransactionsByReferenceQueryDto,
) { ) {
const data = await this.transactionsByReferenceApp.getTransactions(query); const data = await this.transactionsByReferenceApp.getTransactions(query);

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { import {
ITransactionsByReferencePojo, ITransactionsByReferencePojo,
ITransactionsByReferenceQuery, ITransactionsByReferenceQuery,
@@ -13,7 +13,7 @@ export class TransactionsByReferenceService {
constructor( constructor(
private readonly repository: TransactionsByReferenceRepository, private readonly repository: TransactionsByReferenceRepository,
private readonly tenancyContext: TenancyContext private readonly tenancyContext: TenancyContext
) {} ) { }
/** /**
* Retrieve accounts transactions by given reference id and type. * Retrieve accounts transactions by given reference id and type.
@@ -23,6 +23,12 @@ export class TransactionsByReferenceService {
public async getTransactionsByReference( public async getTransactionsByReference(
query: ITransactionsByReferenceQuery query: ITransactionsByReferenceQuery
): Promise<ITransactionsByReferencePojo> { ): Promise<ITransactionsByReferencePojo> {
// Validate referenceId is a valid positive number
const referenceId = Number(query.referenceId);
if (isNaN(referenceId) || referenceId <= 0) {
throw new BadRequestException('referenceId must be a valid positive number');
}
const filter = { const filter = {
...getTransactionsByReferenceQuery(), ...getTransactionsByReferenceQuery(),
...query, ...query,
@@ -31,7 +37,7 @@ export class TransactionsByReferenceService {
// Retrieve the accounts transactions of the given reference. // Retrieve the accounts transactions of the given reference.
const transactions = await this.repository.getTransactions( const transactions = await this.repository.getTransactions(
Number(filter.referenceId), referenceId,
filter.referenceType filter.referenceType
); );
// Transactions by reference report. // Transactions by reference report.

View File

@@ -0,0 +1,22 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class TransactionsByReferenceQueryDto {
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The type of the reference (e.g., SaleInvoice, Bill, etc.)',
example: 'SaleInvoice',
required: true,
})
referenceType: string;
@IsString()
@IsNotEmpty()
@ApiProperty({
description: 'The ID of the reference',
example: '1',
required: true,
})
referenceId: string;
}

View File

@@ -47,18 +47,18 @@ export class TransactionsByReference extends FinancialSheet {
debit: this.getAmountMeta(transaction.debit, { money: false }), debit: this.getAmountMeta(transaction.debit, { money: false }),
// @ts-ignore // @ts-ignore
// referenceTypeFormatted: transaction.referenceTypeFormatted, // formattedReferenceType: transaction.referenceTypeFormatted,
referenceTypeFormatted: '', formattedReferenceType: '',
referenceType: transaction.referenceType, referenceType: transaction.referenceType,
referenceId: transaction.referenceId, referenceId: transaction.referenceId,
contactId: transaction.contactId, contactId: transaction.contactId,
contactType: transaction.contactType, contactType: transaction.contactType,
contactTypeFormatted: transaction.contactType, formattedContactType: transaction.contactType || '',
accountName: transaction.account.name, accountName: transaction.account?.name || '',
accountCode: transaction.account.code, accountCode: transaction.account?.code || '',
accountId: transaction.accountId, accountId: transaction.accountId,
}; };
}; };

View File

@@ -32,7 +32,7 @@ export class PaymentReceivedValidators {
@Inject(Account.name) @Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>, private readonly accountModel: TenantModelProxy<typeof Account>,
) {} ) { }
/** /**
* Validates the payment existance. * Validates the payment existance.

View File

@@ -1,6 +1,7 @@
import { import {
Controller, Controller,
Post, Post,
Put,
Get, Get,
Delete, Delete,
Param, Param,
@@ -45,7 +46,7 @@ export class RolesController {
}; };
} }
@Post(':id') @Put(':id')
@ApiOperation({ summary: 'Edit an existing role' }) @ApiOperation({ summary: 'Edit an existing role' })
@ApiParam({ name: 'id', description: 'Role ID' }) @ApiParam({ name: 'id', description: 'Role ID' })
@ApiBody({ type: EditRoleDto }) @ApiBody({ type: EditRoleDto })

View File

@@ -17,7 +17,7 @@ export class EditRoleService {
@Inject(Role.name) @Inject(Role.name)
private readonly roleModel: TenantModelProxy<typeof Role>, private readonly roleModel: TenantModelProxy<typeof Role>,
) {} ) { }
/** /**
* Edits details of the given role on the storage. * Edits details of the given role on the storage.
@@ -30,7 +30,13 @@ export class EditRoleService {
// Retrieve the given role or throw not found serice error. // Retrieve the given role or throw not found serice error.
const oldRole = await this.roleModel().query().findById(roleId); const oldRole = await this.roleModel().query().findById(roleId);
const permissions = editRoleDTO.permissions; // Transform permissions: map permissionId to id for Objection.js upsertGraph
const permissions = editRoleDTO.permissions.map((perm) => ({
id: perm.permissionId,
subject: perm.subject,
ability: perm.ability,
value: perm.value,
}));
// Updates the role on the storage. // Updates the role on the storage.
return this.uow.withTransaction(async (trx: Knex.Transaction) => { return this.uow.withTransaction(async (trx: Knex.Transaction) => {

View File

@@ -38,7 +38,7 @@ export class CommandRolePermissionDto {
value: boolean; value: boolean;
} }
export class CreateRolePermissionDto extends CommandRolePermissionDto {} export class CreateRolePermissionDto extends CommandRolePermissionDto { }
export class EditRolePermissionDto extends CommandRolePermissionDto { export class EditRolePermissionDto extends CommandRolePermissionDto {
@IsNumber() @IsNumber()
@IsNotEmpty() @IsNotEmpty()
@@ -83,9 +83,9 @@ export class EditRoleDto extends CommandRoleDto {
@IsArray() @IsArray()
@ArrayMinSize(1) @ArrayMinSize(1)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CommandRolePermissionDto) @Type(() => EditRolePermissionDto)
@ApiProperty({ @ApiProperty({
type: [CommandRolePermissionDto], type: [EditRolePermissionDto],
description: 'The permissions of the role', description: 'The permissions of the role',
}) })
permissions: Array<EditRolePermissionDto>; permissions: Array<EditRolePermissionDto>;

View File

@@ -168,7 +168,7 @@ export class SaleReceiptsController {
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
) { ) {
if (acceptHeader.includes(AcceptType.ApplicationPdf)) { if (acceptHeader?.includes(AcceptType.ApplicationPdf)) {
const [pdfContent] = const [pdfContent] =
await this.saleReceiptApplication.getSaleReceiptPdf(id); await this.saleReceiptApplication.getSaleReceiptPdf(id);
@@ -177,7 +177,7 @@ export class SaleReceiptsController {
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
}); });
res.send(pdfContent); res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) { } else if (acceptHeader?.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent = const htmlContent =
await this.saleReceiptApplication.getSaleReceiptHtml(id); await this.saleReceiptApplication.getSaleReceiptHtml(id);

View File

@@ -38,7 +38,7 @@ export class CreateSaleReceipt {
@Inject(Customer.name) @Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>, private readonly customerModel: TenantModelProxy<typeof Customer>,
) {} ) { }
/** /**
* Creates a new sale receipt with associated entries. * Creates a new sale receipt with associated entries.
@@ -89,7 +89,7 @@ export class CreateSaleReceipt {
// Inserts the sale receipt graph to the storage. // Inserts the sale receipt graph to the storage.
const saleReceipt = await this.saleReceiptModel() const saleReceipt = await this.saleReceiptModel()
.query() .query(trx)
.upsertGraph({ .upsertGraph({
...saleReceiptObj, ...saleReceiptObj,
}); });

View File

@@ -163,7 +163,11 @@ describe('Financial Statements (e2e)', () => {
it('/reports/transactions-by-reference (GET)', () => { it('/reports/transactions-by-reference (GET)', () => {
return request(app.getHttpServer()) return request(app.getHttpServer())
.get('/reports/transactions-by-reference') .get('/reports/transactions-by-reference')
.query(baseQuery) .query({
...baseQuery,
referenceId: '1',
referenceType: 'SaleInvoice',
})
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.expect(200); .expect(200);

View File

@@ -7,13 +7,13 @@ const createRoleRequest = () => ({
roleDescription: faker.lorem.sentence(), roleDescription: faker.lorem.sentence(),
permissions: [ permissions: [
{ {
subject: 'items', subject: 'Item',
ability: 'read', ability: 'View',
value: true, value: true,
}, },
{ {
subject: 'items', subject: 'Item',
ability: 'create', ability: 'Create',
value: true, value: true,
}, },
], ],
@@ -27,7 +27,7 @@ describe('Roles (e2e)', () => {
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.send(createRoleRequest()) .send(createRoleRequest())
.expect(200); .expect(201);
}); });
it('/roles (GET)', () => { it('/roles (GET)', () => {
@@ -61,16 +61,16 @@ describe('Roles (e2e)', () => {
.expect(200); .expect(200);
}); });
it('/roles/:id (POST)', async () => { it('/roles/:id (PUT)', async () => {
const response = await request(app.getHttpServer()) const createResponse = await request(app.getHttpServer())
.post('/roles') .post('/roles')
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.send(createRoleRequest()); .send(createRoleRequest());
const roleId = response.body.data.id; const roleId = createResponse.body.data.id;
return request(app.getHttpServer()) return request(app.getHttpServer())
.post(`/roles/${roleId}`) .put(`/roles/${roleId}`)
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.send({ .send({
@@ -78,9 +78,8 @@ describe('Roles (e2e)', () => {
roleDescription: faker.lorem.sentence(), roleDescription: faker.lorem.sentence(),
permissions: [ permissions: [
{ {
permissionId: 1, subject: 'Item',
subject: 'items', ability: 'View',
ability: 'read',
value: true, value: true,
}, },
], ],
@@ -94,7 +93,7 @@ describe('Roles (e2e)', () => {
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.send(createRoleRequest()); .send(createRoleRequest());
const roleId = response.body.data.roleId; const roleId = response.body.data.id;
return request(app.getHttpServer()) return request(app.getHttpServer())
.delete(`/roles/${roleId}`) .delete(`/roles/${roleId}`)

View File

@@ -49,7 +49,7 @@ describe('Sale Receipts (e2e)', () => {
costPrice: 100, costPrice: 100,
sellPrice: 100, sellPrice: 100,
}); });
itemId = parseInt(item.text, 10); itemId = parseInt(item.body.id, 10);
}); });
it('/sale-reeipts (POST)', () => { it('/sale-reeipts (POST)', () => {