Files
musadeq/backend/src/modules/invoices/ubl-generator.service.ts

94 lines
4.5 KiB
TypeScript

/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — UBL 2.1 Generator Service
* ════════════════════════════════════════════════════════════
* يقوم بإنشاء ملفات XML المتوافقة مع معيار UBL 2.1 المطلوبة من
* دائرة ضريبة الدخل والمبيعات الأردنية (ISTD).
* ════════════════════════════════════════════════════════════
*/
import { Injectable } from '@nestjs/common';
import { create } from 'xmlbuilder2';
import { Invoice } from './entities/invoice.entity';
@Injectable()
export class UBLGeneratorService {
/**
* توليد UBL 2.1 XML لفاتورة مبيعات
*/
generateXML(invoice: Invoice, company: any): string {
const doc = create({ version: '1.0', encoding: 'UTF-8' })
.ele('Invoice', {
xmlns: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'xmlns:cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'xmlns:cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
})
.ele('cbc:UBLVersionID').txt('2.1').up()
.ele('cbc:CustomizationID').txt('TRX-1.0').up()
.ele('cbc:ID').txt(invoice.invoice_number || 'N/A').up()
.ele('cbc:IssueDate').txt(invoice.invoice_date?.toISOString().split('T')[0] || '').up()
.ele('cbc:InvoiceTypeCode').txt(invoice.ubl_type_code).up()
.ele('cbc:DocumentCurrencyCode').txt(invoice.currency_code).up()
// ── AccountingSupplierParty (المُصدر) ───────────────
.ele('cac:AccountingSupplierParty')
.ele('cac:Party')
.ele('cac:PartyIdentification')
.ele('cbc:ID').txt(company.tax_identification_number).up()
.up()
.ele('cac:PartyName')
.ele('cbc:Name').txt(company.name).up()
.up()
.ele('cac:PostalAddress')
.ele('cbc:StreetName').txt(company.address || '').up()
.ele('cac:Country')
.ele('cbc:IdentificationCode').txt('JO').up()
.up()
.up()
.up()
.up()
// ── AccountingCustomerParty (المشتري) ───────────────
.ele('cac:AccountingCustomerParty')
.ele('cac:Party')
.ele('cac:PartyIdentification')
.ele('cbc:ID').txt(invoice.buyer_tin || invoice.buyer_national_id || '').up()
.up()
.ele('cac:PartyName')
.ele('cbc:Name').txt(invoice.buyer_name || '').up()
.up()
.up()
.up()
// ── TaxTotal ───────────────────────────────────────
.ele('cac:TaxTotal')
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt(invoice.tax_amount.toString()).up()
.up()
// ── LegalMonetaryTotal ─────────────────────────────
.ele('cac:LegalMonetaryTotal')
.ele('cbc:LineExtensionAmount', { currencyID: invoice.currency_code }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxExclusiveAmount', { currencyID: invoice.currency_code }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxInclusiveAmount', { currencyID: invoice.currency_code }).txt(invoice.grand_total.toString()).up()
.ele('cbc:PayableAmount', { currencyID: invoice.currency_code }).txt(invoice.grand_total.toString()).up()
.up();
// ── InvoiceLines ─────────────────────────────────────
invoice.lines.forEach((line) => {
doc.ele('cac:InvoiceLine')
.ele('cbc:ID').txt(line.line_number.toString()).up()
.ele('cbc:InvoicedQuantity', { unitCode: 'PCE' }).txt(line.quantity.toString()).up()
.ele('cbc:LineExtensionAmount', { currencyID: invoice.currency_code }).txt(line.line_total.toString()).up()
.ele('cac:Item')
.ele('cbc:Description').txt(line.description).up()
.up()
.ele('cac:Price')
.ele('cbc:PriceAmount', { currencyID: invoice.currency_code }).txt(line.unit_price.toString()).up()
.up()
.up();
});
return doc.end({ prettyPrint: true });
}
}