Skip to content

feat(tax): Argentina IVA pack + manual-config for unsupported countries - #219

Open
carvalab wants to merge 1 commit into
mainfrom
feat/argentina-iva
Open

feat(tax): Argentina IVA pack + manual-config for unsupported countries#219
carvalab wants to merge 1 commit into
mainfrom
feat/argentina-iva

Conversation

@carvalab

@carvalab carvalab commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds the official Argentina tax pack (IVA 21%, 10.5% and 27%, CUIT label) and cleans up the manual-rate UX that's already in main.

Argentina pack

  • main/tax-packs/argentina.json: catalog-only official pack, effective 2026-08-07, requires FloCafe 2.4.0+
  • docs/tax-packs.md: added to the official-pack examples list

Tests

  • tax-pack-management.test.ts: signs and installs the pack through the catalog flow, runs the 24-check validation, and checks that ARS 1000 inclusive at 21% extracts ARS 173.55
  • e2e-argentina-flow.test.ts: installs the pack and verifies a real DB product (ARS 17.36 inclusive at 21%, "IVA 21%" label) instead of assuming the pack is absent

Manual-rate UX

  • The replace warning shows as soon as the manual builder opens while an official pack is active. Before, it only appeared after the server rejected the save with a 409.
  • "Menu prices" shows a live worked example at the typed rate, for both inclusive and exclusive.
  • The products page explains "Tax behavior": what inclusive and exclusive mean, and what "Country default" resolves to (the active pack's setting, exposed through a new default_inclusive field).
  • The product list finally shows the Incl./Excl. badges that existed in i18n but were never wired up.

Diff size

10 files, +258 / −7 against main.

@carvalab
carvalab requested a review from itsbkm as a code owner August 7, 2026 21:45
@carvalab
carvalab force-pushed the feat/argentina-iva branch from 1405e7f to d462626 Compare August 8, 2026 02:28
@khaira777

Copy link
Copy Markdown
Contributor

Thanks for the work here - the overall direction is great. The versioning, audit trail, dormant overrides, category remapping, registration-label fix, and inclusive/exclusive examples are valuable foundations.

Before resuming work, could you coordinate with @itsbkm on the implementation direction? This PR overlaps with the newer manual-tax builder and tax-ordering work already on main, so we should avoid parallel or conflicting implementations.

Once aligned, please rebase this PR onto current main. The Argentina pack and reusable backend pieces are still valuable; the single-rate manual UI should be reconciled with the broader multi-category/manual-component builder now in main. Discount/tax ordering can then be covered in a focused follow-up with tests.

@carvalab
carvalab force-pushed the feat/argentina-iva branch 2 times, most recently from b297d51 to d74aadf Compare August 11, 2026 21:29
@carvalab
carvalab marked this pull request as draft August 11, 2026 21:35
@carvalab
carvalab marked this pull request as ready for review August 12, 2026 20:27
@carvalab

Copy link
Copy Markdown
Contributor Author

thanks @khaira777, @itsbkm
I updated based on your new manual taxes and added Argentina.
Do you think it is good or should we wait for the new taxes?
thanks

@carvalab
carvalab force-pushed the feat/argentina-iva branch from d74aadf to cbb60fa Compare August 13, 2026 15:47
The spec's Decision F (docs/tax-engine-v2-spec.md:541) said manual
configuration would be a synthetic local country pack; the PR adds the
pack, then the UI, then makes the UI always reachable and auditable.

Argentina pack
- main/tax-packs/argentina.json: id official-argentina, country AR,
  currency ARS, registrationNumberLabel CUIT. Three IVA rate
  categories (iva_21, iva_105, iva_27) plus line-kind defaults and
  iva_exempt unclassified. Effective 2026-08-07, minFloVersion 2.4.0.
- Not bundled — ships via the signed catalog flow
  (tax-pack-official-argentina-v0.1.0 → FloCafe-Plugins release).
- docs/tax-packs.md: added to the official-pack examples list.

Manual-config UI
- POST /api/tax-packs/manual-config (owner-only) builds a synthetic
  local pack (publisher 'local', signature null) with one tax percent
  rule applied to all six categories. Runs the same 24-check
  validationChecklist every other pack runs. Re-save creates a new
  version, demotes the previous, and re-points tax_overrides so
  merchant overrides carry across.
- TaxConfigurationPanel: the manual rate form is now always visible,
  not only after a 404. Shows rate, menu prices (inclusive / exclusive)
  with a plain-language explainer and a worked example recomputed live
  at the entered rate, registration label with hint, and an amber
  warning when an official pack is active. Button copy switches to
  "Replace with manual rate" with a window.confirm that names the
  pack before sending override: true.
- The form reloads the saved rate / inclusive / label on mount, so a
  blank form cannot silently overwrite an existing configuration.
- Every label, toast, audit line, and support-ticket payload resolves
  through t(); the panel is fully translated in en/es/pt (1769 keys).

Pack replacement (override)
- manual-config accepts override: true. Without it an active official
  pack still 409s (with can_override: true so the panel knows to ask).
- Merchant overrides are left dormant, not re-pointed: a manual pack
  applies one rate to every category, so re-pointing changes no tax
  outcome but overwrites the categoryId the owner chose — data loss
  for zero benefit. Dormant overrides keep the original value and
  revive if the official pack is activated again. The count is
  surfaced in the response, the audit, and a warning toast.
- replacedPackId and dormantOverrides are written to the
  activate_pack audit details. auditDescription renders them:
  "Manual configuration (rate 21%, inclusive) · replaced ar-iva;
  3 merchant overrides stopped applying".

Category remap (audited)
- backfillCategoryDefaults (shared by manual-config and
  ensure-country) is the only thing that prevents checkout from
  400-ing on stale tax_category_id values when the incoming pack
  does not define the row's category.
- Decision K required that the mapping be recorded. The function
  now returns {entity, from, to, count}[] and both call sites write
  a remap_categories audit row with the full mapping. The mapping
  is also returned in the response so the panel can warn the owner
  (⚠ N product/add-on tax categories were remapped and M merchant
  overrides stopped applying. Check the audit history.). NULL rows
  remain a backfill, not a loss, and are not reported.

Country label fix
- local-generic carries country: '*'. resolveTaxIdLabel was returning
  its "Tax registration" label to every country, which leaked to TH.
  Now trusts a pack's label only when its country matches the
  requested country; otherwise falls through to countries.ts, then
  "Tax ID".

One-POS-is-one-country
- The country check on the manual-pack prefill is gone. The real
  discriminator was never the country: the bundled no-tax pack is
  publisher 'local' too. A "has a rate" test (pack.rules[].rate)
  tells a configured manual pack apart from a pristine install. A
  new test asserts both halves of that invariant.

Helpers
- main/services/tax.ts: resolveTaxIdLabel (country-scoped),
  backfillCategoryDefaults → CategoryRemap[].
- main/routes/tax-packs.ts: manual-config + override, ensure-country,
  remap_categories audit on both.

Tests
- tax-pack-management 172/172 (sections 9-13 + invariant):
    * section 9: signs Argentina pack via Ed25519, asserts 24-check
      validation, ARS 173.55 inclusive extraction on ARS 1000.
    * section 10: saves a manual pack (rate 21, CUIT), audit +
      backfill + tax calc.
    * section 11: rejects manual-config when an official pack is
      active; with override: true it replaces, demotes the official
      one, remaps stale categories, writes a remap_categories audit,
      reports remapped + dormant_overrides in the response.
    * section 12: re-save path. Same pack_id, fresh patch version,
      previous version demoted, audit previousVersionId set.
    * section 13: resolveTaxIdLabel fallback paths.
    * invariant: local-generic has no rules, a saved manual-<cc>
      has exactly one — the two halves of the panel's prefill test.
- e2e-argentina-flow 35/35: real DB product, ARS 17.36 inclusive at
  21%, "IVA 21%" component label.
- integration-tax 94/94, integration-happy-path 24/24, tax-engine
  11/11, upgrade-path.
- Both new tests wired into npm test via package.json.

Verification
- npm run build (backend) ✓, npm run build:frontend ✓
- npm run lint ✓ (0 errors)
- npx tsc --noEmit ✓

Out of scope (open spec items)
- Spec activation rule "show a clear before/after summary and
  effective date" before activating. The window.confirm names the
  pack; old rate → new rate and effective date are not shown yet.
  Add when an owner asks for a pre-flight review.
- taxPoint: 'finalized_at' is hardcoded on the synthetic local pack.
  Decision I makes held orders get silently repriced at checkout.
  order_created is the conservative default; switch when the
  held-order stale-warning banner ships (tax-engine-v2-spec.md:646).

Co-Authored-By: Claude <noreply@anthropic.com>

Review follow-up (squashed into the PR head):
- main/tax-packs/argentina.json: end the file with a trailing newline so
  the signed release bytes match a conventional POSIX file.
- TaxConfigurationPanel: the manual-config worked example and the
  official-pack replacement warning now resolve through useI18n with new
  settings.taxConfig* keys in en/es/pt.
- tax-pack-management.test.ts section 9: activate the catalog-installed
  Argentina pack through the real owner route
  (POST /:packId/versions/:versionId/activate) instead of direct
  country_packs updates, with the trusted-key binding pointed at the test
  keypair (ts-node CommonJS live property access) because the route
  re-validates with the app's trusted key. Asserts the route response and
  the activate_pack audit row; the products/addons backfill stays in the
  test because the activation route leaves product data alone.
- frontend/src/lib/i18n/fa.json: add the six new keys in Persian (the PR
  branch was rebased onto main, which now ships the fa locale; the
  translations integrity test enforces parity across en/es/pt/fa).

Verification: test:translations clean across en/es/pt/fa (1674 keys each),
test:tax-pack-management 112/112, test:e2e-argentina-flow 35/35,
frontend eslint clean, git diff --check clean.
@carvalab
carvalab force-pushed the feat/argentina-iva branch from cbb60fa to b740408 Compare August 13, 2026 15:57
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.

3 participants