Files
mercadodevida/spec/tech.md
2026-08-22 10:22:25 +02:00

4.0 KiB

F-138 — Especificación técnica

Contrato de separación (F-154 precedent)

  • pricing es dueño de pricing_variant_prices (tabla a su módulo). No se inserta desde catalog con SQL crudo — se expone un método en el servicio público PricingService (patrón idéntico al que cart ya inyecta en build-app: cross-module write vía servicio público, R1 legal).

Cambios

1. pricing/domain/ports.ts

  • Añadir a PricingRepository: seedVariantPrice(variantId: string): Promise<void>.
  • Añadir a PricingService: seedVariantPrice(variantId: string): Promise<void>.

2. pricing/application/pricing-service.ts

  • PricingService.seedVariantPrice(variantId) → delega a this.repository.seedVariantPrice(variantId). Sin validación extra (el variantId ya proviene de una variante creada en la misma transacción lógica).

3. pricing/infrastructure/pg-pricing-repository.ts

async seedVariantPrice(variantId: string): Promise<void> {
  await this.pool.query(
    `INSERT INTO pricing_variant_prices (variant_id, net_unit_amount_cents, offer_cents, cost_cents, vat_rate)
     VALUES ($1, 0, NULL, NULL, 'general')
     ON CONFLICT (variant_id) DO NOTHING`,
    [variantId],
  );
}
  • Columnas idénticas al INSERT de setVariantPrice (omite currency → DDL default 'EUR'; created_at/updated_atnow() DDL default). Reutiliza VatRate = 'general'.
  • ON CONFLICT (variant_id) válido: variant_id es UNIQUE/PK (usado por setVariantPrice). → idempotente / re-sembbrado no-op.

4. catalog/application/variant-use-cases.ts

  • Import (type, público, R1): import type { PricingService } from '../../pricing/index.js'; (catalog/application → ../../pricing/index = modules/pricing/index ✓ R1 a index público).
  • CreateProductVariant recibe pricing: PricingService en ctor.
  • execute: tras this.variants.create(productId, input) (y solo si el producto existe), await this.pricing.seedVariantPrice(variant.id) best-effort (try/catch silencioso: la variante ya persistió; un fallo del seed no revierte la creación).

5. catalog/api/catalog.routes.ts

  • CatalogRoutesDeps += pricing: PricingService (import type desde pricing index — R1).
  • const createVariant = new CreateProductVariant(repository, variants, pricing); (único constructor; cubre los 3 call sites: POST /products autovariante, GET /products/:id/variants lazy, POST /products/:id/variants).

6. build-app.ts

  • Mover const pricing = createPricingService(deps.pool); antes del bloque registerCatalogRoutes (actualmente está después → orden L247 catalog, L264 pricing). Crear pricing antes del registro de catalog permite pasarlo a CatalogRoutesDeps.
  • Pasar pricing en deps de registerCatalogRoutes.
  • La ruta de pricing (registerPricingRoutes) y cart siguen usando pricing (sin cambios; pricing sigue definido). createPricingService ya está importado (L33).

Riesgos / mitigaciones

  • Orden en build-app (timing): mover const pricing antes de catalog es seguro (constructor puro, deps.pool disponible). Precio routes lo vuelve a usar → no se rompe.
  • Fallo del seed: best-effort (try/catch) → no hace 500 en variant creation. AC3 lo prueba.
  • Doble creación (createVariant en autovariante + retry): ON CONFLICT DO NOTHING → idempotente. AC2 lo prueba.
  • Boundary R1: catalog→pricing público index ✓ (cart ya lo usa). Deep imports prohibidos — usar ../../pricing/index.js.

Tests

  • Unitario (runnable, sin DB): catalog/tests/variant-use-cases.test.ts — FakeProductRepository, FakeProductVariantRepository, FakePricingService; assert (a) seedVariantPrice llamado con variant.id tras create, (b) no se sembran si producto no existe, (c) create sigue devolviendo variante si seed lanza.
  • Integración (AC): itest en catalog.itest.ts skipIf(!hasDb) — POST /products/:id/variants → GET /pricing/variants/:id = 200 con netUnitAmountCents=0, vatRate='general'. Skipped sin TEST_DATABASE_URL (no bloquea verify.sh).