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

3.9 KiB

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.