@@ -0,0 +1,55 @@
|
|||||||
|
import { IsArray, IsInt, ArrayNotEmpty, IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { parseBoolean } from '@/utils/parse-boolean';
|
||||||
|
|
||||||
|
export class BulkDeleteDto {
|
||||||
|
@IsArray()
|
||||||
|
@ArrayNotEmpty()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of 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 items will be skipped and only deletable ones will be removed.',
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
skipUndeletable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidateBulkDeleteResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of items that can be deleted',
|
||||||
|
example: 2,
|
||||||
|
})
|
||||||
|
deletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of items that cannot be deleted',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
nonDeletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of items that can be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [1, 2],
|
||||||
|
})
|
||||||
|
deletableIds: number[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of items that cannot be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [3],
|
||||||
|
})
|
||||||
|
nonDeletableIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -27,16 +27,56 @@ import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionRe
|
|||||||
import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto';
|
import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto';
|
||||||
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('accounts')
|
@Controller('accounts')
|
||||||
@ApiTags('Accounts')
|
@ApiTags('Accounts')
|
||||||
@ApiExtraModels(AccountResponseDto)
|
@ApiExtraModels(AccountResponseDto)
|
||||||
@ApiExtraModels(AccountTypeResponseDto)
|
@ApiExtraModels(AccountTypeResponseDto)
|
||||||
@ApiExtraModels(GetAccountTransactionResponseDto)
|
@ApiExtraModels(GetAccountTransactionResponseDto)
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class AccountsController {
|
export class AccountsController {
|
||||||
constructor(private readonly accountsApplication: AccountsApplication) { }
|
constructor(private readonly accountsApplication: AccountsApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed. Returns counts and IDs of deletable and non-deletable accounts.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async validateBulkDeleteAccounts(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.accountsApplication.validateBulkDeleteAccounts(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The accounts have been successfully deleted.',
|
||||||
|
})
|
||||||
|
async bulkDeleteAccounts(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.accountsApplication.bulkDeleteAccounts(bulkDeleteDto.ids, {
|
||||||
|
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create an account' })
|
@ApiOperation({ summary: 'Create an account' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { GetAccountsService } from './GetAccounts.service';
|
|||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
import { AccountsExportable } from './AccountsExportable.service';
|
import { AccountsExportable } from './AccountsExportable.service';
|
||||||
import { AccountsImportable } from './AccountsImportable.service';
|
import { AccountsImportable } from './AccountsImportable.service';
|
||||||
|
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||||
|
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||||
|
|
||||||
const models = [RegisterTenancyModel(BankAccount)];
|
const models = [RegisterTenancyModel(BankAccount)];
|
||||||
|
|
||||||
@@ -40,7 +42,9 @@ const models = [RegisterTenancyModel(BankAccount)];
|
|||||||
GetAccountTransactionsService,
|
GetAccountTransactionsService,
|
||||||
GetAccountsService,
|
GetAccountsService,
|
||||||
AccountsExportable,
|
AccountsExportable,
|
||||||
AccountsImportable
|
AccountsImportable,
|
||||||
|
BulkDeleteAccountsService,
|
||||||
|
ValidateBulkDeleteAccountsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AccountRepository,
|
AccountRepository,
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { GetAccountsService } from './GetAccounts.service';
|
|||||||
import { IFilterMeta } from '@/interfaces/Model';
|
import { IFilterMeta } from '@/interfaces/Model';
|
||||||
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
|
import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.dto';
|
||||||
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto';
|
||||||
|
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||||
|
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||||
|
import { ValidateBulkDeleteResponseDto } from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountsApplication {
|
export class AccountsApplication {
|
||||||
@@ -37,6 +40,8 @@ export class AccountsApplication {
|
|||||||
private readonly getAccountService: GetAccount,
|
private readonly getAccountService: GetAccount,
|
||||||
private readonly getAccountTransactionsService: GetAccountTransactionsService,
|
private readonly getAccountTransactionsService: GetAccountTransactionsService,
|
||||||
private readonly getAccountsService: GetAccountsService,
|
private readonly getAccountsService: GetAccountsService,
|
||||||
|
private readonly bulkDeleteAccountsService: BulkDeleteAccountsService,
|
||||||
|
private readonly validateBulkDeleteAccountsService: ValidateBulkDeleteAccountsService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,4 +133,28 @@ export class AccountsApplication {
|
|||||||
): Promise<Array<GetAccountTransactionResponseDto>> => {
|
): Promise<Array<GetAccountTransactionResponseDto>> => {
|
||||||
return this.getAccountTransactionsService.getAccountsTransactions(filter);
|
return this.getAccountTransactionsService.getAccountsTransactions(filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which accounts can be deleted in bulk.
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteAccounts = (
|
||||||
|
accountIds: number[],
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> => {
|
||||||
|
return this.validateBulkDeleteAccountsService.validateBulkDeleteAccounts(
|
||||||
|
accountIds,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple accounts in bulk.
|
||||||
|
*/
|
||||||
|
public bulkDeleteAccounts = (
|
||||||
|
accountIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
): Promise<void> => {
|
||||||
|
return this.bulkDeleteAccountsService.bulkDeleteAccounts(
|
||||||
|
accountIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteAccount } from './DeleteAccount.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteAccountsService {
|
||||||
|
constructor(private readonly deleteAccountService: DeleteAccount) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple accounts.
|
||||||
|
* @param {number | Array<number>} accountIds - The account id or ids.
|
||||||
|
* @param {Knex.Transaction} trx - The transaction.
|
||||||
|
*/
|
||||||
|
async bulkDeleteAccounts(
|
||||||
|
accountIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const accountsIds = uniq(castArray(accountIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(accountsIds)
|
||||||
|
.process(async (accountId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteAccountService.deleteAccount(accountId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -50,8 +50,12 @@ export class DeleteAccount {
|
|||||||
/**
|
/**
|
||||||
* Deletes the account from the storage.
|
* Deletes the account from the storage.
|
||||||
* @param {number} accountId
|
* @param {number} accountId
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
*/
|
*/
|
||||||
public deleteAccount = async (accountId: number): Promise<void> => {
|
public deleteAccount = async (
|
||||||
|
accountId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> => {
|
||||||
// Retrieve account or not found service error.
|
// Retrieve account or not found service error.
|
||||||
const oldAccount = await this.accountModel().query().findById(accountId);
|
const oldAccount = await this.accountModel().query().findById(accountId);
|
||||||
|
|
||||||
@@ -82,6 +86,6 @@ export class DeleteAccount {
|
|||||||
oldAccount,
|
oldAccount,
|
||||||
trx,
|
trx,
|
||||||
} as IAccountEventDeletedPayload);
|
} as IAccountEventDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteAccount } from './DeleteAccount.service';
|
||||||
|
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteAccountsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteAccountService: DeleteAccount,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which accounts from the provided IDs can be deleted.
|
||||||
|
* Uses the actual deleteAccount service to validate, ensuring the same validation logic.
|
||||||
|
* Uses a transaction that is always rolled back to ensure no database changes.
|
||||||
|
* @param {number[]} accountIds - Array of account IDs to validate
|
||||||
|
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||||
|
*/
|
||||||
|
public async validateBulkDeleteAccounts(accountIds: 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 accountId of accountIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteAccountService.deleteAccount(accountId, trx);
|
||||||
|
deletableIds.push(accountId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ModelHasRelationsError) {
|
||||||
|
nonDeletableIds.push(accountId);
|
||||||
|
} else {
|
||||||
|
nonDeletableIds.push(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { GetBillsService } from './queries/GetBills.service';
|
import { GetBillsService } from './queries/GetBills.service';
|
||||||
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
||||||
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
||||||
|
import { BulkDeleteBillsService } from './BulkDeleteBills.service';
|
||||||
|
import { ValidateBulkDeleteBillsService } from './ValidateBulkDeleteBills.service';
|
||||||
// import { GetBillPayments } from './queries/GetBillPayments';
|
// import { GetBillPayments } from './queries/GetBillPayments';
|
||||||
// import { GetBills } from './queries/GetBills';
|
// import { GetBills } from './queries/GetBills';
|
||||||
|
|
||||||
@@ -23,7 +25,9 @@ export class BillsApplication {
|
|||||||
private openBillService: OpenBillService,
|
private openBillService: OpenBillService,
|
||||||
private getBillsService: GetBillsService,
|
private getBillsService: GetBillsService,
|
||||||
private getBillPaymentTransactionsService: GetBillPaymentTransactionsService,
|
private getBillPaymentTransactionsService: GetBillPaymentTransactionsService,
|
||||||
) {}
|
private bulkDeleteBillsService: BulkDeleteBillsService,
|
||||||
|
private validateBulkDeleteBillsService: ValidateBulkDeleteBillsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new bill with associated GL entries.
|
* Creates a new bill with associated GL entries.
|
||||||
@@ -53,6 +57,25 @@ export class BillsApplication {
|
|||||||
return this.deleteBillService.deleteBill(billId);
|
return this.deleteBillService.deleteBill(billId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple bills.
|
||||||
|
* @param {number[]} billIds
|
||||||
|
*/
|
||||||
|
public bulkDeleteBills(
|
||||||
|
billIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteBillsService.bulkDeleteBills(billIds, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which bills can be deleted.
|
||||||
|
* @param {number[]} billIds
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteBills(billIds: number[]) {
|
||||||
|
return this.validateBulkDeleteBillsService.validateBulkDeleteBills(billIds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve bills data table list.
|
* Retrieve bills data table list.
|
||||||
* @param {IBillsFilter} billsFilter -
|
* @param {IBillsFilter} billsFilter -
|
||||||
|
|||||||
@@ -22,14 +22,51 @@ import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
|
|||||||
import { BillResponseDto } from './dtos/BillResponse.dto';
|
import { BillResponseDto } from './dtos/BillResponse.dto';
|
||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('bills')
|
@Controller('bills')
|
||||||
@ApiTags('Bills')
|
@ApiTags('Bills')
|
||||||
@ApiExtraModels(BillResponseDto)
|
@ApiExtraModels(BillResponseDto)
|
||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
export class BillsController {
|
export class BillsController {
|
||||||
constructor(private billsApplication: BillsApplication) {}
|
constructor(private billsApplication: BillsApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Validate which bills can be deleted and return the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable bills.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
validateBulkDeleteBills(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.billsApplication.validateBulkDeleteBills(bulkDeleteDto.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Bills deleted successfully',
|
||||||
|
})
|
||||||
|
bulkDeleteBills(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.billsApplication.bulkDeleteBills(bulkDeleteDto.ids, {
|
||||||
|
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new bill.' })
|
@ApiOperation({ summary: 'Create a new bill.' })
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
|||||||
import { BillsExportable } from './commands/BillsExportable';
|
import { BillsExportable } from './commands/BillsExportable';
|
||||||
import { BillsImportable } from './commands/BillsImportable';
|
import { BillsImportable } from './commands/BillsImportable';
|
||||||
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
||||||
|
import { BulkDeleteBillsService } from './BulkDeleteBills.service';
|
||||||
|
import { ValidateBulkDeleteBillsService } from './ValidateBulkDeleteBills.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -63,8 +65,10 @@ import { GetBillPaymentTransactionsService } from './queries/GetBillPayments';
|
|||||||
BillsExportable,
|
BillsExportable,
|
||||||
BillsImportable,
|
BillsImportable,
|
||||||
GetBillPaymentTransactionsService,
|
GetBillPaymentTransactionsService,
|
||||||
|
BulkDeleteBillsService,
|
||||||
|
ValidateBulkDeleteBillsService,
|
||||||
],
|
],
|
||||||
controllers: [BillsController],
|
controllers: [BillsController],
|
||||||
exports: [BillsExportable, BillsImportable],
|
exports: [BillsExportable, BillsImportable],
|
||||||
})
|
})
|
||||||
export class BillsModule {}
|
export class BillsModule { }
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteBill } from './commands/DeleteBill.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteBillsService {
|
||||||
|
constructor(private readonly deleteBillService: DeleteBill) { }
|
||||||
|
|
||||||
|
async bulkDeleteBills(
|
||||||
|
billIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const billsIds = uniq(castArray(billIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(billsIds)
|
||||||
|
.process(async (billId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteBillService.deleteBill(billId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteBill } from './commands/DeleteBill.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteBillsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteBillService: DeleteBill,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteBills(billIds: 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 billId of billIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteBillService.deleteBill(billId, trx);
|
||||||
|
deletableIds.push(billId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(billId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,9 +29,10 @@ export class DeleteBill {
|
|||||||
/**
|
/**
|
||||||
* Deletes the bill with associated entries.
|
* Deletes the bill with associated entries.
|
||||||
* @param {number} billId
|
* @param {number} billId
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
public async deleteBill(billId: number) {
|
public async deleteBill(billId: number, trx?: Knex.Transaction) {
|
||||||
// Retrieve the given bill or throw not found error.
|
// Retrieve the given bill or throw not found error.
|
||||||
const oldBill = await this.billModel()
|
const oldBill = await this.billModel()
|
||||||
.query()
|
.query()
|
||||||
@@ -75,6 +76,6 @@ export class DeleteBill {
|
|||||||
oldBill,
|
oldBill,
|
||||||
trx,
|
trx,
|
||||||
} as IBIllEventDeletedPayload);
|
} as IBIllEventDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteCreditNotesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteCreditNoteService: DeleteCreditNoteService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeleteCreditNotes(
|
||||||
|
creditNoteIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const notesIds = uniq(castArray(creditNoteIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(notesIds)
|
||||||
|
.process(async (creditNoteId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteCreditNoteService.deleteCreditNote(creditNoteId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ import { GetCreditNotesService } from './queries/GetCreditNotes.service';
|
|||||||
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
||||||
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
|
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
|
||||||
import { GetCreditNoteService } from './queries/GetCreditNote.service';
|
import { GetCreditNoteService } from './queries/GetCreditNote.service';
|
||||||
|
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
||||||
|
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreditNoteApplication {
|
export class CreditNoteApplication {
|
||||||
@@ -20,8 +22,10 @@ export class CreditNoteApplication {
|
|||||||
private readonly getCreditNotePdfService: GetCreditNotePdf,
|
private readonly getCreditNotePdfService: GetCreditNotePdf,
|
||||||
private readonly getCreditNotesService: GetCreditNotesService,
|
private readonly getCreditNotesService: GetCreditNotesService,
|
||||||
private readonly getCreditNoteStateService: GetCreditNoteState,
|
private readonly getCreditNoteStateService: GetCreditNoteState,
|
||||||
private readonly getCreditNoteService: GetCreditNoteService
|
private readonly getCreditNoteService: GetCreditNoteService,
|
||||||
) {}
|
private readonly bulkDeleteCreditNotesService: BulkDeleteCreditNotesService,
|
||||||
|
private readonly validateBulkDeleteCreditNotesService: ValidateBulkDeleteCreditNotesService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new credit note.
|
* Creates a new credit note.
|
||||||
@@ -97,4 +101,30 @@ export class CreditNoteApplication {
|
|||||||
getCreditNote(creditNoteId: number) {
|
getCreditNote(creditNoteId: number) {
|
||||||
return this.getCreditNoteService.getCreditNote(creditNoteId);
|
return this.getCreditNoteService.getCreditNote(creditNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple credit notes.
|
||||||
|
* @param {number[]} creditNoteIds
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
bulkDeleteCreditNotes(
|
||||||
|
creditNoteIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteCreditNotesService.bulkDeleteCreditNotes(
|
||||||
|
creditNoteIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which credit notes can be deleted.
|
||||||
|
* @param {number[]} creditNoteIds
|
||||||
|
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||||
|
*/
|
||||||
|
validateBulkDeleteCreditNotes(creditNoteIds: number[]) {
|
||||||
|
return this.validateBulkDeleteCreditNotesService.validateBulkDeleteCreditNotes(
|
||||||
|
creditNoteIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,22 @@ import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
|||||||
import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto';
|
import { CreditNoteResponseDto } from './dtos/CreditNoteResponse.dto';
|
||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('credit-notes')
|
@Controller('credit-notes')
|
||||||
@ApiTags('Credit Notes')
|
@ApiTags('Credit Notes')
|
||||||
@ApiExtraModels(CreditNoteResponseDto)
|
@ApiExtraModels(CreditNoteResponseDto)
|
||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class CreditNotesController {
|
export class CreditNotesController {
|
||||||
/**
|
/**
|
||||||
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
||||||
*/
|
*/
|
||||||
constructor(private creditNoteApplication: CreditNoteApplication) {}
|
constructor(private creditNoteApplication: CreditNoteApplication) { }
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new credit note' })
|
@ApiOperation({ summary: 'Create a new credit note' })
|
||||||
@@ -112,6 +117,42 @@ export class CreditNotesController {
|
|||||||
return this.creditNoteApplication.openCreditNote(creditNoteId);
|
return this.creditNoteApplication.openCreditNote(creditNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which credit notes can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable credit notes.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
validateBulkDeleteCreditNotes(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.creditNoteApplication.validateBulkDeleteCreditNotes(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple credit notes.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Credit notes deleted successfully',
|
||||||
|
})
|
||||||
|
bulkDeleteCreditNotes(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.creditNoteApplication.bulkDeleteCreditNotes(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete a credit note' })
|
@ApiOperation({ summary: 'Delete a credit note' })
|
||||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import { CreditNoteInventoryTransactions } from './commands/CreditNotesInventory
|
|||||||
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||||
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
|
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
|
||||||
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
|
import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/CreditNotesApplyInvoice.module';
|
||||||
|
import { BulkDeleteCreditNotesService } from './BulkDeleteCreditNotes.service';
|
||||||
|
import { ValidateBulkDeleteCreditNotesService } from './ValidateBulkDeleteCreditNotes.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -73,6 +75,8 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
|
|||||||
RefundSyncCreditNoteBalanceSubscriber,
|
RefundSyncCreditNoteBalanceSubscriber,
|
||||||
DeleteCustomerLinkedCreditSubscriber,
|
DeleteCustomerLinkedCreditSubscriber,
|
||||||
CreditNoteAutoSerialSubscriber,
|
CreditNoteAutoSerialSubscriber,
|
||||||
|
BulkDeleteCreditNotesService,
|
||||||
|
ValidateBulkDeleteCreditNotesService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CreateCreditNoteService,
|
CreateCreditNoteService,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteCreditNotesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteCreditNoteService: DeleteCreditNoteService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteCreditNotes(creditNoteIds: 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 creditNoteId of creditNoteIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteCreditNoteService.deleteCreditNote(
|
||||||
|
creditNoteId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(creditNoteId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(creditNoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -49,9 +49,13 @@ export class DeleteCreditNoteService {
|
|||||||
/**
|
/**
|
||||||
* Deletes the given credit note transactions.
|
* Deletes the given credit note transactions.
|
||||||
* @param {number} creditNoteId
|
* @param {number} creditNoteId
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async deleteCreditNote(creditNoteId: number): Promise<void> {
|
public async deleteCreditNote(
|
||||||
|
creditNoteId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
// Retrieve the credit note or throw not found service error.
|
// Retrieve the credit note or throw not found service error.
|
||||||
const oldCreditNote = await this.creditNoteModel()
|
const oldCreditNote = await this.creditNoteModel()
|
||||||
.query()
|
.query()
|
||||||
@@ -88,7 +92,7 @@ export class DeleteCreditNoteService {
|
|||||||
creditNoteId,
|
creditNoteId,
|
||||||
trx,
|
trx,
|
||||||
} as ICreditNoteDeletedPayload);
|
} as ICreditNoteDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { DeleteCustomer } from './commands/DeleteCustomer.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteCustomersService {
|
||||||
|
constructor(private readonly deleteCustomerService: DeleteCustomer) {}
|
||||||
|
|
||||||
|
public async bulkDeleteCustomers(
|
||||||
|
customerIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const ids = uniq(castArray(customerIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(ids)
|
||||||
|
.process(async (customerId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteCustomerService.deleteCustomer(customerId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw ?? results.errors[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,6 +24,10 @@ import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
|
|||||||
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
||||||
import { CustomerResponseDto } from './dtos/CustomerResponse.dto';
|
import { CustomerResponseDto } from './dtos/CustomerResponse.dto';
|
||||||
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
||||||
|
import {
|
||||||
|
BulkDeleteCustomersDto,
|
||||||
|
ValidateBulkDeleteCustomersResponseDto,
|
||||||
|
} from './dtos/BulkDeleteCustomers.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
|
||||||
@Controller('customers')
|
@Controller('customers')
|
||||||
@@ -31,7 +35,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
|||||||
@ApiExtraModels(CustomerResponseDto)
|
@ApiExtraModels(CustomerResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class CustomersController {
|
export class CustomersController {
|
||||||
constructor(private customersApplication: CustomersApplication) {}
|
constructor(private customersApplication: CustomersApplication) { }
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Retrieves the customer details.' })
|
@ApiOperation({ summary: 'Retrieves the customer details.' })
|
||||||
@@ -109,4 +113,37 @@ export class CustomersController {
|
|||||||
openingBalanceDTO,
|
openingBalanceDTO,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed. Returns counts and IDs of deletable and non-deletable customers.',
|
||||||
|
schema: { $ref: getSchemaPath(ValidateBulkDeleteCustomersResponseDto) },
|
||||||
|
})
|
||||||
|
validateBulkDeleteCustomers(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteCustomersDto,
|
||||||
|
): Promise<ValidateBulkDeleteCustomersResponseDto> {
|
||||||
|
return this.customersApplication.validateBulkDeleteCustomers(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'The customers have been successfully deleted.',
|
||||||
|
})
|
||||||
|
async bulkDeleteCustomers(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteCustomersDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.customersApplication.bulkDeleteCustomers(bulkDeleteDto.ids, {
|
||||||
|
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { CustomersExportable } from './CustomersExportable';
|
|||||||
import { CustomersImportable } from './CustomersImportable';
|
import { CustomersImportable } from './CustomersImportable';
|
||||||
import { GetCustomers } from './queries/GetCustomers.service';
|
import { GetCustomers } from './queries/GetCustomers.service';
|
||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
|
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
|
||||||
|
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TenancyDatabaseModule, DynamicListModule],
|
imports: [TenancyDatabaseModule, DynamicListModule],
|
||||||
@@ -37,6 +39,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
|||||||
CustomersExportable,
|
CustomersExportable,
|
||||||
CustomersImportable,
|
CustomersImportable,
|
||||||
GetCustomers,
|
GetCustomers,
|
||||||
|
BulkDeleteCustomersService,
|
||||||
|
ValidateBulkDeleteCustomersService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CustomersModule {}
|
export class CustomersModule {}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
|
|||||||
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
import { EditCustomerDto } from './dtos/EditCustomer.dto';
|
||||||
import { GetCustomers } from './queries/GetCustomers.service';
|
import { GetCustomers } from './queries/GetCustomers.service';
|
||||||
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
import { GetCustomersQueryDto } from './dtos/GetCustomersQuery.dto';
|
||||||
|
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
|
||||||
|
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomersApplication {
|
export class CustomersApplication {
|
||||||
@@ -22,6 +24,8 @@ export class CustomersApplication {
|
|||||||
private deleteCustomerService: DeleteCustomer,
|
private deleteCustomerService: DeleteCustomer,
|
||||||
private editOpeningBalanceService: EditOpeningBalanceCustomer,
|
private editOpeningBalanceService: EditOpeningBalanceCustomer,
|
||||||
private getCustomersService: GetCustomers,
|
private getCustomersService: GetCustomers,
|
||||||
|
private readonly bulkDeleteCustomersService: BulkDeleteCustomersService,
|
||||||
|
private readonly validateBulkDeleteCustomersService: ValidateBulkDeleteCustomersService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,4 +87,20 @@ export class CustomersApplication {
|
|||||||
public getCustomers = (filterDTO: GetCustomersQueryDto) => {
|
public getCustomers = (filterDTO: GetCustomersQueryDto) => {
|
||||||
return this.getCustomersService.getCustomersList(filterDTO);
|
return this.getCustomersService.getCustomersList(filterDTO);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public bulkDeleteCustomers = (
|
||||||
|
customerIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) => {
|
||||||
|
return this.bulkDeleteCustomersService.bulkDeleteCustomers(
|
||||||
|
customerIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public validateBulkDeleteCustomers = (customerIds: number[]) => {
|
||||||
|
return this.validateBulkDeleteCustomersService.validateBulkDeleteCustomers(
|
||||||
|
customerIds,
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteCustomer } from './commands/DeleteCustomer.service';
|
||||||
|
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteCustomersService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteCustomerService: DeleteCustomer,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async validateBulkDeleteCustomers(customerIds: 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 customerId of customerIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteCustomerService.deleteCustomer(customerId, trx);
|
||||||
|
deletableIds.push(customerId);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof ModelHasRelationsError ||
|
||||||
|
(error instanceof ServiceError &&
|
||||||
|
error.errorType === 'CUSTOMER_HAS_TRANSACTIONS')
|
||||||
|
) {
|
||||||
|
nonDeletableIds.push(customerId);
|
||||||
|
} else {
|
||||||
|
nonDeletableIds.push(customerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@ export class DeleteCustomer {
|
|||||||
* @param {number} customerId - Customer ID.
|
* @param {number} customerId - Customer ID.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async deleteCustomer(customerId: number): Promise<void> {
|
public async deleteCustomer(
|
||||||
|
customerId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
// Retrieve the customer or throw not found service error.
|
// Retrieve the customer or throw not found service error.
|
||||||
const oldCustomer = await this.customerModel()
|
const query = this.customerModel().query(trx);
|
||||||
.query()
|
const oldCustomer = await query.findById(customerId).throwIfNotFound();
|
||||||
.findById(customerId)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
// Triggers `onCustomerDeleting` event.
|
// Triggers `onCustomerDeleting` event.
|
||||||
await this.eventPublisher.emitAsync(events.customers.onDeleting, {
|
await this.eventPublisher.emitAsync(events.customers.onDeleting, {
|
||||||
@@ -45,10 +46,10 @@ export class DeleteCustomer {
|
|||||||
} as ICustomerDeletingPayload);
|
} as ICustomerDeletingPayload);
|
||||||
|
|
||||||
// Deletes the customer and associated entities under UOW transaction.
|
// Deletes the customer and associated entities under UOW transaction.
|
||||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(async (transaction: Knex.Transaction) => {
|
||||||
// Delete the customer from the storage.
|
// Delete the customer from the storage.
|
||||||
await this.customerModel()
|
await this.customerModel()
|
||||||
.query(trx)
|
.query(transaction)
|
||||||
.findById(customerId)
|
.findById(customerId)
|
||||||
.deleteIfNoRelations({
|
.deleteIfNoRelations({
|
||||||
type: ERRORS.CUSTOMER_HAS_TRANSACTIONS,
|
type: ERRORS.CUSTOMER_HAS_TRANSACTIONS,
|
||||||
@@ -58,8 +59,8 @@ export class DeleteCustomer {
|
|||||||
await this.eventPublisher.emitAsync(events.customers.onDeleted, {
|
await this.eventPublisher.emitAsync(events.customers.onDeleted, {
|
||||||
customerId,
|
customerId,
|
||||||
oldCustomer,
|
oldCustomer,
|
||||||
trx,
|
trx: transaction,
|
||||||
} as ICustomerEventDeletedPayload);
|
} as ICustomerEventDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 BulkDeleteCustomersDto {
|
||||||
|
@IsArray()
|
||||||
|
@ArrayNotEmpty()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of customer 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 customers will be skipped and only deletable ones removed.',
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
skipUndeletable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidateBulkDeleteCustomersResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of customers that can be deleted',
|
||||||
|
example: 2,
|
||||||
|
})
|
||||||
|
deletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of customers that cannot be deleted',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
nonDeletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of customers that can be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [1, 2],
|
||||||
|
})
|
||||||
|
deletableIds: number[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of customers that cannot be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [3],
|
||||||
|
})
|
||||||
|
nonDeletableIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteExpensesService {
|
||||||
|
constructor(private readonly deleteExpenseService: DeleteExpense) { }
|
||||||
|
|
||||||
|
async bulkDeleteExpenses(
|
||||||
|
expenseIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const expensesIds = uniq(castArray(expenseIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(expensesIds)
|
||||||
|
.process(async (expenseId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteExpenseService.deleteExpense(expenseId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,13 +21,55 @@ import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
|||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { ExpenseResponseDto } from './dtos/ExpenseResponse.dto';
|
import { ExpenseResponseDto } from './dtos/ExpenseResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('expenses')
|
@Controller('expenses')
|
||||||
@ApiTags('Expenses')
|
@ApiTags('Expenses')
|
||||||
@ApiExtraModels(PaginatedResponseDto, ExpenseResponseDto)
|
@ApiExtraModels(
|
||||||
|
PaginatedResponseDto,
|
||||||
|
ExpenseResponseDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class ExpensesController {
|
export class ExpensesController {
|
||||||
constructor(private readonly expensesApplication: ExpensesApplication) {}
|
constructor(private readonly expensesApplication: ExpensesApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Validate which expenses can be deleted and return the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable expenses.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public validateBulkDeleteExpenses(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.expensesApplication.validateBulkDeleteExpenses(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple expenses.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Expenses deleted successfully',
|
||||||
|
})
|
||||||
|
public bulkDeleteExpenses(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
) {
|
||||||
|
return this.expensesApplication.bulkDeleteExpenses(bulkDeleteDto.ids, {
|
||||||
|
skipUndeletable: bulkDeleteDto.skipUndeletable ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new expense transaction.
|
* Create a new expense transaction.
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { GetExpensesService } from './queries/GetExpenses.service';
|
|||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
import { ExpensesExportable } from './ExpensesExportable';
|
import { ExpensesExportable } from './ExpensesExportable';
|
||||||
import { ExpensesImportable } from './ExpensesImportable';
|
import { ExpensesImportable } from './ExpensesImportable';
|
||||||
|
import { BulkDeleteExpensesService } from './BulkDeleteExpenses.service';
|
||||||
|
import { ValidateBulkDeleteExpensesService } from './ValidateBulkDeleteExpenses.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LedgerModule, BranchesModule, DynamicListModule],
|
imports: [LedgerModule, BranchesModule, DynamicListModule],
|
||||||
@@ -41,6 +43,8 @@ import { ExpensesImportable } from './ExpensesImportable';
|
|||||||
GetExpensesService,
|
GetExpensesService,
|
||||||
ExpensesExportable,
|
ExpensesExportable,
|
||||||
ExpensesImportable,
|
ExpensesImportable,
|
||||||
|
BulkDeleteExpensesService,
|
||||||
|
ValidateBulkDeleteExpensesService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ExpensesModule {}
|
export class ExpensesModule {}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { GetExpenseService } from './queries/GetExpense.service';
|
|||||||
import { IExpensesFilter } from './interfaces/Expenses.interface';
|
import { IExpensesFilter } from './interfaces/Expenses.interface';
|
||||||
import { GetExpensesService } from './queries/GetExpenses.service';
|
import { GetExpensesService } from './queries/GetExpenses.service';
|
||||||
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
import { CreateExpenseDto, EditExpenseDto } from './dtos/Expense.dto';
|
||||||
|
import { BulkDeleteExpensesService } from './BulkDeleteExpenses.service';
|
||||||
|
import { ValidateBulkDeleteExpensesService } from './ValidateBulkDeleteExpenses.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpensesApplication {
|
export class ExpensesApplication {
|
||||||
@@ -17,7 +19,9 @@ export class ExpensesApplication {
|
|||||||
private readonly publishExpenseService: PublishExpense,
|
private readonly publishExpenseService: PublishExpense,
|
||||||
private readonly getExpenseService: GetExpenseService,
|
private readonly getExpenseService: GetExpenseService,
|
||||||
private readonly getExpensesService: GetExpensesService,
|
private readonly getExpensesService: GetExpensesService,
|
||||||
) {}
|
private readonly bulkDeleteExpensesService: BulkDeleteExpensesService,
|
||||||
|
private readonly validateBulkDeleteExpensesService: ValidateBulkDeleteExpensesService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new expense transaction.
|
* Create a new expense transaction.
|
||||||
@@ -47,6 +51,30 @@ export class ExpensesApplication {
|
|||||||
return this.deleteExpenseService.deleteExpense(expenseId);
|
return this.deleteExpenseService.deleteExpense(expenseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes expenses in bulk.
|
||||||
|
* @param {number[]} expenseIds - Expense ids.
|
||||||
|
*/
|
||||||
|
public bulkDeleteExpenses(
|
||||||
|
expenseIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteExpensesService.bulkDeleteExpenses(
|
||||||
|
expenseIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which expenses can be deleted.
|
||||||
|
* @param {number[]} expenseIds - Expense ids.
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteExpenses(expenseIds: number[]) {
|
||||||
|
return this.validateBulkDeleteExpensesService.validateBulkDeleteExpenses(
|
||||||
|
expenseIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publishes the given expense.
|
* Publishes the given expense.
|
||||||
* @param {number} expenseId - Expense id.
|
* @param {number} expenseId - Expense id.
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteExpense } from './commands/DeleteExpense.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteExpensesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteExpenseService: DeleteExpense,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteExpenses(expenseIds: 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 expenseId of expenseIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteExpenseService.deleteExpense(expenseId, trx);
|
||||||
|
deletableIds.push(expenseId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(expenseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,12 @@ export class DeleteExpense {
|
|||||||
/**
|
/**
|
||||||
* Deletes the given expense.
|
* Deletes the given expense.
|
||||||
* @param {number} expenseId
|
* @param {number} expenseId
|
||||||
* @param {ISystemUser} authorizedUser
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
*/
|
*/
|
||||||
public async deleteExpense(expenseId: number): Promise<void> {
|
public async deleteExpense(
|
||||||
|
expenseId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
// Retrieves the expense transaction with associated entries or
|
// Retrieves the expense transaction with associated entries or
|
||||||
// throw not found error.
|
// throw not found error.
|
||||||
const oldExpense = await this.expenseModel()
|
const oldExpense = await this.expenseModel()
|
||||||
@@ -74,6 +77,6 @@ export class DeleteExpense {
|
|||||||
oldExpense,
|
oldExpense,
|
||||||
trx,
|
trx,
|
||||||
} as IExpenseEventDeletePayload);
|
} as IExpenseEventDeletePayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteItemCategoriesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteItemCategoryService: DeleteItemCategoryService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeleteItemCategories(
|
||||||
|
itemCategoryIds: number | Array<number>,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const categoriesIds = uniq(castArray(itemCategoryIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(categoriesIds)
|
||||||
|
.process(async (itemCategoryId: number) => {
|
||||||
|
await this.deleteItemCategoryService.deleteItemCategory(itemCategoryId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteItemCategoryService } from './commands/DeleteItemCategory.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteItemCategoriesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteItemCategoryService: DeleteItemCategoryService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async validateBulkDeleteItemCategories(itemCategoryIds: 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 itemCategoryId of itemCategoryIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteItemCategoryService.deleteItemCategory(
|
||||||
|
itemCategoryId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(itemCategoryId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(itemCategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -32,9 +32,13 @@ export class DeleteItemCategoryService {
|
|||||||
/**
|
/**
|
||||||
* Deletes the given item category.
|
* Deletes the given item category.
|
||||||
* @param {number} itemCategoryId - Item category id.
|
* @param {number} itemCategoryId - Item category id.
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async deleteItemCategory(itemCategoryId: number) {
|
public async deleteItemCategory(
|
||||||
|
itemCategoryId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
) {
|
||||||
// Retrieve item category or throw not found error.
|
// Retrieve item category or throw not found error.
|
||||||
const oldItemCategory = await this.itemCategoryModel()
|
const oldItemCategory = await this.itemCategoryModel()
|
||||||
.query()
|
.query()
|
||||||
@@ -56,7 +60,7 @@ export class DeleteItemCategoryService {
|
|||||||
itemCategoryId,
|
itemCategoryId,
|
||||||
oldItemCategory,
|
oldItemCategory,
|
||||||
} as IItemCategoryDeletedPayload);
|
} as IItemCategoryDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteItemService } from './DeleteItem.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteItemsService {
|
||||||
|
constructor(private readonly deleteItemService: DeleteItemService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple items.
|
||||||
|
* @param {number | Array<number>} itemIds - The item id or ids.
|
||||||
|
* @param {Knex.Transaction} trx - The transaction.
|
||||||
|
*/
|
||||||
|
async bulkDeleteItems(
|
||||||
|
itemIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const itemsIds = uniq(castArray(itemIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(itemsIds)
|
||||||
|
.process(async (itemId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteItemService.deleteItem(itemId, trx);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw ?? results.errors[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@ import { ItemEstimatesResponseDto } from './dtos/ItemEstimatesResponse.dto';
|
|||||||
import { ItemBillsResponseDto } from './dtos/ItemBillsResponse.dto';
|
import { ItemBillsResponseDto } from './dtos/ItemBillsResponse.dto';
|
||||||
import { ItemReceiptsResponseDto } from './dtos/ItemReceiptsResponse.dto';
|
import { ItemReceiptsResponseDto } from './dtos/ItemReceiptsResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteItemsDto,
|
||||||
|
ValidateBulkDeleteItemsResponseDto,
|
||||||
|
} from './dtos/BulkDeleteItems.dto';
|
||||||
|
|
||||||
@Controller('/items')
|
@Controller('/items')
|
||||||
@ApiTags('Items')
|
@ApiTags('Items')
|
||||||
@@ -39,6 +43,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
|||||||
@ApiExtraModels(ItemBillsResponseDto)
|
@ApiExtraModels(ItemBillsResponseDto)
|
||||||
@ApiExtraModels(ItemEstimatesResponseDto)
|
@ApiExtraModels(ItemEstimatesResponseDto)
|
||||||
@ApiExtraModels(ItemReceiptsResponseDto)
|
@ApiExtraModels(ItemReceiptsResponseDto)
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class ItemsController extends TenantController {
|
export class ItemsController extends TenantController {
|
||||||
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
||||||
@@ -340,4 +345,37 @@ export class ItemsController extends TenantController {
|
|||||||
const itemId = parseInt(id, 10);
|
const itemId = parseInt(id, 10);
|
||||||
return this.itemsApplication.getItemReceiptsTransactions(itemId);
|
return this.itemsApplication.getItemReceiptsTransactions(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
|||||||
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
|
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
|
||||||
import { ItemsExportable } from './ItemsExportable.service';
|
import { ItemsExportable } from './ItemsExportable.service';
|
||||||
import { ItemsImportable } from './ItemsImportable.service';
|
import { ItemsImportable } from './ItemsImportable.service';
|
||||||
|
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
||||||
|
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -41,8 +43,10 @@ import { ItemsImportable } from './ItemsImportable.service';
|
|||||||
TransformerInjectable,
|
TransformerInjectable,
|
||||||
ItemsEntriesService,
|
ItemsEntriesService,
|
||||||
ItemsExportable,
|
ItemsExportable,
|
||||||
ItemsImportable
|
ItemsImportable,
|
||||||
|
BulkDeleteItemsService,
|
||||||
|
ValidateBulkDeleteItemsService,
|
||||||
],
|
],
|
||||||
exports: [ItemsEntriesService, ItemsExportable, ItemsImportable],
|
exports: [ItemsEntriesService, ItemsExportable, ItemsImportable],
|
||||||
})
|
})
|
||||||
export class ItemsModule {}
|
export class ItemsModule { }
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { GetItemsService } from './GetItems.service';
|
|||||||
import { IItemsFilter } from './types/Items.types';
|
import { IItemsFilter } from './types/Items.types';
|
||||||
import { EditItemDto, CreateItemDto } from './dtos/Item.dto';
|
import { EditItemDto, CreateItemDto } from './dtos/Item.dto';
|
||||||
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
|
import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto';
|
||||||
|
import { BulkDeleteItemsService } from './BulkDeleteItems.service';
|
||||||
|
import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemsApplicationService {
|
export class ItemsApplicationService {
|
||||||
@@ -25,7 +27,9 @@ export class ItemsApplicationService {
|
|||||||
private readonly getItemService: GetItemService,
|
private readonly getItemService: GetItemService,
|
||||||
private readonly getItemsService: GetItemsService,
|
private readonly getItemsService: GetItemsService,
|
||||||
private readonly itemTransactionsService: ItemTransactionsService,
|
private readonly itemTransactionsService: ItemTransactionsService,
|
||||||
) {}
|
private readonly bulkDeleteItemsService: BulkDeleteItemsService,
|
||||||
|
private readonly validateBulkDeleteItemsService: ValidateBulkDeleteItemsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new item.
|
* Creates a new item.
|
||||||
@@ -134,4 +138,30 @@ export class ItemsApplicationService {
|
|||||||
async getItemReceiptsTransactions(itemId: number): Promise<any> {
|
async getItemReceiptsTransactions(itemId: number): Promise<any> {
|
||||||
return this.itemTransactionsService.getItemReceiptTransactions(itemId);
|
return this.itemTransactionsService.getItemReceiptTransactions(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which items can be deleted in bulk.
|
||||||
|
* @param {number[]} itemIds - Array of item IDs to validate
|
||||||
|
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||||
|
*/
|
||||||
|
async validateBulkDeleteItems(itemIds: number[]): Promise<{
|
||||||
|
deletableCount: number;
|
||||||
|
nonDeletableCount: number;
|
||||||
|
deletableIds: number[];
|
||||||
|
nonDeletableIds: number[];
|
||||||
|
}> {
|
||||||
|
return this.validateBulkDeleteItemsService.validateBulkDeleteItems(itemIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple items in bulk.
|
||||||
|
* @param {number[]} itemIds - Array of item IDs to delete
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async bulkDeleteItems(
|
||||||
|
itemIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
return this.bulkDeleteItemsService.bulkDeleteItems(itemIds, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteItemService } from './DeleteItem.service';
|
||||||
|
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteItemsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteItemService: DeleteItemService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which items from the provided IDs can be deleted.
|
||||||
|
* Uses the actual deleteItem service to validate, ensuring the same validation logic.
|
||||||
|
* Uses a transaction that is always rolled back to ensure no database changes.
|
||||||
|
* @param {number[]} itemIds - Array of item IDs to validate
|
||||||
|
* @returns {Promise<{deletableCount: number, nonDeletableCount: number, deletableIds: number[], nonDeletableIds: number[]}>}
|
||||||
|
*/
|
||||||
|
public async validateBulkDeleteItems(itemIds: number[]): Promise<{
|
||||||
|
deletableCount: number;
|
||||||
|
nonDeletableCount: number;
|
||||||
|
deletableIds: number[];
|
||||||
|
nonDeletableIds: number[];
|
||||||
|
}> {
|
||||||
|
// Create a transaction that will be rolled back
|
||||||
|
const trx = await this.tenantKnex().transaction({
|
||||||
|
isolationLevel: 'read uncommitted',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deletableIds: number[] = [];
|
||||||
|
const nonDeletableIds: number[] = [];
|
||||||
|
|
||||||
|
// Check each item to see if it can be deleted by attempting deletion in transaction
|
||||||
|
for (const itemId of itemIds) {
|
||||||
|
try {
|
||||||
|
// Attempt to delete the item using the deleteItem service with the transaction
|
||||||
|
// This will use the exact same validation logic as the actual delete
|
||||||
|
await this.deleteItemService.deleteItem(itemId, trx);
|
||||||
|
|
||||||
|
// If deletion succeeds, item is deletable
|
||||||
|
deletableIds.push(itemId);
|
||||||
|
} catch (error) {
|
||||||
|
// If error occurs, check the type of error
|
||||||
|
if (error instanceof ModelHasRelationsError) {
|
||||||
|
// Item has associated transactions/relations, cannot be deleted
|
||||||
|
nonDeletableIds.push(itemId);
|
||||||
|
} else {
|
||||||
|
// Other errors (e.g., item not found), also mark as non-deletable
|
||||||
|
nonDeletableIds.push(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always rollback the transaction to ensure no changes are persisted
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Rollback in case of any error
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
ArrayNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
import { parseBoolean } from '@/utils/parse-boolean';
|
||||||
|
|
||||||
|
export class BulkDeleteItemsDto {
|
||||||
|
@IsArray()
|
||||||
|
@ArrayNotEmpty()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of item 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 items will be skipped and only deletable ones removed.',
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
skipUndeletable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidateBulkDeleteItemsResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of items that can be deleted',
|
||||||
|
example: 2,
|
||||||
|
})
|
||||||
|
deletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Number of items that cannot be deleted',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
nonDeletableCount: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of items that can be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [1, 2],
|
||||||
|
})
|
||||||
|
deletableIds: number[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'IDs of items that cannot be deleted',
|
||||||
|
type: [Number],
|
||||||
|
example: [3],
|
||||||
|
})
|
||||||
|
nonDeletableIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteManualJournalsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteManualJournalService: DeleteManualJournalService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeleteManualJournals(
|
||||||
|
manualJournalIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const journalsIds = uniq(castArray(manualJournalIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(journalsIds)
|
||||||
|
.process(async (manualJournalId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteManualJournalService.deleteManualJournal(
|
||||||
|
manualJournalId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,13 +25,54 @@ import {
|
|||||||
import { IManualJournalsFilter } from './types/ManualJournals.types';
|
import { IManualJournalsFilter } from './types/ManualJournals.types';
|
||||||
import { ManualJournalResponseDto } from './dtos/ManualJournalResponse.dto';
|
import { ManualJournalResponseDto } from './dtos/ManualJournalResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('manual-journals')
|
@Controller('manual-journals')
|
||||||
@ApiTags('Manual Journals')
|
@ApiTags('Manual Journals')
|
||||||
@ApiExtraModels(ManualJournalResponseDto)
|
@ApiExtraModels(ManualJournalResponseDto)
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class ManualJournalsController {
|
export class ManualJournalsController {
|
||||||
constructor(private manualJournalsApplication: ManualJournalsApplication) {}
|
constructor(private manualJournalsApplication: ManualJournalsApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validate which manual journals can be deleted and return the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable manual journals.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public validateBulkDeleteManualJournals(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.manualJournalsApplication.validateBulkDeleteManualJournals(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple manual journals.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Manual journals deleted successfully',
|
||||||
|
})
|
||||||
|
public bulkDeleteManualJournals(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.manualJournalsApplication.bulkDeleteManualJournals(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new manual journal.' })
|
@ApiOperation({ summary: 'Create a new manual journal.' })
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { ManualJournalsExportable } from './commands/ManualJournalExportable';
|
|||||||
import { ManualJournalImportable } from './commands/ManualJournalsImport';
|
import { ManualJournalImportable } from './commands/ManualJournalsImport';
|
||||||
import { GetManualJournals } from './queries/GetManualJournals.service';
|
import { GetManualJournals } from './queries/GetManualJournals.service';
|
||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
|
import { BulkDeleteManualJournalsService } from './BulkDeleteManualJournals.service';
|
||||||
|
import { ValidateBulkDeleteManualJournalsService } from './ValidateBulkDeleteManualJournals.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BranchesModule, LedgerModule, DynamicListModule],
|
imports: [BranchesModule, LedgerModule, DynamicListModule],
|
||||||
@@ -41,6 +43,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
|||||||
ManualJournalWriteGLSubscriber,
|
ManualJournalWriteGLSubscriber,
|
||||||
ManualJournalsExportable,
|
ManualJournalsExportable,
|
||||||
ManualJournalImportable,
|
ManualJournalImportable,
|
||||||
|
BulkDeleteManualJournalsService,
|
||||||
|
ValidateBulkDeleteManualJournalsService,
|
||||||
],
|
],
|
||||||
exports: [ManualJournalsExportable, ManualJournalImportable],
|
exports: [ManualJournalsExportable, ManualJournalImportable],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
EditManualJournalDto,
|
EditManualJournalDto,
|
||||||
} from './dtos/ManualJournal.dto';
|
} from './dtos/ManualJournal.dto';
|
||||||
import { GetManualJournals } from './queries/GetManualJournals.service';
|
import { GetManualJournals } from './queries/GetManualJournals.service';
|
||||||
|
import { BulkDeleteManualJournalsService } from './BulkDeleteManualJournals.service';
|
||||||
|
import { ValidateBulkDeleteManualJournalsService } from './ValidateBulkDeleteManualJournals.service';
|
||||||
// import { GetManualJournals } from './queries/GetManualJournals';
|
// import { GetManualJournals } from './queries/GetManualJournals';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -21,7 +23,9 @@ export class ManualJournalsApplication {
|
|||||||
private publishManualJournalService: PublishManualJournal,
|
private publishManualJournalService: PublishManualJournal,
|
||||||
private getManualJournalService: GetManualJournal,
|
private getManualJournalService: GetManualJournal,
|
||||||
private getManualJournalsService: GetManualJournals,
|
private getManualJournalsService: GetManualJournals,
|
||||||
) {}
|
private bulkDeleteManualJournalsService: BulkDeleteManualJournalsService,
|
||||||
|
private validateBulkDeleteManualJournalsService: ValidateBulkDeleteManualJournalsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make journal entries.
|
* Make journal entries.
|
||||||
@@ -57,6 +61,30 @@ export class ManualJournalsApplication {
|
|||||||
return this.deleteManualJournalService.deleteManualJournal(manualJournalId);
|
return this.deleteManualJournalService.deleteManualJournal(manualJournalId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk deletes manual journals.
|
||||||
|
* @param {number[]} manualJournalIds
|
||||||
|
*/
|
||||||
|
public bulkDeleteManualJournals = (
|
||||||
|
manualJournalIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) => {
|
||||||
|
return this.bulkDeleteManualJournalsService.bulkDeleteManualJournals(
|
||||||
|
manualJournalIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which manual journals can be deleted.
|
||||||
|
* @param {number[]} manualJournalIds
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteManualJournals = (manualJournalIds: number[]) => {
|
||||||
|
return this.validateBulkDeleteManualJournalsService.validateBulkDeleteManualJournals(
|
||||||
|
manualJournalIds,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish the given manual journal.
|
* Publish the given manual journal.
|
||||||
* @param {number} manualJournalId - Manual journal id.
|
* @param {number} manualJournalId - Manual journal id.
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteManualJournalsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteManualJournalService: DeleteManualJournalService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async validateBulkDeleteManualJournals(
|
||||||
|
manualJournalIds: 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 manualJournalId of manualJournalIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteManualJournalService.deleteManualJournal(
|
||||||
|
manualJournalId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(manualJournalId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(manualJournalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,10 +29,12 @@ export class DeleteManualJournalService {
|
|||||||
/**
|
/**
|
||||||
* Deletes the given manual journal
|
* Deletes the given manual journal
|
||||||
* @param {number} manualJournalId
|
* @param {number} manualJournalId
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public deleteManualJournal = async (
|
public deleteManualJournal = async (
|
||||||
manualJournalId: number,
|
manualJournalId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
oldManualJournal: ManualJournal;
|
oldManualJournal: ManualJournal;
|
||||||
}> => {
|
}> => {
|
||||||
@@ -70,6 +72,6 @@ export class DeleteManualJournalService {
|
|||||||
} as IManualJournalEventDeletedPayload);
|
} as IManualJournalEventDeletedPayload);
|
||||||
|
|
||||||
return { oldManualJournal };
|
return { oldManualJournal };
|
||||||
});
|
}, trx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeletePaymentReceivedService } from './commands/DeletePaymentReceived.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeletePaymentReceivedService {
|
||||||
|
constructor(
|
||||||
|
private readonly deletePaymentReceivedService: DeletePaymentReceivedService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeletePaymentReceived(
|
||||||
|
paymentReceiveIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const paymentsIds = uniq(castArray(paymentReceiveIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(paymentsIds)
|
||||||
|
.process(async (paymentReceiveId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deletePaymentReceivedService.deletePaymentReceive(
|
||||||
|
paymentReceiveId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
} from './dtos/PaymentReceived.dto';
|
} from './dtos/PaymentReceived.dto';
|
||||||
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
|
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
|
||||||
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
||||||
|
import { BulkDeletePaymentReceivedService } from './BulkDeletePaymentReceived.service';
|
||||||
|
import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePaymentReceived.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentReceivesApplication {
|
export class PaymentReceivesApplication {
|
||||||
@@ -33,7 +35,9 @@ export class PaymentReceivesApplication {
|
|||||||
private getPaymentReceivePdfService: GetPaymentReceivedPdfService,
|
private getPaymentReceivePdfService: GetPaymentReceivedPdfService,
|
||||||
private getPaymentReceivedStateService: GetPaymentReceivedStateService,
|
private getPaymentReceivedStateService: GetPaymentReceivedStateService,
|
||||||
private paymentsReceivedPagesService: PaymentsReceivedPagesService,
|
private paymentsReceivedPagesService: PaymentsReceivedPagesService,
|
||||||
) {}
|
private bulkDeletePaymentReceivedService: BulkDeletePaymentReceivedService,
|
||||||
|
private validateBulkDeletePaymentReceivedService: ValidateBulkDeletePaymentReceivedService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new payment receive.
|
* Creates a new payment receive.
|
||||||
@@ -73,6 +77,29 @@ export class PaymentReceivesApplication {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple payment receives.
|
||||||
|
* @param {number[]} paymentReceiveIds
|
||||||
|
*/
|
||||||
|
public bulkDeletePaymentReceives(
|
||||||
|
paymentReceiveIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeletePaymentReceivedService.bulkDeletePaymentReceived(
|
||||||
|
paymentReceiveIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which payment receives can be deleted.
|
||||||
|
* @param {number[]} paymentReceiveIds
|
||||||
|
*/
|
||||||
|
public validateBulkDeletePaymentReceives(paymentReceiveIds: number[]) {
|
||||||
|
return this.validateBulkDeletePaymentReceivedService
|
||||||
|
.validateBulkDeletePaymentReceived(paymentReceiveIds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve payment receives paginated and filterable.
|
* Retrieve payment receives paginated and filterable.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
|
|||||||
@@ -32,15 +32,20 @@ import { PaymentReceivedResponseDto } from './dtos/PaymentReceivedResponse.dto';
|
|||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { PaymentReceivedStateResponseDto } from './dtos/PaymentReceivedStateResponse.dto';
|
import { PaymentReceivedStateResponseDto } from './dtos/PaymentReceivedStateResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('payments-received')
|
@Controller('payments-received')
|
||||||
@ApiTags('Payments Received')
|
@ApiTags('Payments Received')
|
||||||
@ApiExtraModels(PaymentReceivedResponseDto)
|
@ApiExtraModels(PaymentReceivedResponseDto)
|
||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiExtraModels(PaymentReceivedStateResponseDto)
|
@ApiExtraModels(PaymentReceivedStateResponseDto)
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class PaymentReceivesController {
|
export class PaymentReceivesController {
|
||||||
constructor(private paymentReceivesApplication: PaymentReceivesApplication) {}
|
constructor(private paymentReceivesApplication: PaymentReceivesApplication) { }
|
||||||
|
|
||||||
@Post(':id/mail')
|
@Post(':id/mail')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@@ -143,6 +148,42 @@ export class PaymentReceivesController {
|
|||||||
return this.paymentReceivesApplication.getPaymentsReceived(filterDTO);
|
return this.paymentReceivesApplication.getPaymentsReceived(filterDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which payments received can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable payments received.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public validateBulkDeletePaymentsReceived(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.paymentReceivesApplication.validateBulkDeletePaymentReceives(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple payments received.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Payments received deleted successfully.',
|
||||||
|
})
|
||||||
|
public bulkDeletePaymentsReceived(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
) {
|
||||||
|
return this.paymentReceivesApplication.bulkDeletePaymentReceives(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('state')
|
@Get('state')
|
||||||
@ApiOperation({ summary: 'Retrieves the payment received state.' })
|
@ApiOperation({ summary: 'Retrieves the payment received state.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import { PaymentsReceivedImportable } from './commands/PaymentsReceivedImportabl
|
|||||||
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
|
import { PaymentsReceivedPagesService } from './queries/PaymentsReceivedPages.service';
|
||||||
import { GetPaymentReceivedMailTemplate } from './queries/GetPaymentReceivedMailTemplate.service';
|
import { GetPaymentReceivedMailTemplate } from './queries/GetPaymentReceivedMailTemplate.service';
|
||||||
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailState.service';
|
||||||
|
import { BulkDeletePaymentReceivedService } from './BulkDeletePaymentReceived.service';
|
||||||
|
import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePaymentReceived.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PaymentReceivesController],
|
controllers: [PaymentReceivesController],
|
||||||
@@ -68,7 +70,9 @@ import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailSta
|
|||||||
PaymentsReceivedImportable,
|
PaymentsReceivedImportable,
|
||||||
PaymentsReceivedPagesService,
|
PaymentsReceivedPagesService,
|
||||||
GetPaymentReceivedMailTemplate,
|
GetPaymentReceivedMailTemplate,
|
||||||
GetPaymentReceivedMailState
|
GetPaymentReceivedMailState,
|
||||||
|
BulkDeletePaymentReceivedService,
|
||||||
|
ValidateBulkDeletePaymentReceivedService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
PaymentReceivesApplication,
|
PaymentReceivesApplication,
|
||||||
@@ -76,7 +80,7 @@ import { GetPaymentReceivedMailState } from './queries/GetPaymentReceivedMailSta
|
|||||||
PaymentReceivedGLEntries,
|
PaymentReceivedGLEntries,
|
||||||
PaymentsReceivedExportable,
|
PaymentsReceivedExportable,
|
||||||
PaymentsReceivedImportable,
|
PaymentsReceivedImportable,
|
||||||
PaymentReceivedValidators
|
PaymentReceivedValidators,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
ChromiumlyTenancyModule,
|
ChromiumlyTenancyModule,
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeletePaymentReceivedService } from './commands/DeletePaymentReceived.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeletePaymentReceivedService {
|
||||||
|
constructor(
|
||||||
|
private readonly deletePaymentReceivedService: DeletePaymentReceivedService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeletePaymentReceived(
|
||||||
|
paymentReceiveIds: 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 paymentReceiveId of paymentReceiveIds) {
|
||||||
|
try {
|
||||||
|
await this.deletePaymentReceivedService.deletePaymentReceive(
|
||||||
|
paymentReceiveId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(paymentReceiveId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(paymentReceiveId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+7
-4
@@ -30,7 +30,7 @@ export class DeletePaymentReceivedService {
|
|||||||
private paymentReceiveEntryModel: TenantModelProxy<
|
private paymentReceiveEntryModel: TenantModelProxy<
|
||||||
typeof PaymentReceivedEntry
|
typeof PaymentReceivedEntry
|
||||||
>,
|
>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given payment receive with associated entries
|
* Deletes the given payment receive with associated entries
|
||||||
@@ -43,9 +43,12 @@ export class DeletePaymentReceivedService {
|
|||||||
* - Revert the payment amount of the associated invoices.
|
* - Revert the payment amount of the associated invoices.
|
||||||
* @async
|
* @async
|
||||||
* @param {Integer} paymentReceiveId - Payment receive id.
|
* @param {Integer} paymentReceiveId - Payment receive id.
|
||||||
* @param {IPaymentReceived} paymentReceive - Payment receive object.
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
*/
|
*/
|
||||||
public async deletePaymentReceive(paymentReceiveId: number) {
|
public async deletePaymentReceive(
|
||||||
|
paymentReceiveId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
) {
|
||||||
// Retreive payment receive or throw not found service error.
|
// Retreive payment receive or throw not found service error.
|
||||||
const oldPaymentReceive = await this.paymentReceiveModel()
|
const oldPaymentReceive = await this.paymentReceiveModel()
|
||||||
.query()
|
.query()
|
||||||
@@ -79,6 +82,6 @@ export class DeletePaymentReceivedService {
|
|||||||
oldPaymentReceive,
|
oldPaymentReceive,
|
||||||
trx,
|
trx,
|
||||||
} as IPaymentReceivedDeletedPayload);
|
} as IPaymentReceivedDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteSaleEstimatesService {
|
||||||
|
constructor(private readonly deleteSaleEstimateService: DeleteSaleEstimate) { }
|
||||||
|
|
||||||
|
async bulkDeleteSaleEstimates(
|
||||||
|
saleEstimateIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const estimatesIds = uniq(castArray(saleEstimateIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(estimatesIds)
|
||||||
|
.process(async (saleEstimateId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleEstimateService.deleteEstimate(saleEstimateId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
EditSaleEstimateDto,
|
EditSaleEstimateDto,
|
||||||
} from './dtos/SaleEstimate.dto';
|
} from './dtos/SaleEstimate.dto';
|
||||||
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
|
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
|
||||||
|
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
|
||||||
|
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SaleEstimatesApplication {
|
export class SaleEstimatesApplication {
|
||||||
@@ -35,7 +37,9 @@ export class SaleEstimatesApplication {
|
|||||||
private readonly getSaleEstimateStateService: GetSaleEstimateState,
|
private readonly getSaleEstimateStateService: GetSaleEstimateState,
|
||||||
private readonly saleEstimatesPdfService: GetSaleEstimatePdf,
|
private readonly saleEstimatesPdfService: GetSaleEstimatePdf,
|
||||||
private readonly getSaleEstimateMailStateService: GetSaleEstimateMailStateService,
|
private readonly getSaleEstimateMailStateService: GetSaleEstimateMailStateService,
|
||||||
) {}
|
private readonly bulkDeleteSaleEstimatesService: BulkDeleteSaleEstimatesService,
|
||||||
|
private readonly validateBulkDeleteSaleEstimatesService: ValidateBulkDeleteSaleEstimatesService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a sale estimate.
|
* Create a sale estimate.
|
||||||
@@ -68,6 +72,31 @@ export class SaleEstimatesApplication {
|
|||||||
return this.deleteSaleEstimateService.deleteEstimate(estimateId);
|
return this.deleteSaleEstimateService.deleteEstimate(estimateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple sale estimates.
|
||||||
|
* @param {number[]} saleEstimateIds
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public bulkDeleteSaleEstimates(
|
||||||
|
saleEstimateIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteSaleEstimatesService.bulkDeleteSaleEstimates(
|
||||||
|
saleEstimateIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which sale estimates can be deleted.
|
||||||
|
* @param {number[]} saleEstimateIds
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteSaleEstimates(saleEstimateIds: number[]) {
|
||||||
|
return this.validateBulkDeleteSaleEstimatesService.validateBulkDeleteSaleEstimates(
|
||||||
|
saleEstimateIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the given sale estimate.
|
* Retrieves the given sale estimate.
|
||||||
* @param {number} estimateId - Sale estimate ID.
|
* @param {number} estimateId - Sale estimate ID.
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ import { SaleEstimateResponseDto } from './dtos/SaleEstimateResponse.dto';
|
|||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { SaleEstiamteStateResponseDto } from './dtos/SaleEstimateStateResponse.dto';
|
import { SaleEstiamteStateResponseDto } from './dtos/SaleEstimateStateResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('sale-estimates')
|
@Controller('sale-estimates')
|
||||||
@ApiTags('Sale Estimates')
|
@ApiTags('Sale Estimates')
|
||||||
@@ -43,13 +47,50 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
|||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiExtraModels(SaleEstiamteStateResponseDto)
|
@ApiExtraModels(SaleEstiamteStateResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
export class SaleEstimatesController {
|
export class SaleEstimatesController {
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which sale estimates can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable sale estimates.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
public validateBulkDeleteSaleEstimates(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.saleEstimatesApplication.validateBulkDeleteSaleEstimates(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple sale estimates.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sale estimates deleted successfully',
|
||||||
|
})
|
||||||
|
public bulkDeleteSaleEstimates(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleEstimatesApplication.bulkDeleteSaleEstimates(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {SaleEstimatesApplication} saleEstimatesApplication - Sale estimates application.
|
* @param {SaleEstimatesApplication} saleEstimatesApplication - Sale estimates application.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private readonly saleEstimatesApplication: SaleEstimatesApplication,
|
private readonly saleEstimatesApplication: SaleEstimatesApplication,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new sale estimate.' })
|
@ApiOperation({ summary: 'Create a new sale estimate.' })
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import { SaleEstimatesImportable } from './SaleEstimatesImportable';
|
|||||||
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
|
import { GetSaleEstimateMailStateService } from './queries/GetSaleEstimateMailState.service';
|
||||||
import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMailTemplate.service';
|
import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMailTemplate.service';
|
||||||
import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateAutoIncrementSubscriber';
|
import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateAutoIncrementSubscriber';
|
||||||
|
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
|
||||||
|
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -85,6 +87,8 @@ import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateA
|
|||||||
GetSaleEstimateMailStateService,
|
GetSaleEstimateMailStateService,
|
||||||
GetSaleEstimateMailTemplateService,
|
GetSaleEstimateMailTemplateService,
|
||||||
SaleEstimateAutoIncrementSubscriber,
|
SaleEstimateAutoIncrementSubscriber,
|
||||||
|
BulkDeleteSaleEstimatesService,
|
||||||
|
ValidateBulkDeleteSaleEstimatesService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SaleEstimatesExportable,
|
SaleEstimatesExportable,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteSaleEstimatesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteSaleEstimateService: DeleteSaleEstimate,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteSaleEstimates(
|
||||||
|
saleEstimateIds: 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 saleEstimateId of saleEstimateIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleEstimateService.deleteEstimate(
|
||||||
|
saleEstimateId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(saleEstimateId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(saleEstimateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,18 +24,22 @@ export class DeleteSaleEstimate {
|
|||||||
|
|
||||||
private readonly eventPublisher: EventEmitter2,
|
private readonly eventPublisher: EventEmitter2,
|
||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given estimate id with associated entries.
|
* Deletes the given estimate id with associated entries.
|
||||||
* @async
|
* @async
|
||||||
* @param {number} estimateId
|
* @param {number} estimateId - Sale estimate id.
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async deleteEstimate(estimateId: number): Promise<void> {
|
public async deleteEstimate(
|
||||||
|
estimateId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
// Retrieve sale estimate or throw not found service error.
|
// Retrieve sale estimate or throw not found service error.
|
||||||
const oldSaleEstimate = await this.saleEstimateModel()
|
const oldSaleEstimate = await this.saleEstimateModel()
|
||||||
.query()
|
.query(trx)
|
||||||
.findById(estimateId)
|
.findById(estimateId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
@@ -70,6 +74,6 @@ export class DeleteSaleEstimate {
|
|||||||
oldSaleEstimate,
|
oldSaleEstimate,
|
||||||
trx,
|
trx,
|
||||||
} as ISaleEstimateDeletedPayload);
|
} as ISaleEstimateDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteSaleInvoicesService {
|
||||||
|
constructor(private readonly deleteSaleInvoiceService: DeleteSaleInvoice) { }
|
||||||
|
|
||||||
|
async bulkDeleteSaleInvoices(
|
||||||
|
saleInvoiceIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const invoicesIds = uniq(castArray(saleInvoiceIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(invoicesIds)
|
||||||
|
.process(async (saleInvoiceId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
EditSaleInvoiceDto,
|
EditSaleInvoiceDto,
|
||||||
} from './dtos/SaleInvoice.dto';
|
} from './dtos/SaleInvoice.dto';
|
||||||
import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service';
|
import { GenerateShareLink } from './commands/GenerateInvoicePaymentLink.service';
|
||||||
|
import { BulkDeleteSaleInvoicesService } from './BulkDeleteSaleInvoices.service';
|
||||||
|
import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleInvoices.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SaleInvoiceApplication {
|
export class SaleInvoiceApplication {
|
||||||
@@ -41,7 +43,9 @@ export class SaleInvoiceApplication {
|
|||||||
private sendSaleInvoiceMailService: SendSaleInvoiceMail,
|
private sendSaleInvoiceMailService: SendSaleInvoiceMail,
|
||||||
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState,
|
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState,
|
||||||
private generateShareLinkService: GenerateShareLink,
|
private generateShareLinkService: GenerateShareLink,
|
||||||
) {}
|
private bulkDeleteSaleInvoicesService: BulkDeleteSaleInvoicesService,
|
||||||
|
private validateBulkDeleteSaleInvoicesService: ValidateBulkDeleteSaleInvoicesService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoice with associated GL entries.
|
* Creates a new sale invoice with associated GL entries.
|
||||||
@@ -78,6 +82,31 @@ export class SaleInvoiceApplication {
|
|||||||
return this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
return this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple sale invoices.
|
||||||
|
* @param {number[]} saleInvoiceIds
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public bulkDeleteSaleInvoices(
|
||||||
|
saleInvoiceIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteSaleInvoicesService.bulkDeleteSaleInvoices(
|
||||||
|
saleInvoiceIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which sale invoices can be deleted.
|
||||||
|
* @param {number[]} saleInvoiceIds
|
||||||
|
*/
|
||||||
|
public validateBulkDeleteSaleInvoices(saleInvoiceIds: number[]) {
|
||||||
|
return this.validateBulkDeleteSaleInvoicesService.validateBulkDeleteSaleInvoices(
|
||||||
|
saleInvoiceIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the given sale invoice details.
|
* Retrieves the given sale invoice details.
|
||||||
* @param {ISalesInvoicesFilter} filterDTO
|
* @param {ISalesInvoicesFilter} filterDTO
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
|||||||
import { SaleInvoiceStateResponseDto } from './dtos/SaleInvoiceState.dto';
|
import { SaleInvoiceStateResponseDto } from './dtos/SaleInvoiceState.dto';
|
||||||
import { GenerateSaleInvoiceSharableLinkResponseDto } from './dtos/GenerateSaleInvoiceSharableLinkResponse.dto';
|
import { GenerateSaleInvoiceSharableLinkResponseDto } from './dtos/GenerateSaleInvoiceSharableLinkResponse.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('sale-invoices')
|
@Controller('sale-invoices')
|
||||||
@ApiTags('Sale Invoices')
|
@ApiTags('Sale Invoices')
|
||||||
@@ -47,9 +51,46 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
|||||||
@ApiExtraModels(SaleInvoiceStateResponseDto)
|
@ApiExtraModels(SaleInvoiceStateResponseDto)
|
||||||
@ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto)
|
@ApiExtraModels(GenerateSaleInvoiceSharableLinkResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
export class SaleInvoicesController {
|
export class SaleInvoicesController {
|
||||||
constructor(private saleInvoiceApplication: SaleInvoiceApplication) { }
|
constructor(private saleInvoiceApplication: SaleInvoiceApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which sale invoices can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable sale invoices.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
validateBulkDeleteSaleInvoices(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.saleInvoiceApplication.validateBulkDeleteSaleInvoices(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple sale invoices.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sale invoices deleted successfully',
|
||||||
|
})
|
||||||
|
bulkDeleteSaleInvoices(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleInvoiceApplication.bulkDeleteSaleInvoices(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new sale invoice.' })
|
@ApiOperation({ summary: 'Create a new sale invoice.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ import { SaleInvoicesCost } from './SalesInvoicesCost';
|
|||||||
import { SaleInvoicesExportable } from './commands/SaleInvoicesExportable';
|
import { SaleInvoicesExportable } from './commands/SaleInvoicesExportable';
|
||||||
import { SaleInvoicesImportable } from './commands/SaleInvoicesImportable';
|
import { SaleInvoicesImportable } from './commands/SaleInvoicesImportable';
|
||||||
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
||||||
|
import { BulkDeleteSaleInvoicesService } from './BulkDeleteSaleInvoices.service';
|
||||||
|
import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleInvoices.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -126,6 +128,8 @@ import { PaymentLinksModule } from '../PaymentLinks/PaymentLinks.module';
|
|||||||
SaleInvoicesCost,
|
SaleInvoicesCost,
|
||||||
SaleInvoicesExportable,
|
SaleInvoicesExportable,
|
||||||
SaleInvoicesImportable,
|
SaleInvoicesImportable,
|
||||||
|
BulkDeleteSaleInvoicesService,
|
||||||
|
ValidateBulkDeleteSaleInvoicesService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
GetSaleInvoice,
|
GetSaleInvoice,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteSaleInvoice } from './commands/DeleteSaleInvoice.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteSaleInvoicesService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteSaleInvoiceService: DeleteSaleInvoice,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteSaleInvoices(saleInvoiceIds: 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 saleInvoiceId of saleInvoiceIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleInvoiceService.deleteSaleInvoice(
|
||||||
|
saleInvoiceId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(saleInvoiceId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(saleInvoiceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export class DeleteSaleInvoice {
|
|||||||
|
|
||||||
@Inject(ItemEntry.name)
|
@Inject(ItemEntry.name)
|
||||||
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the sale invoice has no payment entries.
|
* Validate the sale invoice has no payment entries.
|
||||||
@@ -86,9 +86,12 @@ export class DeleteSaleInvoice {
|
|||||||
* Deletes the given sale invoice with associated entries
|
* Deletes the given sale invoice with associated entries
|
||||||
* and journal transactions.
|
* and journal transactions.
|
||||||
* @param {Number} saleInvoiceId - The given sale invoice id.
|
* @param {Number} saleInvoiceId - The given sale invoice id.
|
||||||
* @param {ISystemUser} authorizedUser -
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
*/
|
*/
|
||||||
public async deleteSaleInvoice(saleInvoiceId: number): Promise<void> {
|
public async deleteSaleInvoice(
|
||||||
|
saleInvoiceId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
// Retrieve the given sale invoice with associated entries
|
// Retrieve the given sale invoice with associated entries
|
||||||
// or throw not found error.
|
// or throw not found error.
|
||||||
const oldSaleInvoice = await this.saleInvoiceModel()
|
const oldSaleInvoice = await this.saleInvoiceModel()
|
||||||
@@ -138,6 +141,6 @@ export class DeleteSaleInvoice {
|
|||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
trx,
|
trx,
|
||||||
} as ISaleInvoiceDeletedPayload);
|
} as ISaleInvoiceDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteSaleReceiptsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteSaleReceiptService: DeleteSaleReceipt,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeleteSaleReceipts(
|
||||||
|
saleReceiptIds: number | number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const receiptIds = uniq(castArray(saleReceiptIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(receiptIds)
|
||||||
|
.process(async (saleReceiptId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleReceiptService.deleteSaleReceipt(saleReceiptId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
EditSaleReceiptDto,
|
EditSaleReceiptDto,
|
||||||
} from './dtos/SaleReceipt.dto';
|
} from './dtos/SaleReceipt.dto';
|
||||||
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
|
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
|
||||||
|
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
|
||||||
|
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SaleReceiptApplication {
|
export class SaleReceiptApplication {
|
||||||
@@ -36,7 +38,9 @@ export class SaleReceiptApplication {
|
|||||||
private getSaleReceiptStateService: GetSaleReceiptState,
|
private getSaleReceiptStateService: GetSaleReceiptState,
|
||||||
private saleReceiptNotifyByMailService: SaleReceiptMailNotification,
|
private saleReceiptNotifyByMailService: SaleReceiptMailNotification,
|
||||||
private getSaleReceiptMailStateService: GetSaleReceiptMailStateService,
|
private getSaleReceiptMailStateService: GetSaleReceiptMailStateService,
|
||||||
) {}
|
private bulkDeleteSaleReceiptsService: BulkDeleteSaleReceiptsService,
|
||||||
|
private validateBulkDeleteSaleReceiptsService: ValidateBulkDeleteSaleReceiptsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale receipt with associated entries.
|
* Creates a new sale receipt with associated entries.
|
||||||
@@ -85,6 +89,30 @@ export class SaleReceiptApplication {
|
|||||||
return this.deleteSaleReceiptService.deleteSaleReceipt(saleReceiptId);
|
return this.deleteSaleReceiptService.deleteSaleReceipt(saleReceiptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple sale receipts.
|
||||||
|
* @param {number[]} saleReceiptIds
|
||||||
|
*/
|
||||||
|
public async bulkDeleteSaleReceipts(
|
||||||
|
saleReceiptIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteSaleReceiptsService.bulkDeleteSaleReceipts(
|
||||||
|
saleReceiptIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates which sale receipts can be deleted.
|
||||||
|
* @param {number[]} saleReceiptIds
|
||||||
|
*/
|
||||||
|
public async validateBulkDeleteSaleReceipts(saleReceiptIds: number[]) {
|
||||||
|
return this.validateBulkDeleteSaleReceiptsService.validateBulkDeleteSaleReceipts(
|
||||||
|
saleReceiptIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sales receipts paginated and filterable list.
|
* Retrieve sales receipts paginated and filterable list.
|
||||||
* @param {ISalesReceiptsFilter} filterDTO
|
* @param {ISalesReceiptsFilter} filterDTO
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ import { SaleReceiptResponseDto } from './dtos/SaleReceiptResponse.dto';
|
|||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { SaleReceiptStateResponseDto } from './dtos/SaleReceiptState.dto';
|
import { SaleReceiptStateResponseDto } from './dtos/SaleReceiptState.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('sale-receipts')
|
@Controller('sale-receipts')
|
||||||
@ApiTags('Sale Receipts')
|
@ApiTags('Sale Receipts')
|
||||||
@@ -39,8 +43,45 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
|||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiExtraModels(SaleReceiptStateResponseDto)
|
@ApiExtraModels(SaleReceiptStateResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
export class SaleReceiptsController {
|
export class SaleReceiptsController {
|
||||||
constructor(private saleReceiptApplication: SaleReceiptApplication) {}
|
constructor(private saleReceiptApplication: SaleReceiptApplication) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which sale receipts can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable sale receipts.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
validateBulkDeleteSaleReceipts(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.saleReceiptApplication.validateBulkDeleteSaleReceipts(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple sale receipts.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sale receipts deleted successfully',
|
||||||
|
})
|
||||||
|
bulkDeleteSaleReceipts(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleReceiptApplication.bulkDeleteSaleReceipts(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new sale receipt.' })
|
@ApiOperation({ summary: 'Create a new sale receipt.' })
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
|
|||||||
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
|
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
|
||||||
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
|
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
|
||||||
import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber';
|
import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber';
|
||||||
|
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
|
||||||
|
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [SaleReceiptsController],
|
controllers: [SaleReceiptsController],
|
||||||
@@ -85,6 +87,8 @@ import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAut
|
|||||||
GetSaleReceiptMailStateService,
|
GetSaleReceiptMailStateService,
|
||||||
GetSaleReceiptMailTemplateService,
|
GetSaleReceiptMailTemplateService,
|
||||||
SaleReceiptAutoIncrementSubscriber,
|
SaleReceiptAutoIncrementSubscriber,
|
||||||
|
BulkDeleteSaleReceiptsService,
|
||||||
|
ValidateBulkDeleteSaleReceiptsService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SaleReceiptsModule { }
|
export class SaleReceiptsModule { }
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteSaleReceipt } from './commands/DeleteSaleReceipt.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteSaleReceiptsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteSaleReceiptService: DeleteSaleReceipt,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async validateBulkDeleteSaleReceipts(
|
||||||
|
saleReceiptIds: 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 saleReceiptId of saleReceiptIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteSaleReceiptService.deleteSaleReceipt(
|
||||||
|
saleReceiptId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(saleReceiptId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(saleReceiptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -29,9 +29,13 @@ export class DeleteSaleReceipt {
|
|||||||
/**
|
/**
|
||||||
* Deletes the sale receipt with associated entries.
|
* Deletes the sale receipt with associated entries.
|
||||||
* @param {Integer} saleReceiptId - Sale receipt identifier.
|
* @param {Integer} saleReceiptId - Sale receipt identifier.
|
||||||
|
* @param {Knex.Transaction} trx - Database transaction instance.
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
public async deleteSaleReceipt(saleReceiptId: number) {
|
public async deleteSaleReceipt(
|
||||||
|
saleReceiptId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
) {
|
||||||
const oldSaleReceipt = await this.saleReceiptModel()
|
const oldSaleReceipt = await this.saleReceiptModel()
|
||||||
.query()
|
.query()
|
||||||
.findById(saleReceiptId)
|
.findById(saleReceiptId)
|
||||||
@@ -65,6 +69,6 @@ export class DeleteSaleReceipt {
|
|||||||
oldSaleReceipt,
|
oldSaleReceipt,
|
||||||
trx,
|
trx,
|
||||||
} as ISaleReceiptEventDeletedPayload);
|
} as ISaleReceiptEventDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
|
import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BulkDeleteVendorCreditsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteVendorCreditService: DeleteVendorCreditService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async bulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds: number | Array<number>,
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
const { skipUndeletable = false } = options ?? {};
|
||||||
|
const creditsIds = uniq(castArray(vendorCreditIds));
|
||||||
|
|
||||||
|
const results = await PromisePool.withConcurrency(1)
|
||||||
|
.for(creditsIds)
|
||||||
|
.process(async (vendorCreditId: number) => {
|
||||||
|
try {
|
||||||
|
await this.deleteVendorCreditService.deleteVendorCredit(
|
||||||
|
vendorCreditId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!skipUndeletable) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipUndeletable && results.errors && results.errors.length > 0) {
|
||||||
|
throw results.errors[0].raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValidateBulkDeleteVendorCreditsService {
|
||||||
|
constructor(
|
||||||
|
private readonly deleteVendorCreditService: DeleteVendorCreditService,
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKnex: () => Knex,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async validateBulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds: 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 vendorCreditId of vendorCreditIds) {
|
||||||
|
try {
|
||||||
|
await this.deleteVendorCreditService.deleteVendorCredit(
|
||||||
|
vendorCreditId,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
deletableIds.push(vendorCreditId);
|
||||||
|
} catch (error) {
|
||||||
|
nonDeletableIds.push(vendorCreditId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.rollback();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletableCount: deletableIds.length,
|
||||||
|
nonDeletableCount: nonDeletableIds.length,
|
||||||
|
deletableIds,
|
||||||
|
nonDeletableIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,20 +10,67 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { VendorCreditsApplicationService } from './VendorCreditsApplication.service';
|
import { VendorCreditsApplicationService } from './VendorCreditsApplication.service';
|
||||||
import { IVendorCreditsQueryDTO } from './types/VendorCredit.types';
|
import { IVendorCreditsQueryDTO } from './types/VendorCredit.types';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import {
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
getSchemaPath,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
CreateVendorCreditDto,
|
CreateVendorCreditDto,
|
||||||
EditVendorCreditDto,
|
EditVendorCreditDto,
|
||||||
} from './dtos/VendorCredit.dto';
|
} from './dtos/VendorCredit.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteDto,
|
||||||
|
ValidateBulkDeleteResponseDto,
|
||||||
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Controller('vendor-credits')
|
@Controller('vendor-credits')
|
||||||
@ApiTags('Vendor Credits')
|
@ApiTags('Vendor Credits')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
export class VendorCreditsController {
|
export class VendorCreditsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly vendorCreditsApplication: VendorCreditsApplicationService,
|
private readonly vendorCreditsApplication: VendorCreditsApplicationService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
|
@Post('validate-bulk-delete')
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
'Validates which vendor credits can be deleted and returns the results.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description:
|
||||||
|
'Validation completed with counts and IDs of deletable and non-deletable vendor credits.',
|
||||||
|
schema: {
|
||||||
|
$ref: getSchemaPath(ValidateBulkDeleteResponseDto),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async validateBulkDeleteVendorCredits(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.vendorCreditsApplication.validateBulkDeleteVendorCredits(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-delete')
|
||||||
|
@ApiOperation({ summary: 'Deletes multiple vendor credits.' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Vendor credits deleted successfully',
|
||||||
|
})
|
||||||
|
async bulkDeleteVendorCredits(
|
||||||
|
@Body() bulkDeleteDto: BulkDeleteDto,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.vendorCreditsApplication.bulkDeleteVendorCredits(
|
||||||
|
bulkDeleteDto.ids,
|
||||||
|
{ skipUndeletable: bulkDeleteDto.skipUndeletable ?? false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new vendor credit.' })
|
@ApiOperation({ summary: 'Create a new vendor credit.' })
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
|||||||
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
|
||||||
import { VendorCreditsExportable } from './commands/VendorCreditsExportable';
|
import { VendorCreditsExportable } from './commands/VendorCreditsExportable';
|
||||||
import { VendorCreditsImportable } from './commands/VendorCreditsImportable';
|
import { VendorCreditsImportable } from './commands/VendorCreditsImportable';
|
||||||
|
import { BulkDeleteVendorCreditsService } from './BulkDeleteVendorCredits.service';
|
||||||
|
import { ValidateBulkDeleteVendorCreditsService } from './ValidateBulkDeleteVendorCredits.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -61,6 +63,8 @@ import { VendorCreditsImportable } from './commands/VendorCreditsImportable';
|
|||||||
VendorCreditAutoSerialSubscriber,
|
VendorCreditAutoSerialSubscriber,
|
||||||
VendorCreditsExportable,
|
VendorCreditsExportable,
|
||||||
VendorCreditsImportable,
|
VendorCreditsImportable,
|
||||||
|
BulkDeleteVendorCreditsService,
|
||||||
|
ValidateBulkDeleteVendorCreditsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CreateVendorCreditService,
|
CreateVendorCreditService,
|
||||||
@@ -74,6 +78,8 @@ import { VendorCreditsImportable } from './commands/VendorCreditsImportable';
|
|||||||
OpenVendorCreditService,
|
OpenVendorCreditService,
|
||||||
VendorCreditsExportable,
|
VendorCreditsExportable,
|
||||||
VendorCreditsImportable,
|
VendorCreditsImportable,
|
||||||
|
BulkDeleteVendorCreditsService,
|
||||||
|
ValidateBulkDeleteVendorCreditsService,
|
||||||
],
|
],
|
||||||
controllers: [VendorCreditsController],
|
controllers: [VendorCreditsController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import { CreateVendorCreditService } from './commands/CreateVendorCredit.service
|
|||||||
import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service';
|
import { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service';
|
||||||
import { EditVendorCreditService } from './commands/EditVendorCredit.service';
|
import { EditVendorCreditService } from './commands/EditVendorCredit.service';
|
||||||
import { GetVendorCreditService } from './queries/GetVendorCredit.service';
|
import { GetVendorCreditService } from './queries/GetVendorCredit.service';
|
||||||
import { IVendorCreditEditDTO, IVendorCreditsQueryDTO } from './types/VendorCredit.types';
|
import {
|
||||||
|
IVendorCreditEditDTO,
|
||||||
|
IVendorCreditsQueryDTO,
|
||||||
|
} from './types/VendorCredit.types';
|
||||||
import { IVendorCreditCreateDTO } from './types/VendorCredit.types';
|
import { IVendorCreditCreateDTO } from './types/VendorCredit.types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OpenVendorCreditService } from './commands/OpenVendorCredit.service';
|
import { OpenVendorCreditService } from './commands/OpenVendorCredit.service';
|
||||||
import { GetVendorCreditsService } from './queries/GetVendorCredits.service';
|
import { GetVendorCreditsService } from './queries/GetVendorCredits.service';
|
||||||
import { CreateVendorCreditDto, EditVendorCreditDto } from './dtos/VendorCredit.dto';
|
import {
|
||||||
|
CreateVendorCreditDto,
|
||||||
|
EditVendorCreditDto,
|
||||||
|
} from './dtos/VendorCredit.dto';
|
||||||
|
import { BulkDeleteVendorCreditsService } from './BulkDeleteVendorCredits.service';
|
||||||
|
import { ValidateBulkDeleteVendorCreditsService } from './ValidateBulkDeleteVendorCredits.service';
|
||||||
|
import { ValidateBulkDeleteResponseDto } from '@/common/dtos/BulkDelete.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VendorCreditsApplicationService {
|
export class VendorCreditsApplicationService {
|
||||||
@@ -25,7 +34,9 @@ export class VendorCreditsApplicationService {
|
|||||||
private readonly getVendorCreditService: GetVendorCreditService,
|
private readonly getVendorCreditService: GetVendorCreditService,
|
||||||
private readonly openVendorCreditService: OpenVendorCreditService,
|
private readonly openVendorCreditService: OpenVendorCreditService,
|
||||||
private readonly getVendorCreditsService: GetVendorCreditsService,
|
private readonly getVendorCreditsService: GetVendorCreditsService,
|
||||||
) {}
|
private readonly bulkDeleteVendorCreditsService: BulkDeleteVendorCreditsService,
|
||||||
|
private readonly validateBulkDeleteVendorCreditsService: ValidateBulkDeleteVendorCreditsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new vendor credit.
|
* Creates a new vendor credit.
|
||||||
@@ -90,4 +101,22 @@ export class VendorCreditsApplicationService {
|
|||||||
getVendorCredits(query: IVendorCreditsQueryDTO) {
|
getVendorCredits(query: IVendorCreditsQueryDTO) {
|
||||||
return this.getVendorCreditsService.getVendorCredits(query);
|
return this.getVendorCreditsService.getVendorCredits(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds: number[],
|
||||||
|
options?: { skipUndeletable?: boolean },
|
||||||
|
) {
|
||||||
|
return this.bulkDeleteVendorCreditsService.bulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateBulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds: number[],
|
||||||
|
): Promise<ValidateBulkDeleteResponseDto> {
|
||||||
|
return this.validateBulkDeleteVendorCreditsService.validateBulkDeleteVendorCredits(
|
||||||
|
vendorCreditIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class DeleteVendorCreditService {
|
|||||||
oldVendorCredit,
|
oldVendorCredit,
|
||||||
trx,
|
trx,
|
||||||
} as IVendorCreditDeletedPayload);
|
} as IVendorCreditDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,11 +13,20 @@ import {
|
|||||||
IVendorOpeningBalanceEditDTO,
|
IVendorOpeningBalanceEditDTO,
|
||||||
IVendorsFilter,
|
IVendorsFilter,
|
||||||
} from './types/Vendors.types';
|
} 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 { CreateVendorDto } from './dtos/CreateVendor.dto';
|
||||||
import { EditVendorDto } from './dtos/EditVendor.dto';
|
import { EditVendorDto } from './dtos/EditVendor.dto';
|
||||||
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
|
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import {
|
||||||
|
BulkDeleteVendorsDto,
|
||||||
|
ValidateBulkDeleteVendorsResponseDto,
|
||||||
|
} from './dtos/BulkDeleteVendors.dto';
|
||||||
|
|
||||||
@Controller('vendors')
|
@Controller('vendors')
|
||||||
@ApiTags('Vendors')
|
@ApiTags('Vendors')
|
||||||
@@ -66,4 +75,37 @@ export class VendorsController {
|
|||||||
openingBalanceDTO,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { GetVendorsService } from './queries/GetVendors.service';
|
|||||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||||
import { VendorsExportable } from './VendorsExportable';
|
import { VendorsExportable } from './VendorsExportable';
|
||||||
import { VendorsImportable } from './VendorsImportable';
|
import { VendorsImportable } from './VendorsImportable';
|
||||||
|
import { BulkDeleteVendorsService } from './BulkDeleteVendors.service';
|
||||||
|
import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TenancyDatabaseModule, DynamicListModule],
|
imports: [TenancyDatabaseModule, DynamicListModule],
|
||||||
@@ -31,6 +33,8 @@ import { VendorsImportable } from './VendorsImportable';
|
|||||||
VendorValidators,
|
VendorValidators,
|
||||||
DeleteVendorService,
|
DeleteVendorService,
|
||||||
VendorsApplication,
|
VendorsApplication,
|
||||||
|
BulkDeleteVendorsService,
|
||||||
|
ValidateBulkDeleteVendorsService,
|
||||||
TransformerInjectable,
|
TransformerInjectable,
|
||||||
TenancyContext,
|
TenancyContext,
|
||||||
VendorsExportable,
|
VendorsExportable,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { GetVendorsService } from './queries/GetVendors.service';
|
|||||||
import { CreateVendorDto } from './dtos/CreateVendor.dto';
|
import { CreateVendorDto } from './dtos/CreateVendor.dto';
|
||||||
import { EditVendorDto } from './dtos/EditVendor.dto';
|
import { EditVendorDto } from './dtos/EditVendor.dto';
|
||||||
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
|
import { GetVendorsQueryDto } from './dtos/GetVendorsQuery.dto';
|
||||||
|
import { BulkDeleteVendorsService } from './BulkDeleteVendors.service';
|
||||||
|
import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VendorsApplication {
|
export class VendorsApplication {
|
||||||
@@ -23,6 +25,8 @@ export class VendorsApplication {
|
|||||||
private editOpeningBalanceService: EditOpeningBalanceVendorService,
|
private editOpeningBalanceService: EditOpeningBalanceVendorService,
|
||||||
private getVendorService: GetVendorService,
|
private getVendorService: GetVendorService,
|
||||||
private getVendorsService: GetVendorsService,
|
private getVendorsService: GetVendorsService,
|
||||||
|
private readonly bulkDeleteVendorsService: BulkDeleteVendorsService,
|
||||||
|
private readonly validateBulkDeleteVendorsService: ValidateBulkDeleteVendorsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,4 +90,20 @@ export class VendorsApplication {
|
|||||||
public getVendors(filterDTO: GetVendorsQueryDto) {
|
public getVendors(filterDTO: GetVendorsQueryDto) {
|
||||||
return this.getVendorsService.getVendorsList(filterDTO);
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,10 @@ export class DeleteVendorService {
|
|||||||
* @param {number} vendorId
|
* @param {number} vendorId
|
||||||
* @return {Promise<void>}
|
* @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.
|
// Retrieves the old vendor or throw not found service error.
|
||||||
const oldVendor = await this.vendorModel()
|
const query = this.vendorModel().query(trx);
|
||||||
.query()
|
const oldVendor = await query.findById(vendorId).throwIfNotFound();
|
||||||
.findById(vendorId)
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
// Triggers `onVendorDeleting` event.
|
// Triggers `onVendorDeleting` event.
|
||||||
await this.eventPublisher.emitAsync(events.vendors.onDeleting, {
|
await this.eventPublisher.emitAsync(events.vendors.onDeleting, {
|
||||||
@@ -43,10 +41,10 @@ export class DeleteVendorService {
|
|||||||
} as IVendorEventDeletingPayload);
|
} as IVendorEventDeletingPayload);
|
||||||
|
|
||||||
// Deletes vendor contact under unit-of-work.
|
// 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.
|
// Deletes the vendor contact from the storage.
|
||||||
await this.vendorModel()
|
await this.vendorModel()
|
||||||
.query(trx)
|
.query(transaction)
|
||||||
.findById(vendorId)
|
.findById(vendorId)
|
||||||
.deleteIfNoRelations({
|
.deleteIfNoRelations({
|
||||||
type: ERRORS.VENDOR_HAS_TRANSACTIONS,
|
type: ERRORS.VENDOR_HAS_TRANSACTIONS,
|
||||||
@@ -56,8 +54,8 @@ export class DeleteVendorService {
|
|||||||
await this.eventPublisher.emitAsync(events.vendors.onDeleted, {
|
await this.eventPublisher.emitAsync(events.vendors.onDeleted, {
|
||||||
vendorId,
|
vendorId,
|
||||||
oldVendor,
|
oldVendor,
|
||||||
trx,
|
trx: transaction,
|
||||||
} as IVendorEventDeletedPayload);
|
} as IVendorEventDeletedPayload);
|
||||||
});
|
}, trx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -50,6 +50,19 @@ import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransa
|
|||||||
import { SharePaymentLinkDialog } from '@/containers/PaymentLink/dialogs/SharePaymentLinkDialog/SharePaymentLinkDialog';
|
import { SharePaymentLinkDialog } from '@/containers/PaymentLink/dialogs/SharePaymentLinkDialog/SharePaymentLinkDialog';
|
||||||
import { SelectPaymentMethodsDialog } from '@/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsDialog';
|
import { SelectPaymentMethodsDialog } from '@/containers/PaymentLink/dialogs/SelectPaymentMethodsDialog/SelectPaymentMethodsDialog';
|
||||||
import ApiKeysGenerateDialog from '@/containers/Dialogs/ApiKeysGenerateDialog';
|
import ApiKeysGenerateDialog from '@/containers/Dialogs/ApiKeysGenerateDialog';
|
||||||
|
import InvoiceBulkDeleteDialog from '@/containers/Dialogs/Invoices/InvoiceBulkDeleteDialog';
|
||||||
|
import EstimateBulkDeleteDialog from '@/containers/Dialogs/Estimates/EstimateBulkDeleteDialog';
|
||||||
|
import ReceiptBulkDeleteDialog from '@/containers/Dialogs/Receipts/ReceiptBulkDeleteDialog';
|
||||||
|
import CreditNoteBulkDeleteDialog from '@/containers/Dialogs/CreditNotes/CreditNoteBulkDeleteDialog';
|
||||||
|
import PaymentReceivedBulkDeleteDialog from '@/containers/Dialogs/PaymentsReceived/PaymentReceivedBulkDeleteDialog';
|
||||||
|
import BillBulkDeleteDialog from '@/containers/Dialogs/Bills/BillBulkDeleteDialog';
|
||||||
|
import VendorCreditBulkDeleteDialog from '@/containers/Dialogs/VendorCredits/VendorCreditBulkDeleteDialog';
|
||||||
|
import ManualJournalBulkDeleteDialog from '@/containers/Dialogs/ManualJournals/ManualJournalBulkDeleteDialog';
|
||||||
|
import ExpenseBulkDeleteDialog from '@/containers/Dialogs/Expenses/ExpenseBulkDeleteDialog';
|
||||||
|
import AccountBulkDeleteDialog from '@/containers/Dialogs/Accounts/AccountBulkDeleteDialog';
|
||||||
|
import ItemBulkDeleteDialog from '@/containers/Dialogs/Items/ItemBulkDeleteDialog';
|
||||||
|
import CustomerBulkDeleteDialog from '@/containers/Dialogs/Customers/CustomerBulkDeleteDialog';
|
||||||
|
import VendorBulkDeleteDialog from '@/containers/Dialogs/Vendors/VendorBulkDeleteDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialogs container.
|
* Dialogs container.
|
||||||
@@ -139,6 +152,27 @@ export default function DialogsContainer() {
|
|||||||
<InvoiceExchangeRateChangeDialog
|
<InvoiceExchangeRateChangeDialog
|
||||||
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
||||||
/>
|
/>
|
||||||
|
<InvoiceBulkDeleteDialog dialogName={DialogsName.InvoiceBulkDelete} />
|
||||||
|
<EstimateBulkDeleteDialog dialogName={DialogsName.EstimateBulkDelete} />
|
||||||
|
<ReceiptBulkDeleteDialog dialogName={DialogsName.ReceiptBulkDelete} />
|
||||||
|
<CreditNoteBulkDeleteDialog
|
||||||
|
dialogName={DialogsName.CreditNoteBulkDelete}
|
||||||
|
/>
|
||||||
|
<PaymentReceivedBulkDeleteDialog
|
||||||
|
dialogName={DialogsName.PaymentReceivedBulkDelete}
|
||||||
|
/>
|
||||||
|
<BillBulkDeleteDialog dialogName={DialogsName.BillBulkDelete} />
|
||||||
|
<VendorCreditBulkDeleteDialog
|
||||||
|
dialogName={DialogsName.VendorCreditBulkDelete}
|
||||||
|
/>
|
||||||
|
<ManualJournalBulkDeleteDialog
|
||||||
|
dialogName={DialogsName.ManualJournalBulkDelete}
|
||||||
|
/>
|
||||||
|
<ExpenseBulkDeleteDialog dialogName={DialogsName.ExpenseBulkDelete} />
|
||||||
|
<AccountBulkDeleteDialog dialogName={DialogsName.AccountBulkDelete} />
|
||||||
|
<ItemBulkDeleteDialog dialogName={DialogsName.ItemBulkDelete} />
|
||||||
|
<CustomerBulkDeleteDialog dialogName={DialogsName.CustomerBulkDelete} />
|
||||||
|
<VendorBulkDeleteDialog dialogName={DialogsName.VendorBulkDelete} />
|
||||||
<ExportDialog dialogName={DialogsName.Export} />
|
<ExportDialog dialogName={DialogsName.Export} />
|
||||||
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
|
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
|
||||||
<DisconnectBankAccountDialog
|
<DisconnectBankAccountDialog
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ export enum DialogsName {
|
|||||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||||
TaxRateForm = 'tax-rate-form',
|
TaxRateForm = 'tax-rate-form',
|
||||||
InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice',
|
InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice',
|
||||||
|
InvoiceBulkDelete = 'invoices-bulk-delete',
|
||||||
|
EstimateBulkDelete = 'estimates-bulk-delete',
|
||||||
|
ReceiptBulkDelete = 'receipts-bulk-delete',
|
||||||
|
CreditNoteBulkDelete = 'credit-notes-bulk-delete',
|
||||||
|
PaymentReceivedBulkDelete = 'payments-received-bulk-delete',
|
||||||
|
BillBulkDelete = 'bills-bulk-delete',
|
||||||
|
VendorCreditBulkDelete = 'vendor-credits-bulk-delete',
|
||||||
|
ManualJournalBulkDelete = 'manual-journals-bulk-delete',
|
||||||
|
ExpenseBulkDelete = 'expenses-bulk-delete',
|
||||||
|
AccountBulkDelete = 'accounts-bulk-delete',
|
||||||
|
ItemBulkDelete = 'items-bulk-delete',
|
||||||
|
CustomerBulkDelete = 'customers-bulk-delete',
|
||||||
|
VendorBulkDelete = 'vendors-bulk-delete',
|
||||||
InvoiceMail = 'invoice-mail',
|
InvoiceMail = 'invoice-mail',
|
||||||
EstimateMail = 'estimate-mail',
|
EstimateMail = 'estimate-mail',
|
||||||
ReceiptMail = 'receipt-mail',
|
ReceiptMail = 'receipt-mail',
|
||||||
|
|||||||
+30
-3
@@ -8,6 +8,7 @@ import {
|
|||||||
Intent,
|
Intent,
|
||||||
Alignment,
|
Alignment,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
@@ -33,6 +34,7 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|||||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import { useBulkDeleteManualJournalsDialog } from './hooks/use-bulk-delete-manual-journals-dialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manual journal actions bar.
|
* Manual journal actions bar.
|
||||||
@@ -43,6 +45,7 @@ function ManualJournalActionsBar({
|
|||||||
|
|
||||||
// #withManualJournals
|
// #withManualJournals
|
||||||
manualJournalsFilterConditions,
|
manualJournalsFilterConditions,
|
||||||
|
manualJournalsSelectedRows = [],
|
||||||
|
|
||||||
// #withSettings
|
// #withSettings
|
||||||
manualJournalsTableSize,
|
manualJournalsTableSize,
|
||||||
@@ -69,8 +72,14 @@ function ManualJournalActionsBar({
|
|||||||
const onClickNewManualJournal = () => {
|
const onClickNewManualJournal = () => {
|
||||||
history.push('/make-journal-entry');
|
history.push('/make-journal-entry');
|
||||||
};
|
};
|
||||||
// Handle delete button click.
|
const {
|
||||||
const handleBulkDelete = () => {};
|
openBulkDeleteDialog,
|
||||||
|
isValidatingBulkDeleteManualJournals,
|
||||||
|
} = useBulkDeleteManualJournalsDialog();
|
||||||
|
|
||||||
|
const handleBulkDelete = () => {
|
||||||
|
openBulkDeleteDialog(manualJournalsSelectedRows);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle tab change.
|
// Handle tab change.
|
||||||
const handleTabChange = (view) => {
|
const handleTabChange = (view) => {
|
||||||
@@ -100,6 +109,23 @@ function ManualJournalActionsBar({
|
|||||||
downloadExportPdf({ resource: 'ManualJournal' });
|
downloadExportPdf({ resource: 'ManualJournal' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isEmpty(manualJournalsSelectedRows)) {
|
||||||
|
return (
|
||||||
|
<DashboardActionsBar>
|
||||||
|
<NavbarGroup>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
text={<T id={'delete'} />}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
disabled={isValidatingBulkDeleteManualJournals}
|
||||||
|
/>
|
||||||
|
</NavbarGroup>
|
||||||
|
</DashboardActionsBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -184,8 +210,9 @@ export default compose(
|
|||||||
withDialogActions,
|
withDialogActions,
|
||||||
withManualJournalsActions,
|
withManualJournalsActions,
|
||||||
withSettingsActions,
|
withSettingsActions,
|
||||||
withManualJournals(({ manualJournalsTableState }) => ({
|
withManualJournals(({ manualJournalsTableState, manualJournalsSelectedRows }) => ({
|
||||||
manualJournalsFilterConditions: manualJournalsTableState.filterRoles,
|
manualJournalsFilterConditions: manualJournalsTableState.filterRoles,
|
||||||
|
manualJournalsSelectedRows,
|
||||||
})),
|
})),
|
||||||
withSettings(({ manualJournalsSettings }) => ({
|
withSettings(({ manualJournalsSettings }) => ({
|
||||||
manualJournalsTableSize: manualJournalsSettings?.tableSize,
|
manualJournalsTableSize: manualJournalsSettings?.tableSize,
|
||||||
|
|||||||
+8
-5
@@ -33,6 +33,7 @@ import { DRAWERS } from '@/constants/drawers';
|
|||||||
function ManualJournalsDataTable({
|
function ManualJournalsDataTable({
|
||||||
// #withManualJournalsActions
|
// #withManualJournalsActions
|
||||||
setManualJournalsTableState,
|
setManualJournalsTableState,
|
||||||
|
setManualJournalsSelectedRows,
|
||||||
|
|
||||||
// #withAlertsActions
|
// #withAlertsActions
|
||||||
openAlert,
|
openAlert,
|
||||||
@@ -67,31 +68,26 @@ function ManualJournalsDataTable({
|
|||||||
const handlePublishJournal = ({ id }) => {
|
const handlePublishJournal = ({ id }) => {
|
||||||
openAlert('journal-publish', { manualJournalId: id });
|
openAlert('journal-publish', { manualJournalId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle the journal edit action.
|
// Handle the journal edit action.
|
||||||
const handleEditJournal = ({ id }) => {
|
const handleEditJournal = ({ id }) => {
|
||||||
history.push(`/manual-journals/${id}/edit`);
|
history.push(`/manual-journals/${id}/edit`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle the journal delete action.
|
// Handle the journal delete action.
|
||||||
const handleDeleteJournal = ({ id }) => {
|
const handleDeleteJournal = ({ id }) => {
|
||||||
openAlert('journal-delete', { manualJournalId: id });
|
openAlert('journal-delete', { manualJournalId: id });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle view detail journal.
|
// Handle view detail journal.
|
||||||
const handleViewDetailJournal = ({ id }) => {
|
const handleViewDetailJournal = ({ id }) => {
|
||||||
openDrawer(DRAWERS.JOURNAL_DETAILS, {
|
openDrawer(DRAWERS.JOURNAL_DETAILS, {
|
||||||
manualJournalId: id,
|
manualJournalId: id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle cell click.
|
// Handle cell click.
|
||||||
const handleCellClick = (cell, event) => {
|
const handleCellClick = (cell, event) => {
|
||||||
openDrawer(DRAWERS.JOURNAL_DETAILS, {
|
openDrawer(DRAWERS.JOURNAL_DETAILS, {
|
||||||
manualJournalId: cell.row.original.id,
|
manualJournalId: cell.row.original.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
// Local storage memorizing columns widths.
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
useMemorizedColumnsWidths(TABLES.MANUAL_JOURNALS);
|
useMemorizedColumnsWidths(TABLES.MANUAL_JOURNALS);
|
||||||
@@ -107,6 +103,12 @@ function ManualJournalsDataTable({
|
|||||||
},
|
},
|
||||||
[setManualJournalsTableState],
|
[setManualJournalsTableState],
|
||||||
);
|
);
|
||||||
|
// Handle selected rows change.
|
||||||
|
const handleSelectedRowsChange = (selectedFlatRows) => {
|
||||||
|
const selectedIds = selectedFlatRows?.map((row) => row.original.id) || [];
|
||||||
|
setManualJournalsSelectedRows(selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Display manual journal empty status instead of the table.
|
// Display manual journal empty status instead of the table.
|
||||||
if (isEmptyStatus) {
|
if (isEmptyStatus) {
|
||||||
@@ -130,6 +132,7 @@ function ManualJournalsDataTable({
|
|||||||
pagesCount={pagination.pagesCount}
|
pagesCount={pagination.pagesCount}
|
||||||
autoResetSortBy={false}
|
autoResetSortBy={false}
|
||||||
autoResetPage={false}
|
autoResetPage={false}
|
||||||
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
TableLoadingRenderer={TableSkeletonRows}
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||||
ContextMenu={ActionsMenu}
|
ContextMenu={ActionsMenu}
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ export const StatusAccessor = (row) => {
|
|||||||
return (
|
return (
|
||||||
<Choose>
|
<Choose>
|
||||||
<Choose.When condition={!!row.is_published}>
|
<Choose.When condition={!!row.is_published}>
|
||||||
<Tag round>
|
<Tag round minimal>
|
||||||
<T id={'published'} />
|
<T id={'published'} />
|
||||||
</Tag>
|
</Tag>
|
||||||
</Choose.When>
|
</Choose.When>
|
||||||
|
|
||||||
<Choose.Otherwise>
|
<Choose.Otherwise>
|
||||||
<Tag intent={Intent.WARNING} round>
|
<Tag intent={Intent.WARNING} round minimal>
|
||||||
<T id={'draft'} />
|
<T id={'draft'} />
|
||||||
</Tag>
|
</Tag>
|
||||||
</Choose.Otherwise>
|
</Choose.Otherwise>
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import { useValidateBulkDeleteManualJournals } from '@/hooks/query/manualJournals';
|
||||||
|
import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog';
|
||||||
|
|
||||||
|
export const useBulkDeleteManualJournalsDialog = () => {
|
||||||
|
const validateBulkDeleteMutation = useValidateBulkDeleteManualJournals();
|
||||||
|
const {
|
||||||
|
openBulkDeleteDialog,
|
||||||
|
closeBulkDeleteDialog,
|
||||||
|
isValidatingBulkDelete,
|
||||||
|
} = useBulkDeleteDialog(DialogsName.ManualJournalBulkDelete, validateBulkDeleteMutation);
|
||||||
|
|
||||||
|
return {
|
||||||
|
openBulkDeleteDialog,
|
||||||
|
closeBulkDeleteDialog,
|
||||||
|
isValidatingBulkDeleteManualJournals: isValidatingBulkDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
|
getManualJournalsSelectedRowsFactory,
|
||||||
getManualJournalsTableStateFactory,
|
getManualJournalsTableStateFactory,
|
||||||
manualJournalTableStateChangedFactory,
|
manualJournalTableStateChangedFactory,
|
||||||
} from '@/store/manualJournals/manualJournals.selectors';
|
} from '@/store/manualJournals/manualJournals.selectors';
|
||||||
@@ -9,6 +10,7 @@ export default (mapState) => {
|
|||||||
const getJournalsTableQuery = getManualJournalsTableStateFactory();
|
const getJournalsTableQuery = getManualJournalsTableStateFactory();
|
||||||
const manualJournalTableStateChanged =
|
const manualJournalTableStateChanged =
|
||||||
manualJournalTableStateChangedFactory();
|
manualJournalTableStateChangedFactory();
|
||||||
|
const getSelectedRows = getManualJournalsSelectedRowsFactory();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const mapped = {
|
const mapped = {
|
||||||
@@ -17,6 +19,7 @@ export default (mapState) => {
|
|||||||
state,
|
state,
|
||||||
props,
|
props,
|
||||||
),
|
),
|
||||||
|
manualJournalsSelectedRows: getSelectedRows(state, props),
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
+3
@@ -2,11 +2,14 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
setManualJournalsTableState,
|
setManualJournalsTableState,
|
||||||
|
setManualJournalsSelectedRows,
|
||||||
} from '@/store/manualJournals/manualJournals.actions';
|
} from '@/store/manualJournals/manualJournals.actions';
|
||||||
|
|
||||||
const mapActionsToProps = (dispatch) => ({
|
const mapActionsToProps = (dispatch) => ({
|
||||||
setManualJournalsTableState: (queries) =>
|
setManualJournalsTableState: (queries) =>
|
||||||
dispatch(setManualJournalsTableState(queries)),
|
dispatch(setManualJournalsTableState(queries)),
|
||||||
|
setManualJournalsSelectedRows: (selectedRows) =>
|
||||||
|
dispatch(setManualJournalsSelectedRows(selectedRows)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(null, mapActionsToProps);
|
export default connect(null, mapActionsToProps);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { useRefreshAccounts } from '@/hooks/query/accounts';
|
import { useRefreshAccounts } from '@/hooks/query/accounts';
|
||||||
import { useAccountsChartContext } from './AccountsChartProvider';
|
import { useAccountsChartContext } from './AccountsChartProvider';
|
||||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||||
|
import { useBulkDeleteAccountsDialog } from './hooks/use-bulk-delete-accounts-dialog';
|
||||||
|
|
||||||
import withAccounts from './withAccounts';
|
import withAccounts from './withAccounts';
|
||||||
import withAccountsTableActions from './withAccountsTableActions';
|
import withAccountsTableActions from './withAccountsTableActions';
|
||||||
@@ -78,9 +79,15 @@ function AccountsActionsBar({
|
|||||||
// Accounts refresh action.
|
// Accounts refresh action.
|
||||||
const { refresh } = useRefreshAccounts();
|
const { refresh } = useRefreshAccounts();
|
||||||
|
|
||||||
|
// Bulk delete accounts dialog.
|
||||||
|
const {
|
||||||
|
openBulkDeleteDialog,
|
||||||
|
isValidatingBulkDeleteAccounts,
|
||||||
|
} = useBulkDeleteAccountsDialog();
|
||||||
|
|
||||||
// Handle bulk accounts delete.
|
// Handle bulk accounts delete.
|
||||||
const handleBulkDelete = () => {
|
const handleBulkDelete = () => {
|
||||||
openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows });
|
openBulkDeleteDialog(accountsSelectedRows);
|
||||||
};
|
};
|
||||||
// Handle bulk accounts activate.
|
// Handle bulk accounts activate.
|
||||||
const handelBulkActivate = () => {
|
const handelBulkActivate = () => {
|
||||||
@@ -126,6 +133,35 @@ function AccountsActionsBar({
|
|||||||
openDialog(DialogsName.AccountForm, {});
|
openDialog(DialogsName.AccountForm, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isEmpty(accountsSelectedRows)) {
|
||||||
|
return (
|
||||||
|
<DashboardActionsBar>
|
||||||
|
<NavbarGroup>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="play-16" iconSize={16} />}
|
||||||
|
text={<T id={'activate'} />}
|
||||||
|
onClick={handelBulkActivate}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="pause-16" iconSize={16} />}
|
||||||
|
text={<T id={'inactivate'} />}
|
||||||
|
onClick={handelBulkInactive}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
text={<T id={'delete'} />}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
disabled={isValidatingBulkDeleteAccounts}
|
||||||
|
/>
|
||||||
|
</NavbarGroup>
|
||||||
|
</DashboardActionsBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -162,28 +198,6 @@ function AccountsActionsBar({
|
|||||||
|
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
|
|
||||||
<If condition={!isEmpty(accountsSelectedRows)}>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="play-16" iconSize={16} />}
|
|
||||||
text={<T id={'activate'} />}
|
|
||||||
onClick={handelBulkActivate}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="pause-16" iconSize={16} />}
|
|
||||||
text={<T id={'inactivate'} />}
|
|
||||||
onClick={handelBulkInactive}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
|
||||||
text={<T id={'delete'} />}
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
onClick={handleBulkDelete}
|
|
||||||
/>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="print-16" iconSize={16} />}
|
icon={<Icon icon="print-16" iconSize={16} />}
|
||||||
|
|||||||
@@ -10,9 +10,17 @@ const AccountInactivateAlert = React.lazy(
|
|||||||
const AccountActivateAlert = React.lazy(
|
const AccountActivateAlert = React.lazy(
|
||||||
() => import('@/containers/Alerts/Accounts/AccountActivateAlert'),
|
() => import('@/containers/Alerts/Accounts/AccountActivateAlert'),
|
||||||
);
|
);
|
||||||
|
const AccountBulkActivateAlert = React.lazy(
|
||||||
|
() => import('@/containers/Alerts/Accounts/AccountBulkActivateAlert'),
|
||||||
|
);
|
||||||
|
const AccountBulkInactivateAlert = React.lazy(
|
||||||
|
() => import('@/containers/Alerts/Accounts/AccountBulkInactivateAlert'),
|
||||||
|
);
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ name: 'account-delete', component: AccountDeleteAlert },
|
{ name: 'account-delete', component: AccountDeleteAlert },
|
||||||
{ name: 'account-inactivate', component: AccountInactivateAlert },
|
{ name: 'account-inactivate', component: AccountInactivateAlert },
|
||||||
{ name: 'account-activate', component: AccountActivateAlert },
|
{ name: 'account-activate', component: AccountActivateAlert },
|
||||||
|
{ name: 'accounts-bulk-activate', component: AccountBulkActivateAlert },
|
||||||
|
{ name: 'accounts-bulk-inactivate', component: AccountBulkInactivateAlert },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import withSettings from '@/containers/Settings/withSettings';
|
|||||||
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import withAccountsTableActions from './withAccountsTableActions';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ function AccountsDataTable({
|
|||||||
|
|
||||||
// #withSettings
|
// #withSettings
|
||||||
accountsTableSize,
|
accountsTableSize,
|
||||||
|
|
||||||
|
// #withAccountsTableActions
|
||||||
|
setAccountsSelectedRows,
|
||||||
}) {
|
}) {
|
||||||
const { isAccountsLoading, isAccountsFetching, accounts } =
|
const { isAccountsLoading, isAccountsFetching, accounts } =
|
||||||
useAccountsChartContext();
|
useAccountsChartContext();
|
||||||
@@ -91,6 +95,12 @@ function AccountsDataTable({
|
|||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
useMemorizedColumnsWidths(TABLES.ACCOUNTS);
|
useMemorizedColumnsWidths(TABLES.ACCOUNTS);
|
||||||
|
|
||||||
|
// Handle selected rows change.
|
||||||
|
const handleSelectedRowsChange = (selectedFlatRows) => {
|
||||||
|
const selectedIds = selectedFlatRows?.map((row) => row.original.id) || [];
|
||||||
|
setAccountsSelectedRows(selectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
@@ -118,6 +128,7 @@ function AccountsDataTable({
|
|||||||
vListrowHeight={accountsTableSize == 'small' ? 40 : 42}
|
vListrowHeight={accountsTableSize == 'small' ? 40 : 42}
|
||||||
vListOverscanRowCount={0}
|
vListOverscanRowCount={0}
|
||||||
onCellClick={handleCellClick}
|
onCellClick={handleCellClick}
|
||||||
|
onSelectedRowsChange={handleSelectedRowsChange}
|
||||||
initialColumnsWidths={initialColumnsWidths}
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
onColumnResizing={handleColumnResizing}
|
onColumnResizing={handleColumnResizing}
|
||||||
size={accountsTableSize}
|
size={accountsTableSize}
|
||||||
@@ -137,6 +148,7 @@ export default compose(
|
|||||||
withAlertsActions,
|
withAlertsActions,
|
||||||
withDrawerActions,
|
withDrawerActions,
|
||||||
withDialogActions,
|
withDialogActions,
|
||||||
|
withAccountsTableActions,
|
||||||
withSettings(({ accountsSettings }) => ({
|
withSettings(({ accountsSettings }) => ({
|
||||||
accountsTableSize: accountsSettings.tableSize,
|
accountsTableSize: accountsSettings.tableSize,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import { useValidateBulkDeleteAccounts } from '@/hooks/query/accounts';
|
||||||
|
import { useBulkDeleteDialog } from '@/hooks/dialogs/useBulkDeleteDialog';
|
||||||
|
|
||||||
|
export const useBulkDeleteAccountsDialog = () => {
|
||||||
|
const validateBulkDeleteMutation = useValidateBulkDeleteAccounts();
|
||||||
|
const {
|
||||||
|
openBulkDeleteDialog,
|
||||||
|
closeBulkDeleteDialog,
|
||||||
|
isValidatingBulkDelete,
|
||||||
|
} = useBulkDeleteDialog(DialogsName.AccountBulkDelete, validateBulkDeleteMutation);
|
||||||
|
|
||||||
|
return {
|
||||||
|
openBulkDeleteDialog,
|
||||||
|
closeBulkDeleteDialog,
|
||||||
|
isValidatingBulkDeleteAccounts: isValidatingBulkDelete,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export default (mapState) => {
|
|||||||
const mapped = {
|
const mapped = {
|
||||||
accountsTableState: getAccountsTableState(state, props),
|
accountsTableState: getAccountsTableState(state, props),
|
||||||
accountsTableStateChanged: accountsTableStateChanged(state, props),
|
accountsTableStateChanged: accountsTableStateChanged(state, props),
|
||||||
|
accountsSelectedRows: state.accounts?.selectedRows || [],
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { connect } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
setAccountsTableState,
|
setAccountsTableState,
|
||||||
resetAccountsTableState,
|
resetAccountsTableState,
|
||||||
|
setAccountsSelectedRows,
|
||||||
} from '@/store/accounts/accounts.actions';
|
} from '@/store/accounts/accounts.actions';
|
||||||
|
|
||||||
const mapActionsToProps = (dispatch) => ({
|
const mapActionsToProps = (dispatch) => ({
|
||||||
setAccountsTableState: (queries) => dispatch(setAccountsTableState(queries)),
|
setAccountsTableState: (queries) => dispatch(setAccountsTableState(queries)),
|
||||||
resetAccountsTableState: () => dispatch(resetAccountsTableState()),
|
resetAccountsTableState: () => dispatch(resetAccountsTableState()),
|
||||||
|
setAccountsSelectedRows: (selectedRows) =>
|
||||||
|
dispatch(setAccountsSelectedRows(selectedRows)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(null, mapActionsToProps);
|
export default connect(null, mapActionsToProps);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Intent, Alert } from '@blueprintjs/core';
|
|||||||
import { queryCache } from 'react-query';
|
import { queryCache } from 'react-query';
|
||||||
import { FormattedMessage as T, AppToaster } from '@/components';
|
import { FormattedMessage as T, AppToaster } from '@/components';
|
||||||
|
|
||||||
import withAccountsActions from '@/containers/Accounts/withAccountsActions';
|
|
||||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ function AccountBulkActivateAlert({
|
|||||||
// #withAlertActions
|
// #withAlertActions
|
||||||
closeAlert,
|
closeAlert,
|
||||||
|
|
||||||
|
// TODO: Implement bulk activate accounts hook and use it here
|
||||||
requestBulkActivateAccounts,
|
requestBulkActivateAccounts,
|
||||||
}) {
|
}) {
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
@@ -40,7 +40,7 @@ function AccountBulkActivateAlert({
|
|||||||
});
|
});
|
||||||
queryCache.invalidateQueries('accounts-table');
|
queryCache.invalidateQueries('accounts-table');
|
||||||
})
|
})
|
||||||
.catch((errors) => {})
|
.catch((errors) => { })
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
closeAlert(name);
|
closeAlert(name);
|
||||||
@@ -67,5 +67,4 @@ function AccountBulkActivateAlert({
|
|||||||
export default compose(
|
export default compose(
|
||||||
withAlertStoreConnect(),
|
withAlertStoreConnect(),
|
||||||
withAlertActions,
|
withAlertActions,
|
||||||
withAccountsActions,
|
|
||||||
)(AccountBulkActivateAlert);
|
)(AccountBulkActivateAlert);
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { Intent, Alert } from '@blueprintjs/core';
|
|
||||||
import { queryCache } from 'react-query';
|
|
||||||
import { AppToaster } from '@/components';
|
|
||||||
|
|
||||||
import { handleDeleteErrors } from '@/containers/Accounts/utils';
|
|
||||||
|
|
||||||
import withAccountsActions from '@/containers/Accounts/withAccountsActions';
|
|
||||||
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
|
||||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Account bulk delete alert.
|
|
||||||
*/
|
|
||||||
function AccountBulkDeleteAlert({
|
|
||||||
// #ownProps
|
|
||||||
name,
|
|
||||||
|
|
||||||
// #withAlertStoreConnect
|
|
||||||
isOpen,
|
|
||||||
payload: { accountsIds },
|
|
||||||
|
|
||||||
// #withAlertActions
|
|
||||||
closeAlert,
|
|
||||||
|
|
||||||
// #withAccountsActions
|
|
||||||
requestDeleteBulkAccounts,
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const selectedRowsCount = 0;
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
closeAlert(name);
|
|
||||||
};
|
|
||||||
// Handle confirm accounts bulk delete.
|
|
||||||
const handleConfirmBulkDelete = () => {
|
|
||||||
setLoading(true);
|
|
||||||
requestDeleteBulkAccounts(accountsIds)
|
|
||||||
.then(() => {
|
|
||||||
AppToaster.show({
|
|
||||||
message: intl.get('the_accounts_has_been_successfully_deleted'),
|
|
||||||
intent: Intent.SUCCESS,
|
|
||||||
});
|
|
||||||
queryCache.invalidateQueries('accounts-table');
|
|
||||||
})
|
|
||||||
.catch((errors) => {
|
|
||||||
handleDeleteErrors(errors);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
closeAlert(name);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
cancelButtonText={<T id={'cancel'} />}
|
|
||||||
confirmButtonText={`${intl.get('delete')} (${selectedRowsCount})`}
|
|
||||||
icon="trash"
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onConfirm={handleConfirmBulkDelete}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<T id={'once_delete_these_accounts_you_will_not_able_restore_them'} />
|
|
||||||
</p>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withAlertStoreConnect(),
|
|
||||||
withAlertActions,
|
|
||||||
withAccountsActions,
|
|
||||||
)(AccountBulkDeleteAlert);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user