mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
feat(billing): render EIMS documents in the MoR tax-document layout
This commit is contained in:
@@ -3,6 +3,7 @@ import { EimsConfig } from "../../config/eims.config";
|
||||
import { MorGeoCodes } from "../../config/mor-location.resolver";
|
||||
import { EimsSessionContext } from "./eims-auth.service";
|
||||
import {
|
||||
EimsLineTax,
|
||||
EimsMapperContext,
|
||||
EimsMapperLine,
|
||||
EimsSellerDetails,
|
||||
@@ -172,12 +173,35 @@ export interface EimsContextInput {
|
||||
relatedDocument?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax treatment of one charge type: its per-`chargeType` override when one is configured
|
||||
* (validated symmetric in `assertChargeTypeOverrides`), else the single invoice-wide default.
|
||||
*
|
||||
* Exported because the printed tax document has to state the same Tax Code, Excise and Discount
|
||||
* per line that was filed with MoR, and it must be able to do so without a live EIMS session —
|
||||
* `buildEimsContext` needs a system number from an access token, printing does not.
|
||||
*/
|
||||
export function resolveLineTax(config: EimsConfig, chargeType: string): EimsLineTax {
|
||||
const { invoice } = config;
|
||||
return {
|
||||
code: invoice.taxCodeByChargeType[chargeType] ?? invoice.taxCode,
|
||||
ratePercent:
|
||||
chargeType in invoice.taxRateByChargeType
|
||||
? Number(invoice.taxRateByChargeType[chargeType])
|
||||
: invoice.taxRatePercent!,
|
||||
exciseTaxValue:
|
||||
chargeType in invoice.exciseByChargeType
|
||||
? Number(invoice.exciseByChargeType[chargeType])
|
||||
: (invoice.exciseTaxValue ?? 0),
|
||||
discount:
|
||||
chargeType in invoice.discountByChargeType
|
||||
? Number(invoice.discountByChargeType[chargeType])
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -191,23 +215,7 @@ export function buildEimsContext(config: EimsConfig, input: EimsContextInput): E
|
||||
payment: { mode: invoice.paymentMode, term: invoice.paymentTerm },
|
||||
// Per-`chargeType` override when one is configured (validated symmetric in
|
||||
// assertChargeTypeOverrides), else the single invoice-wide default.
|
||||
taxForLine: (line: EimsMapperLine) => {
|
||||
const { chargeType } = line;
|
||||
const code = invoice.taxCodeByChargeType[chargeType] ?? taxCode;
|
||||
const rate =
|
||||
chargeType in invoice.taxRateByChargeType
|
||||
? Number(invoice.taxRateByChargeType[chargeType])
|
||||
: ratePercent;
|
||||
const excise =
|
||||
chargeType in invoice.exciseByChargeType
|
||||
? Number(invoice.exciseByChargeType[chargeType])
|
||||
: exciseTaxValue;
|
||||
const discount =
|
||||
chargeType in invoice.discountByChargeType
|
||||
? Number(invoice.discountByChargeType[chargeType])
|
||||
: 0;
|
||||
return { code, ratePercent: rate, exciseTaxValue: excise, discount };
|
||||
},
|
||||
taxForLine: (line: EimsMapperLine) => resolveLineTax(config, line.chargeType),
|
||||
natureOfSupplies: invoice.natureOfSupplies,
|
||||
unitDefault: invoice.unitDefault,
|
||||
incomeWithholdValue: invoice.incomeWithholdValue!,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { EimsConfig } from "../../config/eims.config";
|
||||
import { Invoice } from "../billing/entities/invoice.entity";
|
||||
import {
|
||||
InvoiceDocumentModel,
|
||||
MorPartyDetails,
|
||||
pngDataUrl,
|
||||
} from "../billing/documents/invoice-document.service";
|
||||
import { buildEimsSeller } from "./eims-invoice-context";
|
||||
import { EimsReceipt, EimsReceiptStatus } from "./entities/eims-receipt.entity";
|
||||
import { EimsSalesReceiptRequest, EimsWithholdReceiptRequest } from "./eims-receipt.types";
|
||||
|
||||
@@ -20,7 +23,11 @@ import { EimsSalesReceiptRequest, EimsWithholdReceiptRequest } from "./eims-rece
|
||||
* would read as a genuine tax document. Callers (`EimsReceiptService.document`) let this throw
|
||||
* surface as a 400 — there is nothing sensible to render instead.
|
||||
*/
|
||||
export function toReceiptDocumentModel(receipt: EimsReceipt, invoice: Invoice): InvoiceDocumentModel {
|
||||
export function toReceiptDocumentModel(
|
||||
receipt: EimsReceipt,
|
||||
invoice: Invoice,
|
||||
config?: EimsConfig,
|
||||
): InvoiceDocumentModel {
|
||||
if (receipt.status !== EimsReceiptStatus.Registered) {
|
||||
throw new Error(
|
||||
`Receipt ${receipt.receiptNumber} is ${receipt.status}, not REGISTERED — refusing to print an unfiled receipt.`,
|
||||
@@ -45,6 +52,34 @@ export function toReceiptDocumentModel(receipt: EimsReceipt, invoice: Invoice):
|
||||
// if that default changes for an unrelated reason.
|
||||
sealText: "EDR PAID",
|
||||
extraSummary: [{ label: "Mode of payment", value: req.TransactionDetails.ModeOfPayment }],
|
||||
mor: config?.invoice
|
||||
? {
|
||||
titleAm: "የገንዘብ መቀበያ ደረሰኝ",
|
||||
titleEn: "Cash Receipt Voucher",
|
||||
saleType: config.invoice.transactionType,
|
||||
systemNumber: req.SourceSystemNumber || config.systemNumber || null,
|
||||
...parties(config, invoice),
|
||||
payment: {
|
||||
mode: req.TransactionDetails.ModeOfPayment,
|
||||
typeMethod: config.invoice.paymentTerm,
|
||||
receiverName: invoice.company?.name ?? null,
|
||||
},
|
||||
receipt: {
|
||||
rrn: receipt.rrn ?? "",
|
||||
reason: req.Reason,
|
||||
collectedAmount: req.CollectedAmount,
|
||||
// One row per invoice the payment covers — MoR's receipt is invoice-linked, so the
|
||||
// printed voucher has to show which document(s) the money was applied to.
|
||||
invoices: req.Invoices.map((line) => ({
|
||||
irn: line.InvoiceIRN,
|
||||
paymentCoverage: line.PaymentCoverage,
|
||||
totalAmount: line.TotalAmount,
|
||||
remainingAmount: line.RemainingAmount ?? 0,
|
||||
paidAmount: line.InvoicePaidAmount,
|
||||
})),
|
||||
},
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,9 +94,63 @@ export function toReceiptDocumentModel(receipt: EimsReceipt, invoice: Invoice):
|
||||
// wrong here, so this is the one case that MUST override it.
|
||||
sealText: "EDR",
|
||||
extraSummary: [{ label: "Withholding type", value: req.WithholdDetail.Type }],
|
||||
mor: config?.invoice
|
||||
? {
|
||||
titleAm: "ከተከፋይ ሒሳብ ላይ ለተቀነሰ ግብር የተሰጠ ደረሰኝ",
|
||||
titleEn: "Withholding tax on payment",
|
||||
...parties(config, invoice),
|
||||
systemNumber: req.SourceSystemNumber || config.systemNumber || null,
|
||||
withholding: {
|
||||
receiptNumber: receipt.receiptNumber,
|
||||
counter: req.ReceiptCounter,
|
||||
reason: req.Reason,
|
||||
type: req.WithholdDetail.Type,
|
||||
invoiceCurrency: req.InvoiceDetail.Currency,
|
||||
preTaxAmount: req.WithholdDetail.PreTaxAmount,
|
||||
withheldAmount: req.WithholdDetail.WithholdingAmount,
|
||||
systemType: req.SourceSystemType,
|
||||
systemNumber: req.SourceSystemNumber || config.systemNumber || "",
|
||||
},
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* `ከ / From` and `ለ / To` for a receipt. On a withholding receipt the seller is the withholding
|
||||
* agent and the buyer the taxpayer, which is the same pair of blocks in the same order — the
|
||||
* layout relabels them, so the mapping does not change.
|
||||
*/
|
||||
function parties(
|
||||
config: EimsConfig,
|
||||
invoice: Invoice,
|
||||
): { seller: MorPartyDetails; buyer: MorPartyDetails } {
|
||||
const seller = buildEimsSeller(config);
|
||||
const company = invoice.company;
|
||||
return {
|
||||
seller: {
|
||||
name: config.invoice.sellerLegalName || seller.LegalName,
|
||||
city: seller.City,
|
||||
subCity: seller.SubCity,
|
||||
woreda: seller.Wereda,
|
||||
kebele: seller.Locality,
|
||||
houseNo: seller.HouseNumber,
|
||||
tin: seller.Tin,
|
||||
vatNumber: seller.VatNumber,
|
||||
},
|
||||
buyer: {
|
||||
name: company?.name ?? "N/A",
|
||||
city: company?.zone ?? null,
|
||||
subCity: company?.zone ?? null,
|
||||
woreda: company?.woreda ?? null,
|
||||
kebele: company?.kebele ?? null,
|
||||
houseNo: company?.houseNo ?? null,
|
||||
tin: company?.tin ?? null,
|
||||
vatNumber: company?.vatNumber ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function build(
|
||||
receipt: EimsReceipt,
|
||||
invoice: Invoice,
|
||||
@@ -73,6 +162,7 @@ function build(
|
||||
amount: number;
|
||||
sealText: string;
|
||||
extraSummary: Array<{ label: string; value: string | null }>;
|
||||
mor?: InvoiceDocumentModel["mor"];
|
||||
},
|
||||
): InvoiceDocumentModel {
|
||||
return {
|
||||
|
||||
@@ -196,7 +196,7 @@ export class EimsReceiptService {
|
||||
|
||||
let model: ReturnType<typeof toReceiptDocumentModel>;
|
||||
try {
|
||||
model = toReceiptDocumentModel(receipt, invoice);
|
||||
model = toReceiptDocumentModel(receipt, invoice, this.cfg);
|
||||
} catch (err) {
|
||||
// Only the mapper's own refusals (not-yet-registered, missing request body) become a 400 —
|
||||
// a genuine PDF-render failure below is left to surface as whatever InvoiceDocumentService
|
||||
|
||||
Reference in New Issue
Block a user