From a0bc9db9a69886fadf0c27a7863181e9a7719b8a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 3 Nov 2025 21:40:24 +0200 Subject: [PATCH] feat: bulk transcations delete --- .../server/src/common/dtos/BulkDelete.dto.ts | 43 +++++++++++ .../modules/Accounts/Accounts.controller.ts | 36 +++++++++ .../src/modules/Accounts/Accounts.module.ts | 6 +- .../Accounts/AccountsApplication.service.ts | 25 ++++++- .../Accounts/BulkDeleteAccounts.service.ts | 33 +++++++++ .../ValidateBulkDeleteAccounts.service.ts | 63 ++++++++++++++++ .../modules/Bills/BulkDeleteBills.service.ts | 28 +++++++ .../Bills/ValidateBulkDeleteBills.service.ts | 51 +++++++++++++ .../BulkDeleteCreditNotes.service.ts | 30 ++++++++ .../ValidateBulkDeleteCreditNotes.service.ts | 51 +++++++++++++ .../Expenses/BulkDeleteExpenses.service.ts | 28 +++++++ .../ValidateBulkDeleteExpenses.service.ts | 51 +++++++++++++ .../BulkDeleteItemCategories.service.ts | 30 ++++++++ ...alidateBulkDeleteItemCategories.service.ts | 51 +++++++++++++ .../modules/Items/BulkDeleteItems.service.ts | 38 ++++++++++ .../src/modules/Items/Item.controller.ts | 36 +++++++++ .../server/src/modules/Items/Items.module.ts | 8 +- .../modules/Items/ItemsApplication.service.ts | 29 +++++++- .../Items/ValidateBulkDeleteItems.service.ts | 74 +++++++++++++++++++ .../modules/Items/dtos/BulkDeleteItems.dto.ts | 43 +++++++++++ .../BulkDeleteManualJournals.service.ts | 32 ++++++++ ...alidateBulkDeleteManualJournals.service.ts | 55 ++++++++++++++ .../BulkDeletePaymentReceived.service.ts | 32 ++++++++ ...lidateBulkDeletePaymentReceived.service.ts | 55 ++++++++++++++ .../BulkDeleteSaleEstimates.service.ts | 28 +++++++ ...ValidateBulkDeleteSaleEstimates.service.ts | 51 +++++++++++++ .../BulkDeleteSaleInvoices.service.ts | 28 +++++++ .../ValidateBulkDeleteSaleInvoices.service.ts | 51 +++++++++++++ .../BulkDeleteVendorCredits.service.ts | 30 ++++++++ ...ValidateBulkDeleteVendorCredits.service.ts | 55 ++++++++++++++ .../JournalsLanding/ManualJournalsAlerts.tsx | 5 ++ .../Accounting/JournalsLanding/components.tsx | 4 +- .../Accounts/AccountsActionsBar.tsx | 1 + .../containers/Accounts/AccountsAlerts.tsx | 12 +++ .../containers/Accounts/AccountsDataTable.tsx | 12 +++ .../src/containers/Accounts/withAccounts.tsx | 1 + .../Accounts/withAccountsTableActions.tsx | 3 + .../Accounts/AccountBulkActivateAlert.tsx | 5 +- .../Accounts/AccountBulkDeleteAlert.tsx | 25 ++----- .../Alerts/Bills/BillBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../CreditNotes/CreditNoteBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Estimates/EstimateBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Expenses/ExpenseBulkDeleteAlert.tsx | 45 +++++------ .../Invoices/InvoiceBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Alerts/Items/ItemBulkDeleteAlert.tsx | 23 ++---- .../ManualJournals/JournalBulkDeleteAlert.tsx | 73 +++++++++++------- .../PaymentReceivedBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../VendorCreditBulkDeleteAlert.tsx | 69 +++++++++++++++++ .../Expenses/ExpenseForm/ExpenseForm.tsx | 13 ++-- .../containers/Expenses/ExpensesAlerts.tsx | 5 ++ .../Expenses/ExpensesLanding/components.tsx | 2 +- .../src/containers/Items/ItemsDataTable.tsx | 14 +++- .../src/containers/Items/components.tsx | 2 +- .../src/containers/Items/withItemsActions.tsx | 2 + .../Bills/BillsLanding/BillsAlerts.tsx | 5 ++ .../Bills/BillsLanding/components.tsx | 10 +-- .../CreditNotesLanding/components.tsx | 6 +- .../CreditNotes/VendorCreditNotesAlerts.tsx | 9 +++ .../Sales/CreditNotes/CreditNotesAlerts.tsx | 8 ++ .../CreditNotesDataTable.tsx | 12 +++ .../CreditNotesLanding/components.tsx | 6 +- .../CreditNotesLanding/withCreditNotes.tsx | 1 + .../withCreditNotesActions.tsx | 2 + .../Sales/Estimates/EstimatesAlerts.tsx | 5 ++ .../EstimatesLanding/EstimatesActionsBar.tsx | 19 ++++- .../EstimatesLanding/EstimatesDataTable.tsx | 12 +++ .../Estimates/EstimatesLanding/components.tsx | 14 ++-- .../EstimatesLanding/withEstimates.tsx | 1 + .../EstimatesLanding/withEstimatesActions.tsx | 2 + .../Sales/Invoices/InvoicesAlerts.tsx | 5 ++ .../InvoicesLanding/InvoicesActionsBar.tsx | 16 +++- .../InvoicesLanding/InvoicesDataTable.tsx | 12 +++ .../Invoices/InvoicesLanding/components.tsx | 49 +++++------- .../InvoicesLanding/withInvoiceActions.tsx | 4 +- .../Invoices/InvoicesLanding/withInvoices.tsx | 1 + .../PaymentsReceivedAlerts.tsx | 9 +++ .../Receipts/ReceiptsLanding/components.tsx | 4 +- packages/webapp/src/hooks/query/accounts.tsx | 34 +++++++++ packages/webapp/src/hooks/query/bills.tsx | 19 +++++ .../webapp/src/hooks/query/creditNote.tsx | 19 +++++ packages/webapp/src/hooks/query/estimates.tsx | 19 +++++ packages/webapp/src/hooks/query/expenses.tsx | 19 +++++ packages/webapp/src/hooks/query/invoices.tsx | 19 +++++ packages/webapp/src/hooks/query/items.tsx | 19 +++++ .../webapp/src/hooks/query/manualJournals.tsx | 19 +++++ .../src/hooks/query/paymentReceives.tsx | 19 +++++ .../webapp/src/hooks/query/vendorCredit.tsx | 19 +++++ .../webapp/src/store/Bills/bills.actions.tsx | 6 ++ .../webapp/src/store/Bills/bills.reducer.tsx | 5 ++ .../store/CreditNote/creditNote.actions.tsx | 7 +- .../store/CreditNote/creditNote.reducer.tsx | 5 ++ .../src/store/Estimate/estimates.actions.tsx | 7 ++ .../src/store/Estimate/estimates.reducer.tsx | 5 ++ .../src/store/Invoice/invoices.actions.tsx | 7 +- .../src/store/Invoice/invoices.reducer.tsx | 5 ++ .../paymentReceives.actions.tsx | 7 ++ .../paymentReceives.reducer.tsx | 5 ++ .../VendorCredit/VendorCredit.reducer.tsx | 5 ++ .../VendorCredit/vendorCredit.actions.tsx | 7 +- .../src/store/accounts/accounts.actions.tsx | 10 +++ .../src/store/accounts/accounts.reducer.tsx | 5 ++ .../src/store/expenses/expenses.actions.tsx | 6 ++ .../src/store/expenses/expenses.reducer.tsx | 5 ++ .../webapp/src/store/items/items.actions.tsx | 7 +- .../webapp/src/store/items/items.reducer.tsx | 4 + .../manualJournals/manualJournals.actions.tsx | 7 ++ .../manualJournals.reducers.tsx | 5 ++ 107 files changed, 2213 insertions(+), 156 deletions(-) create mode 100644 packages/server/src/common/dtos/BulkDelete.dto.ts create mode 100644 packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts create mode 100644 packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts create mode 100644 packages/server/src/modules/Bills/BulkDeleteBills.service.ts create mode 100644 packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts create mode 100644 packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts create mode 100644 packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts create mode 100644 packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts create mode 100644 packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts create mode 100644 packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts create mode 100644 packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts create mode 100644 packages/server/src/modules/Items/BulkDeleteItems.service.ts create mode 100644 packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts create mode 100644 packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts create mode 100644 packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts create mode 100644 packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts create mode 100644 packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts create mode 100644 packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts create mode 100644 packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts create mode 100644 packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts create mode 100644 packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts create mode 100644 packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts create mode 100644 packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts create mode 100644 packages/webapp/src/containers/Alerts/Bills/BillBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/CreditNotes/CreditNoteBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/Estimates/EstimateBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/Invoices/InvoiceBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/PaymentReceived/PaymentReceivedBulkDeleteAlert.tsx create mode 100644 packages/webapp/src/containers/Alerts/VendorCeditNotes/VendorCreditBulkDeleteAlert.tsx diff --git a/packages/server/src/common/dtos/BulkDelete.dto.ts b/packages/server/src/common/dtos/BulkDelete.dto.ts new file mode 100644 index 000000000..6b1415499 --- /dev/null +++ b/packages/server/src/common/dtos/BulkDelete.dto.ts @@ -0,0 +1,43 @@ +import { IsArray, IsInt, ArrayNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class BulkDeleteDto { + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + @ApiProperty({ + description: 'Array of IDs to delete', + type: [Number], + example: [1, 2, 3], + }) + ids: number[]; +} + +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[]; +} + diff --git a/packages/server/src/modules/Accounts/Accounts.controller.ts b/packages/server/src/modules/Accounts/Accounts.controller.ts index 5d0413f6d..7a9f26163 100644 --- a/packages/server/src/modules/Accounts/Accounts.controller.ts +++ b/packages/server/src/modules/Accounts/Accounts.controller.ts @@ -26,12 +26,17 @@ import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionRe import { GetAccountTransactionsQueryDto } from './dtos/GetAccountTransactionsQuery.dto'; import { GetAccountsQueryDto } from './dtos/GetAccountsQuery.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { + BulkDeleteDto, + ValidateBulkDeleteResponseDto, +} from '@/common/dtos/BulkDelete.dto'; @Controller('accounts') @ApiTags('Accounts') @ApiExtraModels(AccountResponseDto) @ApiExtraModels(AccountTypeResponseDto) @ApiExtraModels(GetAccountTransactionResponseDto) +@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiCommonHeaders() export class AccountsController { constructor(private readonly accountsApplication: AccountsApplication) { } @@ -83,6 +88,37 @@ export class AccountsController { return this.accountsApplication.deleteAccount(id); } + @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 { + 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 { + return this.accountsApplication.bulkDeleteAccounts(bulkDeleteDto.ids); + } + @Post(':id/activate') @ApiOperation({ summary: 'Activate the given account.' }) @ApiResponse({ diff --git a/packages/server/src/modules/Accounts/Accounts.module.ts b/packages/server/src/modules/Accounts/Accounts.module.ts index 42abb8fcf..85f0c6488 100644 --- a/packages/server/src/modules/Accounts/Accounts.module.ts +++ b/packages/server/src/modules/Accounts/Accounts.module.ts @@ -19,6 +19,8 @@ import { GetAccountsService } from './GetAccounts.service'; import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { AccountsExportable } from './AccountsExportable.service'; import { AccountsImportable } from './AccountsImportable.service'; +import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service'; +import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service'; const models = [RegisterTenancyModel(BankAccount)]; @@ -40,7 +42,9 @@ const models = [RegisterTenancyModel(BankAccount)]; GetAccountTransactionsService, GetAccountsService, AccountsExportable, - AccountsImportable + AccountsImportable, + BulkDeleteAccountsService, + ValidateBulkDeleteAccountsService, ], exports: [ AccountRepository, diff --git a/packages/server/src/modules/Accounts/AccountsApplication.service.ts b/packages/server/src/modules/Accounts/AccountsApplication.service.ts index a34cb8bb2..b1a8b60e8 100644 --- a/packages/server/src/modules/Accounts/AccountsApplication.service.ts +++ b/packages/server/src/modules/Accounts/AccountsApplication.service.ts @@ -15,6 +15,9 @@ import { GetAccountsService } from './GetAccounts.service'; import { IFilterMeta } from '@/interfaces/Model'; import { GetAccountTransactionResponseDto } from './dtos/GetAccountTransactionResponse.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() export class AccountsApplication { @@ -37,7 +40,9 @@ export class AccountsApplication { private readonly getAccountService: GetAccount, private readonly getAccountTransactionsService: GetAccountTransactionsService, private readonly getAccountsService: GetAccountsService, - ) { } + private readonly bulkDeleteAccountsService: BulkDeleteAccountsService, + private readonly validateBulkDeleteAccountsService: ValidateBulkDeleteAccountsService, + ) {} /** * Creates a new account. @@ -128,4 +133,22 @@ export class AccountsApplication { ): Promise> => { return this.getAccountTransactionsService.getAccountsTransactions(filter); }; + + /** + * Validates which accounts can be deleted in bulk. + */ + public validateBulkDeleteAccounts = ( + accountIds: number[], + ): Promise => { + return this.validateBulkDeleteAccountsService.validateBulkDeleteAccounts( + accountIds, + ); + }; + + /** + * Deletes multiple accounts in bulk. + */ + public bulkDeleteAccounts = (accountIds: number[]): Promise => { + return this.bulkDeleteAccountsService.bulkDeleteAccounts(accountIds); + }; } diff --git a/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts b/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts new file mode 100644 index 000000000..cf65de70f --- /dev/null +++ b/packages/server/src/modules/Accounts/BulkDeleteAccounts.service.ts @@ -0,0 +1,33 @@ +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} accountIds - The account id or ids. + * @param {Knex.Transaction} trx - The transaction. + */ + async bulkDeleteAccounts( + accountIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const accountsIds = uniq(castArray(accountIds)); + + const results = await PromisePool.withConcurrency(1) + .for(accountsIds) + .process(async (accountId: number) => { + await this.deleteAccountService.deleteAccount(accountId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts b/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts new file mode 100644 index 000000000..de513bb48 --- /dev/null +++ b/packages/server/src/modules/Accounts/ValidateBulkDeleteAccounts.service.ts @@ -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); + 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; + } + } +} + diff --git a/packages/server/src/modules/Bills/BulkDeleteBills.service.ts b/packages/server/src/modules/Bills/BulkDeleteBills.service.ts new file mode 100644 index 000000000..dfc71dc2a --- /dev/null +++ b/packages/server/src/modules/Bills/BulkDeleteBills.service.ts @@ -0,0 +1,28 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const billsIds = uniq(castArray(billIds)); + + const results = await PromisePool.withConcurrency(1) + .for(billsIds) + .process(async (billId: number) => { + await this.deleteBillService.deleteBill(billId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts b/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts new file mode 100644 index 000000000..d16145901 --- /dev/null +++ b/packages/server/src/modules/Bills/ValidateBulkDeleteBills.service.ts @@ -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); + 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; + } + } +} + diff --git a/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts b/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts new file mode 100644 index 000000000..ec226d27d --- /dev/null +++ b/packages/server/src/modules/CreditNotes/BulkDeleteCreditNotes.service.ts @@ -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 { DeleteCreditNoteService } from './commands/DeleteCreditNote.service'; + +@Injectable() +export class BulkDeleteCreditNotesService { + constructor( + private readonly deleteCreditNoteService: DeleteCreditNoteService, + ) { } + + async bulkDeleteCreditNotes( + creditNoteIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const notesIds = uniq(castArray(creditNoteIds)); + + const results = await PromisePool.withConcurrency(1) + .for(notesIds) + .process(async (creditNoteId: number) => { + await this.deleteCreditNoteService.deleteCreditNote(creditNoteId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts b/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts new file mode 100644 index 000000000..46fab3735 --- /dev/null +++ b/packages/server/src/modules/CreditNotes/ValidateBulkDeleteCreditNotes.service.ts @@ -0,0 +1,51 @@ +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); + 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; + } + } +} + diff --git a/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts b/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts new file mode 100644 index 000000000..59dca523b --- /dev/null +++ b/packages/server/src/modules/Expenses/BulkDeleteExpenses.service.ts @@ -0,0 +1,28 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const expensesIds = uniq(castArray(expenseIds)); + + const results = await PromisePool.withConcurrency(1) + .for(expensesIds) + .process(async (expenseId: number) => { + await this.deleteExpenseService.deleteExpense(expenseId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts b/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts new file mode 100644 index 000000000..49684968d --- /dev/null +++ b/packages/server/src/modules/Expenses/ValidateBulkDeleteExpenses.service.ts @@ -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); + 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; + } + } +} + diff --git a/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts b/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts new file mode 100644 index 000000000..30e608d52 --- /dev/null +++ b/packages/server/src/modules/ItemCategories/BulkDeleteItemCategories.service.ts @@ -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, + trx?: Knex.Transaction, + ): Promise { + 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; + } + } +} + diff --git a/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts b/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts new file mode 100644 index 000000000..a9de7ab7d --- /dev/null +++ b/packages/server/src/modules/ItemCategories/ValidateBulkDeleteItemCategories.service.ts @@ -0,0 +1,51 @@ +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); + 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; + } + } +} + diff --git a/packages/server/src/modules/Items/BulkDeleteItems.service.ts b/packages/server/src/modules/Items/BulkDeleteItems.service.ts new file mode 100644 index 000000000..e5ca88a3c --- /dev/null +++ b/packages/server/src/modules/Items/BulkDeleteItems.service.ts @@ -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 { DeleteItemService } from './DeleteItem.service'; + +@Injectable() +export class BulkDeleteItemsService { + constructor(private readonly deleteItemService: DeleteItemService) { } + + /** + * Deletes multiple items. + * @param {number | Array} itemIds - The item id or ids. + * @param {Knex.Transaction} trx - The transaction. + */ + async bulkDeleteItems( + itemIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const itemsIds = uniq(castArray(itemIds)); + + // Use PromisePool to delete items sequentially (concurrency: 1) + // to avoid potential database locks and maintain transaction integrity + const results = await PromisePool.withConcurrency(1) + .for(itemsIds) + .process(async (itemId: number) => { + await this.deleteItemService.deleteItem(itemId, trx); + }); + + // Check if there were any errors + if (results.errors && results.errors.length > 0) { + // If needed, you can throw an error here or handle errors individually + // For now, we'll let individual errors bubble up + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/Items/Item.controller.ts b/packages/server/src/modules/Items/Item.controller.ts index 0afa9b467..eed9ef503 100644 --- a/packages/server/src/modules/Items/Item.controller.ts +++ b/packages/server/src/modules/Items/Item.controller.ts @@ -29,6 +29,10 @@ import { ItemEstimatesResponseDto } from './dtos/ItemEstimatesResponse.dto'; import { ItemBillsResponseDto } from './dtos/ItemBillsResponse.dto'; import { ItemReceiptsResponseDto } from './dtos/ItemReceiptsResponse.dto'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; +import { + BulkDeleteItemsDto, + ValidateBulkDeleteItemsResponseDto, +} from './dtos/BulkDeleteItems.dto'; @Controller('/items') @ApiTags('Items') @@ -39,6 +43,7 @@ import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; @ApiExtraModels(ItemBillsResponseDto) @ApiExtraModels(ItemEstimatesResponseDto) @ApiExtraModels(ItemReceiptsResponseDto) +@ApiExtraModels(ValidateBulkDeleteItemsResponseDto) @ApiCommonHeaders() export class ItemsController extends TenantController { constructor(private readonly itemsApplication: ItemsApplicationService) { @@ -340,4 +345,35 @@ export class ItemsController extends TenantController { const itemId = parseInt(id, 10); 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 { + 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 { + return this.itemsApplication.bulkDeleteItems(bulkDeleteDto.ids); + } } diff --git a/packages/server/src/modules/Items/Items.module.ts b/packages/server/src/modules/Items/Items.module.ts index 8cc26d759..b18e8dad8 100644 --- a/packages/server/src/modules/Items/Items.module.ts +++ b/packages/server/src/modules/Items/Items.module.ts @@ -18,6 +18,8 @@ import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module'; import { ItemsExportable } from './ItemsExportable.service'; import { ItemsImportable } from './ItemsImportable.service'; +import { BulkDeleteItemsService } from './BulkDeleteItems.service'; +import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service'; @Module({ imports: [ @@ -41,8 +43,10 @@ import { ItemsImportable } from './ItemsImportable.service'; TransformerInjectable, ItemsEntriesService, ItemsExportable, - ItemsImportable + ItemsImportable, + BulkDeleteItemsService, + ValidateBulkDeleteItemsService, ], exports: [ItemsEntriesService, ItemsExportable, ItemsImportable], }) -export class ItemsModule {} +export class ItemsModule { } diff --git a/packages/server/src/modules/Items/ItemsApplication.service.ts b/packages/server/src/modules/Items/ItemsApplication.service.ts index a93e29587..f8b347a5d 100644 --- a/packages/server/src/modules/Items/ItemsApplication.service.ts +++ b/packages/server/src/modules/Items/ItemsApplication.service.ts @@ -13,6 +13,8 @@ import { GetItemsService } from './GetItems.service'; import { IItemsFilter } from './types/Items.types'; import { EditItemDto, CreateItemDto } from './dtos/Item.dto'; import { GetItemsQueryDto } from './dtos/GetItemsQuery.dto'; +import { BulkDeleteItemsService } from './BulkDeleteItems.service'; +import { ValidateBulkDeleteItemsService } from './ValidateBulkDeleteItems.service'; @Injectable() export class ItemsApplicationService { @@ -25,7 +27,9 @@ export class ItemsApplicationService { private readonly getItemService: GetItemService, private readonly getItemsService: GetItemsService, private readonly itemTransactionsService: ItemTransactionsService, - ) {} + private readonly bulkDeleteItemsService: BulkDeleteItemsService, + private readonly validateBulkDeleteItemsService: ValidateBulkDeleteItemsService, + ) { } /** * Creates a new item. @@ -134,4 +138,27 @@ export class ItemsApplicationService { async getItemReceiptsTransactions(itemId: number): Promise { 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} + */ + async bulkDeleteItems(itemIds: number[]): Promise { + return this.bulkDeleteItemsService.bulkDeleteItems(itemIds); + } } diff --git a/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts b/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts new file mode 100644 index 000000000..45f7e6c8d --- /dev/null +++ b/packages/server/src/modules/Items/ValidateBulkDeleteItems.service.ts @@ -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; + } + } +} + diff --git a/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts b/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts new file mode 100644 index 000000000..64217cadb --- /dev/null +++ b/packages/server/src/modules/Items/dtos/BulkDeleteItems.dto.ts @@ -0,0 +1,43 @@ +import { IsArray, IsInt, ArrayNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class BulkDeleteItemsDto { + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + @ApiProperty({ + description: 'Array of item IDs to delete', + type: [Number], + example: [1, 2, 3], + }) + ids: number[]; +} + +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[]; +} + diff --git a/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts b/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts new file mode 100644 index 000000000..f4a49ea90 --- /dev/null +++ b/packages/server/src/modules/ManualJournals/BulkDeleteManualJournals.service.ts @@ -0,0 +1,32 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const journalsIds = uniq(castArray(manualJournalIds)); + + const results = await PromisePool.withConcurrency(1) + .for(journalsIds) + .process(async (manualJournalId: number) => { + await this.deleteManualJournalService.deleteManualJournal( + manualJournalId, + ); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts b/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts new file mode 100644 index 000000000..44eba602c --- /dev/null +++ b/packages/server/src/modules/ManualJournals/ValidateBulkDeleteManualJournals.service.ts @@ -0,0 +1,55 @@ +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, + ); + 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; + } + } +} + diff --git a/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts b/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts new file mode 100644 index 000000000..cb0f69348 --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/BulkDeletePaymentReceived.service.ts @@ -0,0 +1,32 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const paymentsIds = uniq(castArray(paymentReceiveIds)); + + const results = await PromisePool.withConcurrency(1) + .for(paymentsIds) + .process(async (paymentReceiveId: number) => { + await this.deletePaymentReceivedService.deletePaymentReceive( + paymentReceiveId, + ); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts b/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts new file mode 100644 index 000000000..159572f2f --- /dev/null +++ b/packages/server/src/modules/PaymentReceived/ValidateBulkDeletePaymentReceived.service.ts @@ -0,0 +1,55 @@ +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, + ); + 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; + } + } +} + diff --git a/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts b/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts new file mode 100644 index 000000000..a122c69ff --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/BulkDeleteSaleEstimates.service.ts @@ -0,0 +1,28 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const estimatesIds = uniq(castArray(saleEstimateIds)); + + const results = await PromisePool.withConcurrency(1) + .for(estimatesIds) + .process(async (saleEstimateId: number) => { + await this.deleteSaleEstimateService.deleteEstimate(saleEstimateId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts b/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts new file mode 100644 index 000000000..efde94c4a --- /dev/null +++ b/packages/server/src/modules/SaleEstimates/ValidateBulkDeleteSaleEstimates.service.ts @@ -0,0 +1,51 @@ +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); + 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; + } + } +} + diff --git a/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts b/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts new file mode 100644 index 000000000..cb29c222a --- /dev/null +++ b/packages/server/src/modules/SaleInvoices/BulkDeleteSaleInvoices.service.ts @@ -0,0 +1,28 @@ +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, + trx?: Knex.Transaction, + ): Promise { + const invoicesIds = uniq(castArray(saleInvoiceIds)); + + const results = await PromisePool.withConcurrency(1) + .for(invoicesIds) + .process(async (saleInvoiceId: number) => { + await this.deleteSaleInvoiceService.deleteSaleInvoice(saleInvoiceId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts b/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts new file mode 100644 index 000000000..c69157c57 --- /dev/null +++ b/packages/server/src/modules/SaleInvoices/ValidateBulkDeleteSaleInvoices.service.ts @@ -0,0 +1,51 @@ +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); + 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; + } + } +} + diff --git a/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts b/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts new file mode 100644 index 000000000..987905af3 --- /dev/null +++ b/packages/server/src/modules/VendorCredit/BulkDeleteVendorCredits.service.ts @@ -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 { DeleteVendorCreditService } from './commands/DeleteVendorCredit.service'; + +@Injectable() +export class BulkDeleteVendorCreditsService { + constructor( + private readonly deleteVendorCreditService: DeleteVendorCreditService, + ) {} + + async bulkDeleteVendorCredits( + vendorCreditIds: number | Array, + trx?: Knex.Transaction, + ): Promise { + const creditsIds = uniq(castArray(vendorCreditIds)); + + const results = await PromisePool.withConcurrency(1) + .for(creditsIds) + .process(async (vendorCreditId: number) => { + await this.deleteVendorCreditService.deleteVendorCredit(vendorCreditId); + }); + + if (results.errors && results.errors.length > 0) { + throw results.errors[0].raw; + } + } +} + diff --git a/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts b/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts new file mode 100644 index 000000000..4bdf864ac --- /dev/null +++ b/packages/server/src/modules/VendorCredit/ValidateBulkDeleteVendorCredits.service.ts @@ -0,0 +1,55 @@ +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, + ); + 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; + } + } +} + diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx index d45132c5a..fe4a242e2 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/ManualJournalsAlerts.tsx @@ -8,6 +8,10 @@ const JournalPublishAlert = React.lazy( () => import('@/containers/Alerts/ManualJournals/JournalPublishAlert'), ); +const JournalBulkDeleteAlert = React.lazy( + () => import('@/containers/Alerts/ManualJournals/JournalBulkDeleteAlert'), +); + /** * Manual journals alerts. */ @@ -15,4 +19,5 @@ const JournalPublishAlert = React.lazy( export default [ { name: 'journal-delete', component: JournalDeleteAlert }, { name: 'journal-publish', component: JournalPublishAlert }, + { name: 'journals-bulk-delete', component: JournalBulkDeleteAlert }, ]; diff --git a/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx b/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx index de5a9bbff..74e3dc902 100644 --- a/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx +++ b/packages/webapp/src/containers/Accounting/JournalsLanding/components.tsx @@ -102,13 +102,13 @@ export const StatusAccessor = (row) => { return ( - + - + diff --git a/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx b/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx index b495842b2..edaac4899 100644 --- a/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx +++ b/packages/webapp/src/containers/Accounts/AccountsActionsBar.tsx @@ -182,6 +182,7 @@ function AccountsActionsBar({ intent={Intent.DANGER} onClick={handleBulkDelete} /> +