mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 13:28:11 +00:00
fix(eims): match MoR's payload rules found by live rejections
Three live attempts turned six guesses into facts. Each fix below is the
gateway's own words, not a reading of the collection.
DocumentNumber and InvoiceCounter move differently, because MoR constrains
them differently. The counter must not skip -- "Invoice counter is not
correct. expected : 1" -- so a definitively refused document hands it back.
The document number must not repeat, so the attempt burns it. Both stay spent
after an ambiguous result, where MoR may have stored the document.
NatureOfSupplies is normalised to MoR's exact lowercase constant and rejected
outright if it is neither 'goods' nor 'service'; its schema branches on this
as a oneOf, so "Service" invalidated the whole ItemList.
Buyer region resolves through a name->code map and now FAILS locally when
unmapped. MoR validates Region against ^[0-9]{1,3}$ on both the seller and
buyer sides, so a name can never be sent and a guessed code on a tax document
is worse than refusing to file.
Seller phone, email, region and wereda are checked against MoR's own regexes
before anything is sent, so a placeholder like "_" fails locally instead of
costing a request and a counter.
EIMS_TAX_CODE stays required and unset in .env.example: the choice between
VAT0 (zero-rated) and VATEX (exempt) is a tax position awaiting finance, and
MoR's enum is recorded there for whoever decides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,37 @@ export function assertEimsInvoiceConfig(config: EimsConfig): void {
|
||||
`(tax values need finance sign-off — they are deliberately not defaulted): ${missing.join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
assertSellerFormats(config.invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* MoR's own patterns for the seller fields, checked here rather than at the gateway.
|
||||
*
|
||||
* A placeholder like `_` is "set" but unfilable, and finding that out costs a real request and a
|
||||
* consumed counter — these are the exact regexes its 400 SCHEMA ERROR quoted back at us.
|
||||
*/
|
||||
const SELLER_FORMATS: { env: string; value: (i: EimsConfig["invoice"]) => string; pattern: RegExp }[] = [
|
||||
{ env: "EIMS_SELLER_PHONE", value: (i) => i.sellerPhone, pattern: /^\+?[0-9]{6,}$/ },
|
||||
{
|
||||
env: "EIMS_SELLER_EMAIL",
|
||||
value: (i) => i.sellerEmail,
|
||||
pattern: /^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$/,
|
||||
},
|
||||
{ env: "EIMS_SELLER_REGION", value: (i) => i.sellerRegion, pattern: /^[0-9]{1,3}$/ },
|
||||
{ env: "EIMS_SELLER_WEREDA", value: (i) => i.sellerWereda, pattern: /^[0-9A-Za-z]{1,10}$/ },
|
||||
];
|
||||
|
||||
function assertSellerFormats(invoice: EimsConfig["invoice"]): void {
|
||||
const bad = SELLER_FORMATS.filter(({ value, pattern }) => !pattern.test(value(invoice))).map(
|
||||
({ env, pattern }) => `${env} (must match ${pattern.source})`,
|
||||
);
|
||||
if (bad.length > 0) {
|
||||
throw new BadRequestException({
|
||||
code: "EIMS_INVOICE_CONFIG_INVALID",
|
||||
message: `EIMS seller details would be rejected by MoR: ${bad.join("; ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEimsSeller(config: EimsConfig): EimsSellerDetails {
|
||||
@@ -112,9 +143,7 @@ export function buildEimsContext(config: EimsConfig, input: EimsContextInput): E
|
||||
incomeWithholdValue: invoice.incomeWithholdValue!,
|
||||
transactionWithholdValue: invoice.transactionWithholdValue!,
|
||||
buyerCountryCode: invoice.buyerCountryCode,
|
||||
// companies.region is free text ("Addis Ababa"); MoR wants ^[0-9]{1,3}$. A stored value that
|
||||
// already looks like a code wins, otherwise the seller's own region stands in.
|
||||
buyerRegionFallback: invoice.buyerRegionFallback || invoice.sellerRegion,
|
||||
buyerRegionCodes: invoice.buyerRegionCodes,
|
||||
exchangeRate: input.exchangeRate ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user