mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 22:53:26 +00:00
Credit and Debt plus reason attribute inide register
This commit is contained in:
@@ -157,6 +157,12 @@ export interface EimsContextInput {
|
||||
session: EimsSessionContext;
|
||||
/** Required when the invoice currency is not ETB. */
|
||||
exchangeRate?: number | null;
|
||||
/** `DocumentDetails.Type` — defaults to "INV" in the mapper when omitted. */
|
||||
documentType?: EimsMapperContext["documentType"];
|
||||
/** Required (by the mapper) when documentType is DEB/CRE. */
|
||||
reason?: string | null;
|
||||
/** `ReferenceDetails.RelatedDocument` — the original invoice's IRN, required for DEB/CRE. */
|
||||
relatedDocument?: string | null;
|
||||
}
|
||||
|
||||
export function buildEimsContext(config: EimsConfig, input: EimsContextInput): EimsMapperContext {
|
||||
@@ -206,5 +212,8 @@ export function buildEimsContext(config: EimsConfig, input: EimsContextInput): E
|
||||
buyerIdType: invoice.buyerIdType,
|
||||
buyerIdNumber: invoice.buyerIdNumber,
|
||||
exchangeRate: input.exchangeRate ?? null,
|
||||
documentType: input.documentType,
|
||||
reason: input.reason ?? null,
|
||||
relatedDocument: input.relatedDocument ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,6 +295,58 @@ describe("EimsInvoiceRegistrationService.registerInvoiceWithEims", () => {
|
||||
expect(request.SourceSystem.SystemNumber).toBe(SYSTEM_NUMBER);
|
||||
});
|
||||
|
||||
it("files a credit note with Type/Reason/RelatedDocument from the invoice row", async () => {
|
||||
const original = invoiceRow({
|
||||
id: "original-invoice",
|
||||
invoiceNumber: "INV-20260807-00001",
|
||||
eimsIrn: IRN,
|
||||
});
|
||||
const db = new FakeDb([
|
||||
invoiceRow({
|
||||
eimsDocumentType: "CRE",
|
||||
eimsReason: "Overbilled freight charge",
|
||||
relatedInvoice: original,
|
||||
} as Partial<Invoice>),
|
||||
]);
|
||||
const postSigned = jest.fn().mockResolvedValue(okResponse());
|
||||
|
||||
await build(db, postSigned).registerInvoiceWithEims(INVOICE_ID);
|
||||
|
||||
const request = postSigned.mock.calls[0][1] as EimsInvoiceRequest;
|
||||
expect(request.DocumentDetails).toMatchObject({ Type: "CRE", Reason: "Overbilled freight charge" });
|
||||
expect(request.ReferenceDetails.RelatedDocument).toBe(IRN);
|
||||
});
|
||||
|
||||
it("refuses a credit/debit note whose related invoice was never registered, before touching a counter", async () => {
|
||||
const original = invoiceRow({ id: "original-invoice", eimsIrn: null });
|
||||
const db = new FakeDb([
|
||||
invoiceRow({
|
||||
eimsDocumentType: "DEB",
|
||||
eimsReason: "Additional handling",
|
||||
relatedInvoice: original,
|
||||
} as Partial<Invoice>),
|
||||
]);
|
||||
const postSigned = jest.fn();
|
||||
|
||||
await expect(build(db, postSigned).registerInvoiceWithEims(INVOICE_ID)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(postSigned).not.toHaveBeenCalled();
|
||||
expect(db.state).toMatchObject({ nextInvoiceCounter: 7 }); // unchanged — never reserved
|
||||
});
|
||||
|
||||
it("refuses a credit/debit note with no related invoice set at all", async () => {
|
||||
const db = new FakeDb([
|
||||
invoiceRow({ eimsDocumentType: "CRE", eimsReason: "x", relatedInvoice: null } as Partial<Invoice>),
|
||||
]);
|
||||
const postSigned = jest.fn();
|
||||
|
||||
await expect(build(db, postSigned).registerInvoiceWithEims(INVOICE_ID)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(postSigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("takes SourceSystem from the token session, not from configuration", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postSigned = jest.fn().mockResolvedValue(okResponse());
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialE
|
||||
import { EimsConfig } from "../../config/eims.config";
|
||||
import { Invoice } from "../billing/entities/invoice.entity";
|
||||
import {
|
||||
EimsDocumentType,
|
||||
EimsInvoiceRequest,
|
||||
EimsMapperLine,
|
||||
toEimsInvoice,
|
||||
@@ -96,6 +97,27 @@ export class EimsInvoiceRegistrationService {
|
||||
const invoice = await this.loadInvoiceForMapping(invoiceId);
|
||||
if (invoice.eimsIrn) return this.toView(invoice);
|
||||
|
||||
// Debit/credit notes (confirmed by MoR support: same endpoint, Type DEB/CRE + Reason,
|
||||
// ReferenceDetails.RelatedDocument = the original's IRN) must fail here — before a counter is
|
||||
// touched — if the original was never actually registered.
|
||||
const documentType = (invoice.eimsDocumentType as EimsDocumentType | undefined) ?? "INV";
|
||||
let relatedDocument: string | null = null;
|
||||
if (documentType !== "INV") {
|
||||
if (!invoice.relatedInvoice) {
|
||||
throw new BadRequestException({
|
||||
code: "EIMS_RELATED_INVOICE_REQUIRED",
|
||||
message: `Invoice ${invoice.invoiceNumber} is a ${documentType} but has no related invoice set.`,
|
||||
});
|
||||
}
|
||||
if (!invoice.relatedInvoice.eimsIrn) {
|
||||
throw new BadRequestException({
|
||||
code: "EIMS_RELATED_INVOICE_NOT_REGISTERED",
|
||||
message: `Invoice ${invoice.invoiceNumber} is a ${documentType} against invoice ${invoice.relatedInvoice.invoiceNumber}, which was never registered with EIMS — nothing to reference.`,
|
||||
});
|
||||
}
|
||||
relatedDocument = invoice.relatedInvoice.eimsIrn;
|
||||
}
|
||||
|
||||
// Authenticate before reserving: the source system comes from the token, and the state row is
|
||||
// keyed by it. A login failure here costs nothing — no counter has been consumed yet.
|
||||
const session = await this.auth.getSessionContext();
|
||||
@@ -114,6 +136,9 @@ export class EimsInvoiceRegistrationService {
|
||||
invoiceCounter: reservation.invoiceCounter,
|
||||
previousIrn: reservation.previousIrn,
|
||||
session,
|
||||
documentType,
|
||||
reason: invoice.eimsReason,
|
||||
relatedDocument,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -639,7 +664,7 @@ export class EimsInvoiceRegistrationService {
|
||||
): Promise<Invoice & { lines: EimsMapperLine[] }> {
|
||||
const invoice = await this.dataSource.getRepository(Invoice).findOne({
|
||||
where: { id: invoiceId },
|
||||
relations: { company: true, companyProfile: true },
|
||||
relations: { company: true, companyProfile: true, relatedInvoice: true },
|
||||
});
|
||||
if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user