Skip to content

fix(billing): EN16931 XML remaining gaps — payment means, tax categories, document-level charges (#158)#160

Open
b3lz3but wants to merge 1 commit intocaptainpragmatic:masterfrom
b3lz3but:fix/en16931-xml-remaining-gaps
Open

fix(billing): EN16931 XML remaining gaps — payment means, tax categories, document-level charges (#158)#160
b3lz3but wants to merge 1 commit intocaptainpragmatic:masterfrom
b3lz3but:fix/en16931-xml-remaining-gaps

Conversation

@b3lz3but
Copy link
Copy Markdown
Contributor

@b3lz3but b3lz3but commented Mar 26, 2026

Summary

Closes the 4 remaining EN16931 compliance gaps in the e-Factura XML builder:

  • Payment means code (BT-81): Derive UNCL4461 code from Payment.payment_method instead of hardcoding 30. Maps: stripe→48, bank→30, paypal→68, cash→10
  • Tax exemption handling (BT-120/BT-121): Add exemption reason text for non-standard categories (AE reverse charge, E exempt, K intra-community, O not subject, Z zero-rated). Also detect K and O categories from customer data
  • Document-level allowances/charges (BG-20/BG-21): Read from invoice.meta['allowances'] and invoice.meta['charges']. Include AllowanceTotalAmount/ChargeTotalAmount in LegalMonetaryTotal, adjust TaxExclusiveAmount
  • Payment terms (BT-20): Include explicit due date in structured note text

Files changed (1)

services/platform/apps/billing/efactura/xml_builder.py — +164/-13

Test plan

  • All 20 existing XML builder tests pass
  • Verify payment means code changes to 48 when invoice has a Stripe payment
  • Verify reverse charge invoice includes TaxExemptionReason in XML
  • Verify document-level allowance appears in XML when meta['allowances'] is populated
  • Verify payment terms note includes due date string

Closes #158

🤖 Generated with Claude Code

@b3lz3but
Copy link
Copy Markdown
Contributor Author

@mostlyvirtual — this PR is ready for review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves Romanian e-Factura (EN16931/CIUS-RO) XML generation by adding payment means mapping, structured payment terms, tax exemption reason elements, and document-level allowance/charge support; also includes a small hardening in SES webhook signature verification and test updates.

Changes:

  • Extend e-Factura XML builder with payment means codes, tax exemption reason output, document-level allowances/charges, and structured payment terms notes.
  • Update portal login lockout tests to execute through the real HMAC middleware test stack.
  • Harden SES SNS signature verification by rejecting non-RSA public keys; adjust an orders edit test expectation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
services/platform/apps/billing/efactura/xml_builder.py Adds EN16931-related XML fields (payment means, tax exemption reasons, allowances/charges, payment terms) and adjusts monetary totals.
services/platform/apps/notifications/webhooks.py Adds RSA public-key type check before verifying AWS SNS signatures for SES webhooks.
services/platform/tests/api/test_api_users_security.py Runs portal login lockout tests through HMAC middleware with dedicated test helpers/settings.
services/platform/tests/orders/test_orders_edit.py Updates draft editable-fields test to assert against Order._SAFE_DRAFT_FIELDS.
Comments suppressed due to low confidence (1)

services/platform/apps/billing/efactura/xml_builder.py:735

  • Document-level allowances/charges are subtracted/added into TaxExclusiveAmount, but TaxTotal/TaxableAmount, TaxTotal/TaxAmount, TaxInclusiveAmount, and PayableAmount still come from the unadjusted invoice fields. If meta['allowances'] / meta['charges'] are populated, the totals in the XML will become internally inconsistent and may fail CIUS-RO/EN16931 validation. Update the calculation flow so document-level allowance/charge amounts (and their taxes) are reflected consistently across TaxTotal and LegalMonetaryTotal, or ensure these meta amounts are already included in invoice.subtotal_cents/tax_total_cents/total_cents before building the XML.
        # Document-level allowances/charges (BT-107/BT-108)
        allowance_total, charge_total = self._get_document_level_totals()
        if allowance_total > 0:
            allow_elem = self._add_cbc(monetary_total, "AllowanceTotalAmount", self._format_amount(allowance_total))
            allow_elem.set("currencyID", currency)
        if charge_total > 0:
            charge_elem = self._add_cbc(monetary_total, "ChargeTotalAmount", self._format_amount(charge_total))
            charge_elem.set("currencyID", currency)

        # Tax Exclusive Amount (line extension - allowances + charges)
        tax_exclusive = subtotal - allowance_total + charge_total
        tax_excl = self._add_cbc(monetary_total, "TaxExclusiveAmount", self._format_amount(tax_exclusive))
        tax_excl.set("currencyID", currency)

        # Tax Inclusive Amount (total with tax)
        total = Decimal(self.invoice.total_cents or 0) / 100
        tax_incl = self._add_cbc(monetary_total, "TaxInclusiveAmount", self._format_amount(total))
        tax_incl.set("currencyID", currency)

        # Payable Amount (amount to be paid)
        payable = self._add_cbc(monetary_total, "PayableAmount", self._format_amount(total))
        payable.set("currencyID", currency)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@mostlyvirtual mostlyvirtual left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: REWORK — 2 blocking EN16931 compliance issues + merge conflict

Must fix before merge (blocking)

1. `TaxExemptionReasonCode` emits category code instead of VATEX code (BT-121)
`xml_builder.py:639,988` — Emits `"AE"`, `"E"`, `"K"` etc. into `TaxExemptionReasonCode`, but BT-121 requires VATEX codes (`"VATEX-EU-AE"`, `"VATEX-EU-IC"`, etc.). ANAF Schematron will reject every non-standard-rate invoice. Either map to proper VATEX codes or omit BT-121 (it's optional when BT-120 free-text reason is present, which it is).

2. `AllowanceChargeReasonCode "FC"` invalid for document charges
`xml_builder.py:673` — `"FC"` is not in UNCL4465 code list. Use `"ZZZ"` (mutually defined) or accept from meta: `charge.get("reason_code", "ZZZ")`. BIS Billing 3.0 rule UBL-SR-44 will reject this.

3. Merge conflict from 3 unrelated files
`webhooks.py`, `test_api_users_security.py`, `test_orders_edit.py` are already on master. Reset these to master versions before merging: `git checkout master -- `.

Should fix

4. `_get_tax_category` classifies non-EU customers as reverse charge
`xml_builder.py:265` — `customer.country_code != "RO" and customer.tax_id` returns `AE` for US/GB customers with tax IDs. Need EU country code check.

5. Credit note `unitCode` hardcoded to `"C62"`
`xml_builder.py:1026` — Invoice builder uses `_get_unit_code(line)` but credit note builder always emits `"C62"` (piece). EN16931 BT-130 requires credited quantity unit to match invoiced unit.

6. Unvalidated JSON meta in allowances/charges
`xml_builder.py:645-683` — `meta["allowances"]` / `meta["charges"]` read without schema validation. Invalid `amount_cents` crashes with `InvalidOperation`, missing fields silently produce zero amounts. Validate each entry and skip bad ones with logging.

7. Totals computed independently from emitted elements
`_add_document_allowances_charges` and `_get_document_level_totals` iterate meta separately — if one skips bad entries and the other doesn't, EN16931 BR-CO-11 (totals must match) fails. Extract a shared validated list.

8. `_get_tax_category` uses unordered `.first()`
`xml_builder.py:254` — Multi-line invoices with mixed tax categories get a non-deterministic document-level category. Add `.order_by("sort_order", "id")` or validate all lines share the same category.

What's good

  • Safe XML construction throughout — lxml auto-escaping, no f-string concatenation
  • Historical tax rate preservation from frozen `line.tax_rate`
  • Payment means codes correct per UNCL4461 (30/48/68/49/10)
  • `_validate_invoice()` is thorough with collected error list
  • None-safe tax total handling preserves zero-rated invoices correctly
  • No new tests needed for merge (new code paths not yet used in production), but should be added before the feature goes live

b3lz3but added a commit to b3lz3but/PRAHO that referenced this pull request Mar 27, 2026
…ax category logic, safe Decimal

- Use VATEX codelist (VATEX-EU-AE, etc.) for TaxExemptionReasonCode instead
  of misusing UNCL5305 category codes
- Only prefer line-level tax_category_code when explicitly non-default (not "S"),
  allowing customer/amount-based detection for AE/O/K/E categories
- Use Decimal(str(...)) for JSON-sourced amounts to avoid float rounding issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b3lz3but added a commit to b3lz3but/PRAHO that referenced this pull request Mar 27, 2026
…ax category logic, safe Decimal

- Use VATEX codelist (VATEX-EU-AE, etc.) for TaxExemptionReasonCode instead
  of misusing UNCL5305 category codes
- Only prefer line-level tax_category_code when explicitly non-default (not "S"),
  allowing customer/amount-based detection for AE/O/K/E categories
- Use Decimal(str(...)) for JSON-sourced amounts to avoid float rounding issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@b3lz3but b3lz3but force-pushed the fix/en16931-xml-remaining-gaps branch from 326fd66 to 94223ec Compare March 27, 2026 15:49
@b3lz3but
Copy link
Copy Markdown
Contributor Author

Review feedback addressed (326fd66, rebased as 94223ec)

  1. TaxExemptionReasonCode — now uses VATEX codelist values (VATEX-EU-AE, VATEX-EU-IC, etc.) instead of misusing UNCL5305 category codes. Added TAX_EXEMPTION_REASON_CODES mapping. Both invoice and credit note builders updated.
  2. _get_tax_category() short-circuit — only prefers line-level tax_category_code when explicitly non-default (not "S"), so customer/amount-based detection for AE/O/K/E categories runs correctly.
  3. Safe Decimal from JSON — changed to Decimal(str(...)) for allowance/charge amounts from JSONField to avoid float binary rounding.
  4. Test coverage — acknowledged, will be added in follow-up.
  5. PR description — acknowledged, will update scope.

…imal

- Payment means code mapping based on Stripe/bank/card/PayPal methods
- Tax category detection from customer location and tax amounts
- Document-level allowances/charges (BG-20/BG-21)
- Use VATEX codelist (VATEX-EU-AE, etc.) for TaxExemptionReasonCode
- Only prefer line-level tax_category_code when non-default (not "S")
- Use Decimal(str(...)) for JSON-sourced amounts to avoid rounding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
@b3lz3but b3lz3but force-pushed the fix/en16931-xml-remaining-gaps branch from 94223ec to 5fc9c9e Compare March 27, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(billing): EN16931 XML remaining gaps — payment means, tax categories, document-level charges

3 participants