feat(F-072): completed feature

This commit is contained in:
chattie
2026-08-19 19:22:08 +02:00
parent 9bdcc8e20e
commit 352033e3fc
16 changed files with 567 additions and 77 deletions

View File

@@ -0,0 +1,111 @@
# F-072 — Architect: Dynamic VAT tax rates in product edit prices tab
## Current state
`PricingSection.tsx` (admin) has **hardcoded** VAT logic:
```ts
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**:
```tsx
<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`:
```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.ts``vatRateSchema`:
```ts
const vatRateSchema = z.enum(['general', 'reduced', 'super-reduced']);
```
### Migration 031
Update CHECK constraints to:
```sql
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:
```ts
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%`:
```tsx
{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'`:
```ts
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

View File

@@ -0,0 +1,74 @@
# F-072 — Implementer evidence: Active tax rates listed in product edit prices tab
## Problem
`PricingSection.tsx` (admin product edit → pricing tab) had **hardcoded** VAT logic:
- Two constants: `VAT_GENERAL = 1.21` and `VAT_REDUCED = 1.10`
- Dropdown with only two options: "21% gen." and "10% red."
- No support for super-reduced (4%) which exists in `tax_rates` DB table
The database `tax_rates` already had 3 active rates (4%, 10%, 21%), but the frontend only showed 2.
## Changes
### Backend domain
`project/src/modules/pricing/domain/price.ts`:
- Added `'super-reduced'` to `VatRate` type
- Added `'super-reduced': 400` to `VAT_BASIS_POINTS`
### Backend routes
`project/src/modules/pricing/api/pricing.routes.ts`:
- `vatRateSchema` updated from `z.enum(['general', 'reduced'])` to `z.enum(['general', 'reduced', 'super-reduced'])`
### Migration 031
`project/migrations/031_vat_super_reduced.js` (ESM, applied to DB):
- Updated CHECK constraint on `pricing_variant_prices.vat_rate` to include 'super-reduced'
- Updated CHECK constraints on `pricing_price_history.previous_vat_rate` and `new_vat_rate` to include 'super-reduced'
### Admin types
`project/apps/admin/src/types/index.ts`:
- `VariantPrice.vatRate` extended to include `'super-reduced'`
### Admin api-client
`project/apps/admin/src/lib/api-client.ts`:
- `setVariantPrice` parameter type extended to `'general' | 'reduced' | 'super-reduced'`
### PricingSection.tsx
- Imported `taxApi` and `TaxRate` from `@/lib/api-client`
- Added `activeTaxRates` state (`TaxRate[]`)
- Added `useEffect` to fetch active tax rates from `taxApi.list()` and filter `active === true`
- `vatRate` state type extended to include `'super-reduced'`
- Dropdown now maps `activeTaxRates.map(r => ({ key: r.appliesTo, label: r.name + ' (' + r.ratePercent + '%)' }))`
- Fallback: when no active rates loaded, shows original two options
- PVP/gross calculation uses dynamic rate: `Math.round(netCents * (1 + ratePercent / 100))`
- Help text updated to reference "Configuración → Tipos impositivos"
### InventorySection.tsx
- `VariantRow.vatRate` type extended to `'general' | 'reduced' | 'super-reduced'`
- Gross price calculation updated to handle `super-reduced` (1.04 multiplier)
- VAT display in non-edit mode updated to show 4%, 10%, or 21% based on rate
- VAT dropdown in edit mode added "4% (superreducido)" option
## Verification
- `npx tsc --noEmit` — backend exit 0 ✅, admin exit 0 ✅
- `npx eslint` on changed files — exit 0 ✅
- Migration 031 applied to DB ✅
- Backend rebuilt and restarted ✅
- **API test**: `PUT /pricing/variants/:id` with `vatRate: 'super-reduced'` → HTTP 200, response includes `"vatRate":"super-reduced"`
- **API test**: `GET /pricing/variants/:id` returns variant with `vatRate: 'super-reduced'`
- **API test**: `GET /admin/tax-rates` returns all 3 active rates (4%, 10%, 21%) ✅
- `./scripts/verify.sh` — exit 0 ✅
## Files touched
```
project/migrations/031_vat_super_reduced.js (new)
project/src/modules/pricing/domain/price.ts (modified)
project/src/modules/pricing/api/pricing.routes.ts (modified)
project/apps/admin/src/types/index.ts (modified)
project/apps/admin/src/lib/api-client.ts (modified)
project/apps/admin/src/features/products/components/sections/PricingSection.tsx (modified)
project/apps/admin/src/features/products/components/sections/InventorySection.tsx (modified)
work/artifacts/F-072/implementer.md (this file)
```

View File

@@ -0,0 +1,18 @@
{
"feature_id": "F-072",
"agent": "leader",
"verdict": "APPROVED",
"summary": "All gates approved. F-072 replaces hardcoded 2-option VAT dropdown with dynamic fetch from GET /admin/tax-rates. Backend supports 'super-reduced' (4%). Migration 031 applied. Typecheck, lint, and verify.sh green.",
"evidence": [
"work/artifacts/F-072/reviewer.json verdict=APPROVED",
"work/artifacts/F-072/security.json verdict=APPROVED",
"work/artifacts/F-072/qa.json verdict=APPROVED",
"Migration 031 applied: CHECK constraints updated to include 'super-reduced'",
"Backend rebuilt and restarted; API verified with vatRate='super-reduced' → HTTP 200",
"npx tsc --noEmit backend exit 0",
"npx tsc --noEmit admin exit 0",
"npx eslint on changed files exit 0",
"./scripts/verify.sh exit 0, 145 features valid"
],
"timestamp": "2026-08-19T17:25:00Z"
}

View File

@@ -0,0 +1,20 @@
{
"feature_id": "F-072",
"agent": "qa",
"verdict": "APPROVED",
"summary": "F-072 fully implemented. Migration applied. API verified. Typecheck and verify.sh green.",
"evidence": [
"AC1 'PricingSection fetches active tax rates from GET /admin/tax-rates' — taxApi.list() called in useEffect, active rates filtered and stored in state, mapped to dropdown options",
"AC2 'Dropdown shows all active rates dynamically' — activeTaxRates.map() renders option per active rate; fallback shows 2 original options if API fails",
"AC3 'VAT calculation uses fetched rate percentages instead of hardcoded constants' — grossCents = Math.round(netCents * (1 + ratePercent / 100)) where ratePercent comes from activeTaxRates.find()",
"AC4 'verify.sh is green' — exit 0, 145 features valid",
"Migration 031 applied successfully to dev DB",
"npx tsc --noEmit backend exit 0",
"npx tsc --noEmit admin exit 0",
"npx eslint on changed files exit 0",
"API: PUT /pricing/variants/:id with vatRate='super-reduced' → HTTP 200, variant saved with super-reduced",
"API: GET /pricing/variants/:id confirms vatRate='super-reduced' persisted",
"API: GET /admin/tax-rates returns all 3 active rates including 4% super-reduced"
],
"timestamp": "2026-08-19T17:24:00Z"
}

View File

@@ -0,0 +1,23 @@
{
"feature_id": "F-072",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "F-072 replaces hardcoded 2-option VAT dropdown with dynamic fetch from GET /admin/tax-rates. Backend domain type updated to support 'super-reduced', migration 031 applied, admin PricingSection and InventorySection updated to show all active rates dynamically. API verified: PUT with vatRate='super-reduced' → HTTP 200.",
"evidence": [
"price.ts: VatRate type extended with 'super-reduced', VAT_BASIS_POINTS updated with 400",
"pricing.routes.ts: vatRateSchema now accepts 'super-reduced'",
"Migration 031 applied: CHECK constraints updated on pricing_variant_prices and pricing_price_history",
"admin types/index.ts: VariantPrice.vatRate includes 'super-reduced'",
"admin api-client.ts: setVariantPrice accepts 'super-reduced'",
"PricingSection.tsx: fetches taxApi.list(), filters active=true, populates dropdown dynamically, uses dynamic rate for calculations",
"InventorySection.tsx: VariantRow type, gross calculation, and display all support 'super-reduced'",
"npx tsc --noEmit backend exit 0",
"npx tsc --noEmit admin exit 0",
"npx eslint on changed files exit 0",
"API: PUT /pricing/variants/:id with vatRate='super-reduced' → HTTP 200",
"API: GET /pricing/variants/:id returns vatRate='super-reduced'",
"API: GET /admin/tax-rates returns 3 active rates (4%, 10%, 21%)",
"verify.sh exit 0"
],
"timestamp": "2026-08-19T17:22:00Z"
}

View File

@@ -0,0 +1,17 @@
{
"feature_id": "F-072",
"agent": "security",
"verdict": "APPROVED",
"summary": "F-072 adds a new allowed value ('super-reduced') to an existing database column with a CHECK constraint, and extends TypeScript union types. No new dependencies, no new endpoints, no auth changes. The backend Zod schema validates the vatRate field (already validated before this change, now accepts 3 values instead of 2). No XSS or injection risk.",
"evidence": [
"No new dependencies added to any package.json",
"No new API endpoints — only extended an existing enum value",
"No auth/authz changes — existing admin role gate applies to all pricing endpoints",
"No env vars introduced",
"Migration adds 'super-reduced' to allowed CHECK constraint values — additive change, no data risk",
"TypeScript types extended (union types) — purely compile-time safety",
"Zod schema in pricing.routes.ts accepts 'super-reduced' — validated server-side before DB write",
"verify.sh exit 0"
],
"timestamp": "2026-08-19T17:23:00Z"
}