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

@@ -11,7 +11,7 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
useFactory: (configService: ConfigService) => {
// Use in-memory storage with very high limits for test environment
const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
if (isTest) {
return {
throttlers: [
@@ -35,10 +35,11 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
const password = configService.get<string>('redis.password');
const db = configService.get<number>('redis.db');
const globalTtl = configService.get<number>('throttle.global.ttl');
const globalLimit = configService.get<number>('throttle.global.limit');
const authTtl = configService.get<number>('throttle.auth.ttl');
const authLimit = configService.get<number>('throttle.auth.limit');
// Ensure we always have valid numbers with fallback defaults
const globalTtl = configService.get<number>('throttle.global.ttl') ?? 60000;
const globalLimit = configService.get<number>('throttle.global.limit') ?? 100;
const authTtl = configService.get<number>('throttle.auth.ttl') ?? 60000;
const authLimit = configService.get<number>('throttle.auth.limit') ?? 10;
return {
throttlers: [

View File

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

View File

@@ -1,6 +1,6 @@
import { Controller, Get, Query } from '@nestjs/common';
import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication';
import { ITransactionsByReferenceQuery } from './TransactionsByReference.types';
import { TransactionsByReferenceQueryDto } from './TransactionsByReferenceQuery.dto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('reports/transactions-by-reference')
@@ -8,13 +8,13 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
export class TransactionsByReferenceController {
constructor(
private readonly transactionsByReferenceApp: TransactionsByReferenceApplication,
) {}
) { }
@Get()
@ApiResponse({ status: 200, description: 'Transactions by reference' })
@ApiOperation({ summary: 'Get transactions by reference' })
async getTransactionsByReference(
@Query() query: ITransactionsByReferenceQuery,
@Query() query: TransactionsByReferenceQueryDto,
) {
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 {
ITransactionsByReferencePojo,
ITransactionsByReferenceQuery,
@@ -13,7 +13,7 @@ export class TransactionsByReferenceService {
constructor(
private readonly repository: TransactionsByReferenceRepository,
private readonly tenancyContext: TenancyContext
) {}
) { }
/**
* Retrieve accounts transactions by given reference id and type.
@@ -23,6 +23,12 @@ export class TransactionsByReferenceService {
public async getTransactionsByReference(
query: ITransactionsByReferenceQuery
): 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 = {
...getTransactionsByReferenceQuery(),
...query,
@@ -31,7 +37,7 @@ export class TransactionsByReferenceService {
// Retrieve the accounts transactions of the given reference.
const transactions = await this.repository.getTransactions(
Number(filter.referenceId),
referenceId,
filter.referenceType
);
// 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 }),
// @ts-ignore
// referenceTypeFormatted: transaction.referenceTypeFormatted,
referenceTypeFormatted: '',
// formattedReferenceType: transaction.referenceTypeFormatted,
formattedReferenceType: '',
referenceType: transaction.referenceType,
referenceId: transaction.referenceId,
contactId: transaction.contactId,
contactType: transaction.contactType,
contactTypeFormatted: transaction.contactType,
formattedContactType: transaction.contactType || '',
accountName: transaction.account.name,
accountCode: transaction.account.code,
accountName: transaction.account?.name || '',
accountCode: transaction.account?.code || '',
accountId: transaction.accountId,
};
};

View File

@@ -32,7 +32,7 @@ export class PaymentReceivedValidators {
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
) {}
) { }
/**
* Validates the payment existance.
@@ -79,11 +79,11 @@ export class PaymentReceivedValidators {
const invoicesIds = paymentReceiveEntries
.map((e: { invoiceId: number }) => e.invoiceId)
.filter((id): id is number => id !== undefined && id !== null);
if (invoicesIds.length === 0) {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
}
const storedInvoices = await this.saleInvoiceModel()
.query()
.whereIn('id', invoicesIds)

View File

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

View File

@@ -17,7 +17,7 @@ export class EditRoleService {
@Inject(Role.name)
private readonly roleModel: TenantModelProxy<typeof Role>,
) {}
) { }
/**
* 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.
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.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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