import { BadRequestException } from "@nestjs/common"; import { EimsConfig } from "../../config/eims.config"; import { EimsSessionContext } from "./eims-auth.service"; import { EimsMapperContext, EimsMapperLine, EimsSellerDetails, } from "../billing/eims-invoice.mapper"; /** * Turns configuration into the seller identity and mapper context that `toEimsInvoice` requires. * * Everything here is unavailable from the database by construction: EDR's own legal identity is not * modelled anywhere, and the application has no tax model at all (`invoice.taxAmount` is always 0, * `invoice_lines` and the rate catalogue carry no fiscal columns). Rather than defaulting any of it, * a missing value fails **here** — locally, before a single byte reaches the gateway — naming the * exact environment variables to set. */ interface RequiredSpec { env: string; value: string | number | null | undefined; } // `systemNumber` / `systemType` are absent by design: they come from the access token, which is // MoR's own statement of who we are. See EimsAuthService.getSessionContext. const REQUIRED = (invoice: EimsConfig["invoice"], tin: string): RequiredSpec[] => [ { env: "EIMS_TIN", value: tin }, { env: "EIMS_SELLER_LEGAL_NAME", value: invoice.sellerLegalName }, { env: "EIMS_SELLER_VAT_NUMBER", value: invoice.sellerVatNumber }, { env: "EIMS_SELLER_PHONE", value: invoice.sellerPhone }, { env: "EIMS_SELLER_EMAIL", value: invoice.sellerEmail }, { env: "EIMS_SELLER_REGION", value: invoice.sellerRegion }, { env: "EIMS_SELLER_WEREDA", value: invoice.sellerWereda }, { env: "EIMS_TAX_CODE", value: invoice.taxCode }, { env: "EIMS_TAX_RATE_PERCENT", value: invoice.taxRatePercent }, { env: "EIMS_INCOME_WITHHOLD_VALUE", value: invoice.incomeWithholdValue }, { env: "EIMS_TRANSACTION_WITHHOLD_VALUE", value: invoice.transactionWithholdValue }, { env: "EIMS_TRANSACTION_TYPE", value: invoice.transactionType }, { env: "EIMS_NATURE_OF_SUPPLIES", value: invoice.natureOfSupplies }, { env: "EIMS_PAYMENT_MODE", value: invoice.paymentMode }, { env: "EIMS_PAYMENT_TERM", value: invoice.paymentTerm }, { env: "EIMS_UNIT_DEFAULT", value: invoice.unitDefault }, ]; /** Throws naming every unset variable at once, so one round trip fixes the whole configuration. */ export function assertEimsInvoiceConfig(config: EimsConfig): void { const missing = REQUIRED(config.invoice, config.tin) .filter(({ value }) => value === null || value === undefined || value === "") .map(({ env }) => env); if (missing.length > 0) { throw new BadRequestException({ code: "EIMS_INVOICE_CONFIG_INCOMPLETE", message: "EIMS invoice registration is not configured. Set these environment variables " + `(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 { const { invoice } = config; return { City: invoice.sellerCity, Email: invoice.sellerEmail, HouseNumber: invoice.sellerHouseNumber, LegalName: invoice.sellerLegalName, Locality: invoice.sellerLocality, Phone: invoice.sellerPhone, Region: invoice.sellerRegion, SubCity: invoice.sellerSubCity, Tin: config.tin, VatNumber: invoice.sellerVatNumber, Wereda: invoice.sellerWereda, }; } export interface EimsContextInput { /** `DocumentDetails.DocumentNumber`. The caller decides its source. */ documentNumber: string; invoiceCounter: number; previousIrn: string | null; /** Source-system identity from the access token, never from configuration. */ session: EimsSessionContext; /** Required when the invoice currency is not ETB. */ exchangeRate?: number | null; } export function buildEimsContext(config: EimsConfig, input: EimsContextInput): EimsMapperContext { const { invoice } = config; // Validated by assertEimsInvoiceConfig; the non-null assertions below are safe after that call. const taxCode = invoice.taxCode; const ratePercent = invoice.taxRatePercent!; const exciseTaxValue = invoice.exciseTaxValue ?? 0; return { systemNumber: input.session.systemNumber, systemType: input.session.systemType, documentNumber: input.documentNumber, invoiceCounter: input.invoiceCounter, previousIrn: input.previousIrn, cashierName: invoice.cashierName, salesPersonName: invoice.salesPersonName, transactionType: invoice.transactionType, payment: { mode: invoice.paymentMode, term: invoice.paymentTerm }, // One treatment for every line today. The mapper resolves tax per line, so a future // charge-type-specific rule slots in here without touching the mapper. taxForLine: (_line: EimsMapperLine) => ({ code: taxCode, ratePercent, exciseTaxValue }), natureOfSupplies: invoice.natureOfSupplies, unitDefault: invoice.unitDefault, incomeWithholdValue: invoice.incomeWithholdValue!, transactionWithholdValue: invoice.transactionWithholdValue!, buyerCountryCode: invoice.buyerCountryCode, buyerRegionCodes: invoice.buyerRegionCodes, exchangeRate: input.exchangeRate ?? null, }; }