This commit is contained in:
Ahmed Bouhuolia
2025-11-20 17:41:16 +02:00
parent d90b6ffbe7
commit 56e00d254b
71 changed files with 1167 additions and 185 deletions

View File

@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { castArray, uniq } from 'lodash';
import { PromisePool } from '@supercharge/promise-pool';
import { DeleteVendorService } from './commands/DeleteVendor.service';
@Injectable()
export class BulkDeleteVendorsService {
constructor(private readonly deleteVendorService: DeleteVendorService) {}
public async bulkDeleteVendors(
vendorIds: number[],
options?: { skipUndeletable?: boolean },
): Promise<void> {
const { skipUndeletable = false } = options ?? {};
const ids = uniq(castArray(vendorIds));
const results = await PromisePool.withConcurrency(1)
.for(ids)
.process(async (vendorId: number) => {
try {
await this.deleteVendorService.deleteVendor(vendorId);
} catch (error) {
if (!skipUndeletable) {
throw error;
}
}
});
if (!skipUndeletable && results.errors && results.errors.length > 0) {
throw results.errors[0].raw ?? results.errors[0];
}
}
}

View File

@@ -0,0 +1,61 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
import { DeleteVendorService } from './commands/DeleteVendor.service';
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
import { ServiceError } from '@/modules/Items/ServiceError';
@Injectable()
export class ValidateBulkDeleteVendorsService {
constructor(
private readonly deleteVendorService: DeleteVendorService,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantKnex: () => Knex,
) {}
public async validateBulkDeleteVendors(vendorIds: number[]): Promise<{
deletableCount: number;
nonDeletableCount: number;
deletableIds: number[];
nonDeletableIds: number[];
}> {
const trx = await this.tenantKnex().transaction({
isolationLevel: 'read uncommitted',
});
try {
const deletableIds: number[] = [];
const nonDeletableIds: number[] = [];
for (const vendorId of vendorIds) {
try {
await this.deleteVendorService.deleteVendor(vendorId, trx);
deletableIds.push(vendorId);
} catch (error) {
if (
error instanceof ModelHasRelationsError ||
(error instanceof ServiceError &&
error.errorType === 'VENDOR_HAS_TRANSACTIONS')
) {
nonDeletableIds.push(vendorId);
} else {
nonDeletableIds.push(vendorId);
}
}
}
await trx.rollback();
return {
deletableCount: deletableIds.length,
nonDeletableCount: nonDeletableIds.length,
deletableIds,
nonDeletableIds,
};
} catch (error) {
await trx.rollback();
throw error;
}
}
}

View File

@@ -13,11 +13,20 @@ import {
IVendorOpeningBalanceEditDTO,
IVendorsFilter,
} from './types/Vendors.types';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiOperation,
ApiResponse,
ApiTags,
getSchemaPath,
} from '@nestjs/swagger';
import { CreateVendorDto } from './dtos/CreateVendor.dto';
import { EditVendorDto } from './dtos/EditVendor.dto';
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import {
BulkDeleteVendorsDto,
ValidateBulkDeleteVendorsResponseDto,
} from './dtos/BulkDeleteVendors.dto';
@Controller('vendors')
@ApiTags('Vendors')
@@ -66,4 +75,37 @@ export class VendorsController {
openingBalanceDTO,
);
}
@Post('validate-bulk-delete')
@ApiOperation({
summary:
'Validates which vendors can be deleted and returns counts of deletable and non-deletable vendors.',
})
@ApiResponse({
status: 200,
description:
'Validation completed. Returns counts and IDs of deletable and non-deletable vendors.',
schema: { $ref: getSchemaPath(ValidateBulkDeleteVendorsResponseDto) },
})
validateBulkDeleteVendors(
@Body() bulkDeleteDto: BulkDeleteVendorsDto,
): Promise<ValidateBulkDeleteVendorsResponseDto> {
return this.vendorsApplication.validateBulkDeleteVendors(
bulkDeleteDto.ids,
);
}
@Post('bulk-delete')
@ApiOperation({ summary: 'Deletes multiple vendors in bulk.' })
@ApiResponse({
status: 200,
description: 'The vendors have been successfully deleted.',
})
async bulkDeleteVendors(
@Body() bulkDeleteDto: BulkDeleteVendorsDto,
): Promise<void> {
return this.vendorsApplication.bulkDeleteVendors(bulkDeleteDto.ids, {
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
});
}
}

View File

@@ -16,6 +16,8 @@ import { GetVendorsService } from './queries/GetVendors.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { VendorsExportable } from './VendorsExportable';
import { VendorsImportable } from './VendorsImportable';
import { BulkDeleteVendorsService } from './BulkDeleteVendors.service';
import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.service';
@Module({
imports: [TenancyDatabaseModule, DynamicListModule],
@@ -31,6 +33,8 @@ import { VendorsImportable } from './VendorsImportable';
VendorValidators,
DeleteVendorService,
VendorsApplication,
BulkDeleteVendorsService,
ValidateBulkDeleteVendorsService,
TransformerInjectable,
TenancyContext,
VendorsExportable,

View File

@@ -13,6 +13,8 @@ import { GetVendorsService } from './queries/GetVendors.service';
import { CreateVendorDto } from './dtos/CreateVendor.dto';
import { EditVendorDto } from './dtos/EditVendor.dto';
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
import { BulkDeleteVendorsService } from './BulkDeleteVendors.service';
import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.service';
@Injectable()
export class VendorsApplication {
@@ -23,6 +25,8 @@ export class VendorsApplication {
private editOpeningBalanceService: EditOpeningBalanceVendorService,
private getVendorService: GetVendorService,
private getVendorsService: GetVendorsService,
private readonly bulkDeleteVendorsService: BulkDeleteVendorsService,
private readonly validateBulkDeleteVendorsService: ValidateBulkDeleteVendorsService,
) {}
/**
@@ -86,4 +90,20 @@ export class VendorsApplication {
public getVendors(filterDTO: GetVendorsQueryDto) {
return this.getVendorsService.getVendorsList(filterDTO);
}
public bulkDeleteVendors(
vendorIds: number[],
options?: { skipUndeletable?: boolean },
) {
return this.bulkDeleteVendorsService.bulkDeleteVendors(
vendorIds,
options,
);
}
public validateBulkDeleteVendors(vendorIds: number[]) {
return this.validateBulkDeleteVendorsService.validateBulkDeleteVendors(
vendorIds,
);
}
}

View File

@@ -29,12 +29,10 @@ export class DeleteVendorService {
* @param {number} vendorId
* @return {Promise<void>}
*/
public async deleteVendor(vendorId: number) {
public async deleteVendor(vendorId: number, trx?: Knex.Transaction) {
// Retrieves the old vendor or throw not found service error.
const oldVendor = await this.vendorModel()
.query()
.findById(vendorId)
.throwIfNotFound();
const query = this.vendorModel().query(trx);
const oldVendor = await query.findById(vendorId).throwIfNotFound();
// Triggers `onVendorDeleting` event.
await this.eventPublisher.emitAsync(events.vendors.onDeleting, {
@@ -43,10 +41,10 @@ export class DeleteVendorService {
} as IVendorEventDeletingPayload);
// Deletes vendor contact under unit-of-work.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
return this.uow.withTransaction(async (transaction: Knex.Transaction) => {
// Deletes the vendor contact from the storage.
await this.vendorModel()
.query(trx)
.query(transaction)
.findById(vendorId)
.deleteIfNoRelations({
type: ERRORS.VENDOR_HAS_TRANSACTIONS,
@@ -56,8 +54,8 @@ export class DeleteVendorService {
await this.eventPublisher.emitAsync(events.vendors.onDeleted, {
vendorId,
oldVendor,
trx,
trx: transaction,
} as IVendorEventDeletedPayload);
});
}, trx);
}
}

View File

@@ -0,0 +1,62 @@
import {
ArrayNotEmpty,
IsArray,
IsInt,
IsOptional,
IsBoolean,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { parseBoolean } from '@/utils/parse-boolean';
export class BulkDeleteVendorsDto {
@IsArray()
@ArrayNotEmpty()
@IsInt({ each: true })
@ApiProperty({
description: 'Array of vendor IDs to delete',
type: [Number],
example: [1, 2, 3],
})
ids: number[];
@IsOptional()
@IsBoolean()
@Transform(({ value, obj }) => parseBoolean(value ?? obj?.skip_undeletable, false))
@ApiPropertyOptional({
description:
'When true, undeletable vendors will be skipped and only deletable ones removed.',
type: Boolean,
default: false,
})
skipUndeletable?: boolean;
}
export class ValidateBulkDeleteVendorsResponseDto {
@ApiProperty({
description: 'Number of vendors that can be deleted',
example: 2,
})
deletableCount: number;
@ApiProperty({
description: 'Number of vendors that cannot be deleted',
example: 1,
})
nonDeletableCount: number;
@ApiProperty({
description: 'IDs of vendors that can be deleted',
type: [Number],
example: [1, 2],
})
deletableIds: number[];
@ApiProperty({
description: 'IDs of vendors that cannot be deleted',
type: [Number],
example: [3],
})
nonDeletableIds: number[];
}