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

This PR adds the Argentina tax pack and the manual-rate UI from the spec (Decision F at docs/tax-engine-v2-spec.md:541). It then makes that UI always reachable, and writes an audit record for every save.

The Argentina pack

A new file, main/tax-packs/argentina.json, defines the official Argentina pack. It has three IVA rate categories (21%, 10.5%, 27%), a CUIT registration label, and the usual line-kind defaults. It is effective 2026-08-07 and needs FloCafe 2.4.0 or later. Owners install it through the existing signed catalog flow once it is published to the FloCafe-Plugins release; it is not bundled into the app. docs/tax-packs.md lists it under the official-pack examples.

The manual-rate UI

Owners who do not have an official pack for their country can set a single rate themselves. POST /api/tax-packs/manual-config builds a synthetic local pack with one tax percent rule that covers every category. The same 24-check validation runs on it that runs on every other pack. Saving again makes a new version, demotes the previous, and re-points merchant overrides so they keep working.

The form is always visible now, not only after a 404. It has three fields: rate, menu prices (with a plain-language explainer of what "inclusive" and "exclusive" actually mean, plus a worked example at the rate the owner typed), and registration label. When an official pack is active, the form shows an amber warning, the save button changes to "Replace with manual rate", and the panel asks for confirmation before sending override: true.

The form also reloads the saved rate, inclusive flag, and label when the page loads, so an empty form cannot silently overwrite a working configuration. Everything in the panel is translated: en, es, and pt, 1769 keys.

Replacing an active pack

Without override: true the server still 409s, with can_override: true so the panel knows to ask. With override: true the server demotes the official pack, activates the new manual-<cc> version, and records the replaced pack id plus the count of dormant overrides in the audit.

Merchant overrides stay dormant instead of being re-pointed. A manual pack applies one rate to every category, so re-pointing an override would change no tax outcome but would lose the categoryId the owner chose (for example iva_105 going to standard). That data is gone forever once re-pointed. Dormant overrides keep the original value and start working again if the official pack is reactivated. The count of dormant overrides shows up in the response, the audit, and a warning toast.

Stale categories are remapped and recorded

backfillCategoryDefaults runs on every pack swap. It is the only thing keeping checkout from returning 400 on every line when the new pack does not define the category a product is on (for example iva_21 after switching from Argentina to a manual pack).

The spec (Decision K) said this remap has to be recorded. The function now returns the mapping as {entity, from, to, count}[] and writes a remap_categories audit row. The panel also gets the mapping in the response and shows a warning toast:

12 product/add-on tax categories were remapped and 3 merchant overrides stopped applying. Check the audit history.

Products that had no category at all are still treated as a backfill, not a loss, and are not reported.

What the owner actually sees when they type a manual value

The receipt tax-id label comes from resolveTaxIdLabel(country). Before the fix, this function returned the active pack's registrationNumberLabel whenever the pack was active. local-generic is the no-tax pack that ships with every install. Its country is * and its label is "Tax registration". On a Thailand install with no official pack yet, this generic pack was active for TH, and the receipt printed "Tax registration: TAXID-0001" instead of the correct "Tax ID: TAXID-0001" from main/countries.ts.

The fix at main/services/tax.ts:113 is one line: a pack's label is only trusted when its country matches the requested country. So:

  • Argentina with the official pack active: the pack's country is AR, the label is CUIT. Receipt prints "CUIT: 30-...".
  • Thailand with no pack yet: local-generic is still the active pack, but its country * does not match TH, so the function falls through to countries.ts and returns "Tax ID".
  • Thailand after the owner saves a manual rate: the new manual-th pack has country: 'TH', so its label (whatever the owner typed, e.g. "Tax ID No.") wins. If the owner leaves the label blank, it defaults to "Tax registration" and that label is now trusted for TH.

For the manual pack prefill, the same one-line guard used to look like detail.pack.publisher === 'local' && detail.pack.country === storeCountry. That country check excluded the local-generic wildcard (country *) so the form did not prefill from a pristine install. But once the manual pack for the store's country was created, it had its own country code and the check passed. The real question the panel needs to answer is not "is this pack for my country" but "does this pack have a rate". The wildcard pack has zero rules; a saved manual pack always has one. The check is now detail.rules.find((rule) => rule.rate)?.rate, and a new test in tests/tax-pack-management.test.ts asserts both halves: local-generic has no rules, a saved manual-<cc> has exactly one.

Diff size

12 files, +1682 / −189 against main. Largest file: tests/tax-pack-management.test.ts at +396 lines.

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

carvalab commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@itsbkm @khaira777 The manual tax is for countries without tax country, as MX, (maybe we need send telemetry of this setting to create a PR for that country with that values for the next customer)
also is the included and no included with examples of the value
Can you check what do you think?
imagen
imagen

@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.

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