mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 01:55:41 +00:00
feat(eims): register invoices with MoR EIMS and persist the outcome
Add manual single-invoice registration, verification and reconciliation. Nothing submits automatically; invoice creation is untouched. Sequencing uses a durable reservation. The counter is consumed and the holder recorded in a committed transaction before the request leaves the process, and the HTTP call runs outside every transaction. A counter is therefore never reused once an attempt begins, a crash mid-flight leaves the reservation standing instead of inviting a blind resubmission, and an ambiguous result blocks the whole system number rather than one invoice -- PreviousIrn is unknown, so any later document would chain to a stale IRN. Deterministic rejections (400/406/401/403) mark the invoice FAILED and clear the block. Timeouts and 5xx mark it UNKNOWN and keep it. Since /v1/verify takes an IRN we never received in that case, POST :id/eims/resolve is the exit: record the IRN confirmed in the MoR portal, or discard. A recorded IRN is verified against the gateway first and refused unless EIMS reports it against this invoice's document number. Business and tax configuration is validated locally before anything is locked, allocated or sent, so a missing tax code fails naming the exact environment variables instead of at the gateway. No tax value is defaulted. Filing gets its own permission (invoices:eims_register) rather than riding on invoices:export -- registration is irreversible at MoR and must not follow from the right to download a PDF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,44 @@ export interface EimsConfig {
|
||||
httpTimeoutMs: number;
|
||||
/** Re-authenticate this many ms before the access token actually expires. */
|
||||
tokenSkewMs: number;
|
||||
/**
|
||||
* Seller identity and tax/business treatment for the invoice document.
|
||||
*
|
||||
* None of this is derivable from the database: EDR's own legal identity exists nowhere in the
|
||||
* codebase, and the app models no tax at all. Values are required at registration time and are
|
||||
* validated there rather than at boot, so a deployment can run with EIMS enabled for
|
||||
* authentication before finance has signed off on the tax treatment.
|
||||
*/
|
||||
invoice: EimsInvoiceConfig;
|
||||
}
|
||||
|
||||
export interface EimsInvoiceConfig {
|
||||
sellerLegalName: string;
|
||||
sellerVatNumber: string;
|
||||
sellerPhone: string;
|
||||
sellerEmail: string;
|
||||
/** MoR *codes*, not names (e.g. "13" for Addis Ababa, "574"). */
|
||||
sellerRegion: string;
|
||||
sellerWereda: string;
|
||||
sellerCity: string | null;
|
||||
sellerSubCity: string | null;
|
||||
sellerHouseNumber: string | null;
|
||||
sellerLocality: string | null;
|
||||
/** REQUIRES_BUSINESS_CONFIRMATION — no tax model exists in this application. */
|
||||
taxCode: string;
|
||||
taxRatePercent: number | null;
|
||||
exciseTaxValue: number | null;
|
||||
incomeWithholdValue: number | null;
|
||||
transactionWithholdValue: number | null;
|
||||
/** B2B / B2C — a tax classification, so it is configured, not inferred. */
|
||||
transactionType: string;
|
||||
natureOfSupplies: string;
|
||||
paymentMode: string;
|
||||
paymentTerm: string;
|
||||
unitDefault: string;
|
||||
buyerCountryCode: string | null;
|
||||
cashierName: string | null;
|
||||
salesPersonName: string | null;
|
||||
}
|
||||
|
||||
const REQUIRED_VARS = [
|
||||
@@ -46,6 +84,14 @@ const positiveInt = (raw: string | undefined, fallback: number, name: string): n
|
||||
return value;
|
||||
};
|
||||
|
||||
/** Unset stays null so the registration-time check can name it; a set-but-bogus value throws. */
|
||||
const optionalNumber = (raw: string | undefined, name: string): number | null => {
|
||||
if (raw === undefined || raw === "") return null;
|
||||
const value = Number(raw);
|
||||
if (!Number.isFinite(value)) throw new Error(`${name} must be a number`);
|
||||
return value;
|
||||
};
|
||||
|
||||
export default registerAs("eims", (): EimsConfig => {
|
||||
const enabled = (process.env.EIMS_ENABLED ?? "false").toLowerCase() === "true";
|
||||
const baseUrl = (process.env.EIMS_BASE_URL ?? "https://core.mor.gov.et").replace(/\/+$/, "");
|
||||
@@ -66,6 +112,37 @@ export default registerAs("eims", (): EimsConfig => {
|
||||
certificatePath: process.env.EIMS_CERTIFICATE_PATH ?? "",
|
||||
httpTimeoutMs,
|
||||
tokenSkewMs,
|
||||
invoice: {
|
||||
sellerLegalName: process.env.EIMS_SELLER_LEGAL_NAME ?? "",
|
||||
sellerVatNumber: process.env.EIMS_SELLER_VAT_NUMBER ?? "",
|
||||
sellerPhone: process.env.EIMS_SELLER_PHONE ?? "",
|
||||
sellerEmail: process.env.EIMS_SELLER_EMAIL ?? "",
|
||||
sellerRegion: process.env.EIMS_SELLER_REGION ?? "",
|
||||
sellerWereda: process.env.EIMS_SELLER_WEREDA ?? "",
|
||||
sellerCity: process.env.EIMS_SELLER_CITY || null,
|
||||
sellerSubCity: process.env.EIMS_SELLER_SUBCITY || null,
|
||||
sellerHouseNumber: process.env.EIMS_SELLER_HOUSE_NUMBER || null,
|
||||
sellerLocality: process.env.EIMS_SELLER_LOCALITY || null,
|
||||
taxCode: process.env.EIMS_TAX_CODE ?? "",
|
||||
taxRatePercent: optionalNumber(process.env.EIMS_TAX_RATE_PERCENT, "EIMS_TAX_RATE_PERCENT"),
|
||||
exciseTaxValue: optionalNumber(process.env.EIMS_EXCISE_TAX_VALUE, "EIMS_EXCISE_TAX_VALUE"),
|
||||
incomeWithholdValue: optionalNumber(
|
||||
process.env.EIMS_INCOME_WITHHOLD_VALUE,
|
||||
"EIMS_INCOME_WITHHOLD_VALUE",
|
||||
),
|
||||
transactionWithholdValue: optionalNumber(
|
||||
process.env.EIMS_TRANSACTION_WITHHOLD_VALUE,
|
||||
"EIMS_TRANSACTION_WITHHOLD_VALUE",
|
||||
),
|
||||
transactionType: process.env.EIMS_TRANSACTION_TYPE ?? "",
|
||||
natureOfSupplies: process.env.EIMS_NATURE_OF_SUPPLIES ?? "",
|
||||
paymentMode: process.env.EIMS_PAYMENT_MODE ?? "",
|
||||
paymentTerm: process.env.EIMS_PAYMENT_TERM ?? "",
|
||||
unitDefault: process.env.EIMS_UNIT_DEFAULT ?? "",
|
||||
buyerCountryCode: process.env.EIMS_BUYER_COUNTRY_CODE || null,
|
||||
cashierName: process.env.EIMS_CASHIER_NAME || null,
|
||||
salesPersonName: process.env.EIMS_SALESPERSON_NAME || null,
|
||||
},
|
||||
};
|
||||
|
||||
if (!enabled) return base;
|
||||
|
||||
Reference in New Issue
Block a user