This commit is contained in:
Ahmed Bouhuolia
2026-01-15 22:04:42 +02:00
parent 3c1273becb
commit 2bbc154f18
34 changed files with 301 additions and 176 deletions

View File

@@ -9,6 +9,27 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
imports: [ConfigModule],
inject: [ConfigService],
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: [
{
name: 'default',
ttl: 60000,
limit: 1000000, // Effectively disable throttling in tests
},
{
name: 'auth',
ttl: 60000,
limit: 1000000, // Effectively disable throttling in tests
},
],
// No storage specified = uses in-memory storage
};
}
const host = configService.get<string>('redis.host') || 'localhost';
const port = Number(configService.get<number>('redis.port') || 6379);
const password = configService.get<string>('redis.password');

View File

@@ -1,5 +1,5 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { BankingMatchingApplication } from './BankingMatchingApplication';
import { GetMatchedTransactionsFilter } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@@ -34,7 +34,7 @@ export class BankingMatchingController {
);
}
@Post('/unmatch/:uncategorizedTransactionId')
@Patch('/unmatch/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
async unmatchMatchedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number,

View File

@@ -11,10 +11,12 @@ import {
Post,
Body,
Put,
Patch,
Param,
Delete,
Get,
Query,
HttpCode,
} from '@nestjs/common';
import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types';
@@ -40,6 +42,7 @@ export class BillsController {
@ApiOperation({
summary: 'Validate which bills can be deleted and return the results.',
})
@HttpCode(200)
@ApiResponse({
status: 200,
description:
@@ -56,6 +59,7 @@ export class BillsController {
@Post('bulk-delete')
@ApiOperation({ summary: 'Deletes multiple bills.' })
@HttpCode(200)
@ApiResponse({
status: 200,
description: 'Bills deleted successfully',
@@ -160,7 +164,7 @@ export class BillsController {
return this.billsApplication.getBill(billId);
}
@Post(':id/open')
@Patch(':id/open')
@ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({
name: 'id',

View File

@@ -6,7 +6,7 @@ import { DeleteItemService } from './DeleteItem.service';
@Injectable()
export class BulkDeleteItemsService {
constructor(private readonly deleteItemService: DeleteItemService) {}
constructor(private readonly deleteItemService: DeleteItemService) { }
/**
* Deletes multiple items.

View File

@@ -163,6 +163,41 @@ export class ItemsController extends TenantController {
return { id: itemId, message: 'The item has been successfully updated.' };
}
@Post('validate-bulk-delete')
@HttpCode(200)
@ApiOperation({
summary:
'Validates which items can be deleted and returns counts of deletable and non-deletable items.',
})
@ApiResponse({
status: 200,
description:
'Validation completed. Returns counts and IDs of deletable and non-deletable items.',
schema: {
$ref: getSchemaPath(ValidateBulkDeleteItemsResponseDto),
},
})
async validateBulkDeleteItems(
@Body() bulkDeleteDto: BulkDeleteItemsDto,
): Promise<ValidateBulkDeleteItemsResponseDto> {
return this.itemsApplication.validateBulkDeleteItems(bulkDeleteDto.ids);
}
@Post('bulk-delete')
@HttpCode(200)
@ApiOperation({ summary: 'Deletes multiple items in bulk.' })
@ApiResponse({
status: 200,
description: 'The items have been successfully deleted.',
})
async bulkDeleteItems(
@Body() bulkDeleteDto: BulkDeleteItemsDto,
): Promise<void> {
return this.itemsApplication.bulkDeleteItems(bulkDeleteDto.ids, {
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
});
}
@Post()
@ApiOperation({ summary: 'Create a new item (product or service).' })
@ApiResponse({
@@ -352,37 +387,4 @@ export class ItemsController extends TenantController {
return this.itemsApplication.getItemReceiptsTransactions(itemId);
}
@Post('validate-bulk-delete')
@HttpCode(200)
@ApiOperation({
summary:
'Validates which items can be deleted and returns counts of deletable and non-deletable items.',
})
@ApiResponse({
status: 200,
description:
'Validation completed. Returns counts and IDs of deletable and non-deletable items.',
schema: {
$ref: getSchemaPath(ValidateBulkDeleteItemsResponseDto),
},
})
async validateBulkDeleteItems(
@Body() bulkDeleteDto: BulkDeleteItemsDto,
): Promise<ValidateBulkDeleteItemsResponseDto> {
return this.itemsApplication.validateBulkDeleteItems(bulkDeleteDto.ids);
}
@Post('bulk-delete')
@ApiOperation({ summary: 'Deletes multiple items in bulk.' })
@ApiResponse({
status: 200,
description: 'The items have been successfully deleted.',
})
async bulkDeleteItems(
@Body() bulkDeleteDto: BulkDeleteItemsDto,
): Promise<void> {
return this.itemsApplication.bulkDeleteItems(bulkDeleteDto.ids, {
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
});
}
}

View File

@@ -229,7 +229,7 @@ export class PaymentReceivesController {
@Headers('accept') acceptHeader: string,
@Res({ passthrough: true }) res: Response,
) {
if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
if (acceptHeader?.includes(AcceptType.ApplicationPdf)) {
const [pdfContent, filename] = await this.paymentReceivesApplication.getPaymentReceivePdf(
paymentReceiveId,
);
@@ -239,7 +239,7 @@ export class PaymentReceivesController {
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
} else if (acceptHeader?.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent =
await this.paymentReceivesApplication.getPaymentReceivedHtml(
paymentReceiveId,

View File

@@ -76,9 +76,14 @@ export class PaymentReceivedValidators {
customerId: number,
paymentReceiveEntries: { invoiceId: number }[],
): Promise<SaleInvoice[]> {
const invoicesIds = paymentReceiveEntries.map(
(e: { invoiceId: number }) => e.invoiceId,
);
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

@@ -5,11 +5,9 @@ import {
Delete,
Param,
Body,
Next,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import { NextFunction } from 'express';
import { CreateRoleDto, EditRoleDto } from './dtos/Role.dto';
import { RolesApplication } from './Roles.application';
import {
@@ -29,7 +27,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
@ApiExtraModels(RoleResponseDto)
@ApiCommonHeaders()
export class RolesController {
constructor(private readonly rolesApp: RolesApplication) {}
constructor(private readonly rolesApp: RolesApplication) { }
@Post()
@ApiOperation({ summary: 'Create a new role' })
@@ -38,14 +36,11 @@ export class RolesController {
status: HttpStatus.OK,
description: 'Role created successfully',
})
async createRole(
@Next() next: NextFunction,
@Body() createRoleDto: CreateRoleDto,
) {
async createRole(@Body() createRoleDto: CreateRoleDto) {
const role = await this.rolesApp.createRole(createRoleDto);
return {
data: { roleId: role.id },
data: { id: role.id },
message: 'The role has been created successfully.',
};
}
@@ -65,7 +60,7 @@ export class RolesController {
const role = await this.rolesApp.editRole(roleId, editRoleDto);
return {
data: { roleId },
data: { id: role.id },
message: 'The given role has been updated successfully.',
};
}
@@ -81,7 +76,7 @@ export class RolesController {
await this.rolesApp.deleteRole(roleId);
return {
data: { roleId },
data: { id: roleId },
message: 'The given role has been deleted successfully.',
};
}

View File

@@ -210,7 +210,7 @@ export class SaleInvoicesController {
@Headers('accept') acceptHeader: string,
@Res({ passthrough: true }) res: Response,
) {
if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
if (acceptHeader?.includes(AcceptType.ApplicationPdf)) {
const [pdfContent, filename] =
await this.saleInvoiceApplication.saleInvoicePdf(id);
@@ -219,7 +219,7 @@ export class SaleInvoicesController {
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} else if (acceptHeader.includes(AcceptType.ApplicationTextHtml)) {
} else if (acceptHeader?.includes(AcceptType.ApplicationTextHtml)) {
const htmlContent = await this.saleInvoiceApplication.saleInvoiceHtml(id);
return { htmlContent };
} else {

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { UsersApplication } from './Users.application';
import { InviteUserDto, SendInviteUserDto } from './dtos/InviteUser.dto';
@@ -38,7 +38,7 @@ export class UsersInviteController {
/**
* Send an invitation to a new user.
*/
@Post()
@Patch()
@ApiOperation({ summary: 'Send an invitation to a new user.' })
async sendInvite(@Body() sendInviteDTO: SendInviteUserDto) {
const result = await this.usersApplication.sendInvite(sendInviteDTO);

View File

@@ -18,7 +18,7 @@ export class EditUserService {
private readonly tenantUserModel: TenantModelProxy<typeof TenantUser>,
private readonly eventEmitter: EventEmitter2,
private readonly tenancyContext: TenancyContext,
) {}
) { }
/**
* Creates a new user.
@@ -52,7 +52,10 @@ export class EditUserService {
const tenantUser = await this.tenantUserModel()
.query()
.updateAndFetchById(userId, {
...editUserDTO,
firstName: editUserDTO.firstName,
lastName: editUserDTO.lastName,
email: editUserDTO.email,
roleId: editUserDTO.roleId,
});
// Triggers `onTenantUserEdited` event.
await this.eventEmitter.emitAsync(events.tenantUser.onEdited, {