Files
mercadodevida/work/artifacts/F-017/architect.md
2026-08-17 22:23:10 +02:00

75 lines
3.9 KiB
Markdown

# Architect — F-017 Pricing module
## Feature
F-017 adds server-side pricing and VAT calculation behind a public `PricingService`, so future cart/checkout flows never trust frontend-supplied prices.
## Design
### Module boundaries
Create `project/src/modules/pricing/` following existing module structure:
- `domain/price.ts` for money, VAT rate, price record and calculation result types.
- `domain/ports.ts` for `PricingRepository` and `PricingService` contracts.
- `domain/errors.ts` for missing price and invalid pricing command errors.
- `application/pricing-service.ts` implementing calculations and price changes.
- `infrastructure/pg-pricing-repository.ts` owning PostgreSQL persistence.
- `api/pricing.routes.ts` for explicit backend endpoints.
- `index.ts` exporting only the public pricing API.
Catalog remains independent. Pricing stores `variant_id` as an opaque UUID reference and does not join or import catalog internals. Future cart/checkout must depend on `PricingService`, injected at composition root.
### Money and VAT rules
Use integer cents only — no floating point money.
- Stored base price: `net_unit_amount_cents` (pre-VAT).
- VAT rate: enum `general` = 21% and `reduced` = 10% for this slice.
- Calculation: `netSubtotal = netUnit * quantity`; `vatAmount = round(netSubtotal * vatBps / 10000)`; `total = netSubtotal + vatAmount`.
- Currency is fixed to `EUR` by database default and API response. Multi-currency is explicitly out of scope.
### Data model
Add migration `012_pricing.js`:
- `pricing_variant_prices`
- `variant_id uuid primary key`
- `net_unit_amount_cents integer not null check >= 0`
- `vat_rate text not null check in ('general', 'reduced')`
- `currency text not null default 'EUR' check = 'EUR'`
- timestamps
- `pricing_price_history`
- append-only audit rows for every price change
- stores previous and new net amount/rate/currency, plus timestamp
- first price creation also writes a history row with previous fields null
### Public service interface
Expose `PricingService` from module index:
- `calculate({ variantId, quantity }): Promise<PriceCalculation>`
- `setVariantPrice({ variantId, netUnitAmountCents, vatRate }): Promise<VariantPrice>`
- `getVariantPrice(variantId): Promise<VariantPrice | undefined>`
`calculate` never accepts or uses client unit price, subtotal, VAT, discount, or total fields. That is the core rule: callers provide product identity and quantity; pricing returns truth.
### API slice
Add routes:
- `GET /pricing/variants/:variantId` — public read of current server-side price.
- `PUT /pricing/variants/:variantId` — admin-only price change.
- `POST /pricing/calculate` — calculation by `variantId` and `quantity`; schema strips/ignores extra client fields.
HTTP errors:
- `PRICING_PRICE_NOT_FOUND` as 404 when calculating/read has no server-side price.
- `INVALID_PRICE` as 422 for invalid commands.
### Tests
- Unit tests for VAT calculation for general/reduced rates and invalid quantity/amount.
- Unit/API test proving client-supplied price fields are ignored by schema/service and server price is used.
- Integration test proving every price change writes a history row, including create and update.
- Boundary test proving catalog does not reference `pricing_` tables or pricing internals.
- Migration suite remains the safety net for up/down/idempotency.
## Acceptance trace
- PricingService.calculate returns totals with VAT from persisted server price.
- Client price ignored: calculate only uses variant id + quantity; schema strips extra price fields and tests send bogus client values.
- Price history: repository writes append-only rows on creation and update.
- `verify.sh`: must pass after gates.
## Risks / constraints
- Do not add decimal/money dependencies; integer cents are sufficient and safer.
- Do not couple pricing to catalog by FK or join in this slice; variant existence validation is a later orchestration concern.
- Do not store or trust client totals anywhere.