fix: Add DELETE endpoint for credit notes applied invoices

- Add missing DELETE /credit-notes/applied-invoices/:id endpoint
- Fix CreditNotesApplyInvoice controller to use correct service methods
- Add missing GetCreditNoteAssociatedInvoicesToApply endpoint
- Add proper DTO for ApplyCreditNoteToInvoices
- Update frontend creditNote hook to use correct API paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Bouhuolia
2026-02-24 02:15:32 +02:00
parent 238b60144f
commit 2a87103bc8
8 changed files with 115 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import bluebird from 'bluebird'; import * as bluebird from 'bluebird';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
validateLinkModelEntryExists, validateLinkModelEntryExists,
@@ -53,7 +53,8 @@ export class LinkAttachment {
const foundLinkModel = await LinkModel().query(trx).findById(modelId); const foundLinkModel = await LinkModel().query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel); validateLinkModelEntryExists(foundLinkModel);
const foundLinks = await this.documentLinkModel().query(trx) const foundLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef) .where('modelRef', modelRef)
.where('modelId', modelId) .where('modelId', modelId)
.where('documentId', foundFile.id); .where('documentId', foundFile.id);

View File

@@ -1,6 +1,7 @@
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Post, Post,
@@ -14,6 +15,10 @@ import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard'; import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types'; import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types'; import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNoteApplyToInvoices } from './commands/CreditNoteApplyToInvoices.service';
import { DeleteCreditNoteApplyToInvoices } from './commands/DeleteCreditNoteApplyToInvoices.service';
import { ApplyCreditNoteToInvoicesDto } from './dtos/ApplyCreditNoteToInvoices.dto';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice') @ApiTags('Credit Notes Apply Invoice')
@@ -22,6 +27,9 @@ import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
export class CreditNotesApplyInvoiceController { export class CreditNotesApplyInvoiceController {
constructor( constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices, private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
) {} ) {}
@Get(':creditNoteId/applied-invoices') @Get(':creditNoteId/applied-invoices')
@@ -39,6 +47,23 @@ export class CreditNotesApplyInvoiceController {
); );
} }
@Get(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note associated invoices to apply' })
@ApiResponse({
status: 200,
description: 'Credit note associated invoices to apply',
})
@ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' })
getCreditNoteAssociatedInvoicesToApply(
@Param('creditNoteId') creditNoteId: number,
) {
return this.getCreditNoteAssociatedInvoicesToApplyService.getCreditAssociatedInvoicesToApply(
creditNoteId,
);
}
@Post(':creditNoteId/apply-invoices') @Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote) @RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' }) @ApiOperation({ summary: 'Apply credit note to invoices' })
@@ -48,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
}) })
@ApiResponse({ status: 404, description: 'Credit note not found' }) @ApiResponse({ status: 404, description: 'Credit note not found' })
@ApiResponse({ status: 400, description: 'Invalid input data' }) @ApiResponse({ status: 400, description: 'Invalid input data' })
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) { applyCreditNoteToInvoices(
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices( @Param('creditNoteId') creditNoteId: number,
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
) {
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
creditNoteId, creditNoteId,
applyDto,
);
}
@Delete('applied-invoices/:applyCreditToInvoicesId')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete applied credit note to invoice' })
@ApiResponse({
status: 200,
description: 'Credit note application successfully deleted',
})
@ApiResponse({
status: 404,
description: 'Credit note application not found',
})
deleteApplyCreditNoteToInvoices(
@Param('applyCreditToInvoicesId') applyCreditToInvoicesId: number,
) {
return this.deleteCreditNoteApplyToInvoicesService.deleteApplyCreditNoteToInvoices(
applyCreditToInvoicesId,
); );
} }
} }

View File

@@ -9,6 +9,8 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service'; import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service'; import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller'; import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
@Module({ @Module({
providers: [ providers: [
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
CreditNoteApplySyncCredit, CreditNoteApplySyncCredit,
GetCreditNoteAssociatedAppliedInvoices, GetCreditNoteAssociatedAppliedInvoices,
GetCreditNoteAssociatedInvoicesToApply, GetCreditNoteAssociatedInvoicesToApply,
CreditNoteApplySyncCreditSubscriber,
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
], ],
exports: [DeleteCustomerLinkedCreditNoteService], exports: [DeleteCustomerLinkedCreditNoteService],
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)], imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types'; import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';

View File

@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsInt,
IsNotEmpty,
IsNumber,
ValidateNested,
} from 'class-validator';
export class ApplyCreditNoteInvoiceEntryDto {
@IsNotEmpty()
@IsInt()
@ApiProperty({ description: 'Invoice ID to apply credit to', example: 1 })
invoiceId: number;
@IsNotEmpty()
@IsNumber()
@ApiProperty({ description: 'Amount to apply', example: 100.5 })
amount: number;
}
export class ApplyCreditNoteToInvoicesDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => ApplyCreditNoteInvoiceEntryDto)
@ApiProperty({
description: 'Entries of invoice ID and amount to apply',
type: [ApplyCreditNoteInvoiceEntryDto],
example: [
{ invoice_id: 1, amount: 100.5 },
{ invoice_id: 2, amount: 50 },
],
})
entries: ApplyCreditNoteInvoiceEntryDto[];
}

View File

@@ -8,7 +8,7 @@ import { CreditNoteApplySyncInvoicesCreditedAmount } from '../commands/CreditNot
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
@Injectable() @Injectable()
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber { export class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
constructor( constructor(
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount, private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
) {} ) {}

View File

@@ -1,4 +1,4 @@
import Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types'; import { IVendorCreditAppliedBill } from '../types/VendorCreditApplyBills.types';

View File

@@ -58,16 +58,13 @@ export function useCreateCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((values) => apiRequest.post('credit-notes', values), {
(values) => apiRequest.post('credit-notes', values),
{
onSuccess: (res, values) => { onSuccess: (res, values) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -218,8 +215,7 @@ export function useCreateRefundCreditNote(props) {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
([id, values]) => ([id, values]) => apiRequest.post(`credit-notes/${id}/refunds`, values),
apiRequest.post(`credit-notes/${id}/refunds`, values),
{ {
onSuccess: (res, [id, values]) => { onSuccess: (res, [id, values]) => {
// Common invalidate queries. // Common invalidate queries.
@@ -240,9 +236,7 @@ export function useDeleteRefundCreditNote(props) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation((id) => apiRequest.delete(`credit-notes/refunds/${id}`), {
(id) => apiRequest.delete(`credit-notes/refunds/${id}`),
{
onSuccess: (res, id) => { onSuccess: (res, id) => {
// Common invalidate queries. // Common invalidate queries.
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
@@ -251,8 +245,7 @@ export function useDeleteRefundCreditNote(props) {
queryClient.invalidateQueries([t.CREDIT_NOTE, id]); queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
}, },
...props, ...props,
}, });
);
} }
/** /**
@@ -301,7 +294,7 @@ export function useReconcileCreditNote(id, props, requestProps) {
[t.RECONCILE_CREDIT_NOTE, id], [t.RECONCILE_CREDIT_NOTE, id],
{ {
method: 'get', method: 'get',
url: `credit-notes/${id}/applied-invoices`, url: `credit-notes/${id}/apply-invoices`,
...requestProps, ...requestProps,
}, },
{ {