Files
mercadodevida/work/artifacts/F-072/architect.md
2026-08-19 19:22:08 +02:00

3.9 KiB

F-072 — Architect: Dynamic VAT tax rates in product edit prices tab

Current state

PricingSection.tsx (admin) has hardcoded VAT logic:

const VAT_GENERAL = 1.21;   // hardcoded constant
const VAT_REDUCED = 1.10;    // hardcoded constant

// calcGross: gross = net * VAT_GENERAL/VAT_REDUCED

The dropdown has two hardcoded options:

<option value="general">21% gen.</option>
<option value="reduced">10% red.</option>

But the DB tax_rates table already has 3 active rates:

  • IVA Superreducido: 4% (applies_to: super-reduced)
  • IVA Reducido: 10% (applies_to: reduced)
  • IVA General: 21% (applies_to: general)

The 4% rate is missing from the dropdown and calculation.

Goal

Replace hardcoded VAT constants and dropdown with dynamic fetch of active tax rates from GET /admin/tax-rates. Also add super-reduced support to the backend so the 4% rate can be used.

Scope IN

  • project/src/modules/pricing/domain/price.ts — add super-reduced to VatRate type and VAT_BASIS_POINTS
  • project/src/modules/pricing/api/pricing.routes.ts — accept 'super-reduced' in vatRateSchema
  • project/migrations/031_vat_super_reduced.js — update CHECK constraints on pricing_variant_prices.vat_rate and pricing_price_history columns to include 'super-reduced'
  • project/apps/admin/src/features/products/components/sections/PricingSection.tsx — fetch active tax rates from GET /admin/tax-rates, populate dropdown dynamically, use fetched rate percentages for calculations
  • project/apps/admin/src/lib/api-client.ts — extend VariantPrice.vatRate type to include super-reduced

Scope OUT

  • Tax rates admin page (/tax-rates) — no changes needed, already supports activate/deactivate
  • Backend pricing service — already generic, only needs domain type update
  • Storefront/frontend — no changes

Design

Backend: add super-reduced

price.ts:

export type VatRate = 'general' | 'reduced' | 'super-reduced';
export const VAT_BASIS_POINTS: Record<VatRate, number> = {
  general: 2100,      // 21%
  reduced: 1000,      // 10%
  'super-reduced': 400, // 4%
};

pricing.routes.tsvatRateSchema:

const vatRateSchema = z.enum(['general', 'reduced', 'super-reduced']);

Migration 031

Update CHECK constraints to:

CHECK (vat_rate IN ('general', 'reduced', 'super-reduced'))

PricingSection.tsx

  1. Add TaxRate[] state for active tax rates
  2. useEffect on mount: fetch taxApi.list(), filter active === true
  3. Replace VAT_GENERAL/VAT_REDUCED constants with dynamic lookup:
    const rate = activeRates.find(r => r.appliesTo === vatRate[variantId]);
    const multiplier = 1 + (rate?.ratePercent ?? 21) / 100;
    const grossCents = Math.round(netCents * multiplier);
    
  4. Dropdown maps appliesTo → option value, displays name + ratePercent%:
    {activeRates.map(r => (
      <option key={r.appliesTo} value={r.appliesTo}>
        {r.name} ({r.ratePercent}%)
      </option>
    ))}
    
  5. setVatRate stores appliesTo key (e.g. 'super-reduced') as the vat rate value
  6. Default: use first active rate if none selected

Admin api-client

Extend VariantPrice.vatRate type to include 'super-reduced':

vatRate: 'general' | 'reduced' | 'super-reduced';

Risk

  • Low risk: adding a new allowed value to an existing enum + updating a CHECK constraint; no data migration needed (existing data already uses 'general' or 'reduced')
  • Risk mitigation: migration is additive (adds 'super-reduced' to allowed values, doesn't remove any); rollback simply reverts constraint

Verification

  • npx tsc --noEmit on all projects
  • npx eslint on changed files
  • Apply migration 031 to dev DB
  • Backend rebuilt and restarted
  • API test: PATCH /pricing/variants/:id with vatRate: 'super-reduced' → HTTP 200
  • Admin product edit → pricing tab → dropdown shows all 3 active rates
  • ./scripts/verify.sh green