event

Product Variant Catalog Changed

A persisted product variant's catalog display data changed. Dispatched after scrape ingest, from-draft persist, model identity update / batch patch, or catalog reconcile.

EventStatus: Implemented in api-v1

Purpose

Integration event for downstream catalog consumers (design-management ProjectProductVariant projections). Payload is primitive IDs only (no domain objects).

Thin payload + read-back (hex §7 deviation, D9). Handlers re-read the current PM snapshot via ForReadingProductVariantSnapshots. Field values are not on the event: for AT-C1 the only trustworthy state is the current Airtable record, and the snapshot is 40+ fields.

Related-product (accessory) links use RelatedProductAdded, not this event — the variant aggregate did not change.

Payload

Envelope is { name, occurredAt, payload } (apps/api-v1/src/modules/shared/domain/events/DomainEvent.ts). One event per distinct productVariantId; do not collapse to a single productId.

After scrape ingest persist success, assembler-local ids (product:sku-1) remap onto Airtable RECORD_ID() values from persist productVariantIds / productModelId when that list is non-empty. Refresh/reload already uses rec or stored domain_id. Design-management readByIds matches RECORD_ID() or domain_id. Dispatch is unique per variant id (model + variant both changing does not send 2N events).

FieldTypeMeaning
productVariantIdstringVariant that changed (Airtable rec after scrape ingest remap, or stored domain_id)
productModelIdstring | nullOwning product model when known

Raised by

  • Ingestion — IngestScrapedCatalogUseCase after scrape persist success (HTTP create-from-url, refresh, reload, scrape-request, live CLI persistRawProduct). Events come from pullDomainEvents() on ProductModel / ProductVariant, then remap/unique before dispatch. Parameter-graph writes on those persist paths ride the same drain.
  • From-draft — RunCreateProductModelFromDraftWorkflowUseCase after persistDraft success.
  • Model identity update / batch patch — UpdateProductModelUseCase and BatchPatchProductModelsUseCase after rename / assignVendor / assignObjectClass (and reconcile-style identity mutations) record events on the model and drain after save.
  • Catalog reconcile — RequestCatalogReconcileUseCase (POST …/product-models/:id/reconcile, POST …/product-variants/:id/reconcile; AT-C1 request_reconcile.js). Recompute only; writes nothing to PM.

HTTP operations that can lead to dispatch (OpenAPI x-domain.domainEvents): create-from-url, from-draft, product-scrape, refresh, reload, reconcile. runBulkProductScraper does not dispatch on enqueue (domainEvents: []); later per-URL ingest may.

Not dispatched when persist writes no variant row, persist skips/fails, or aggregate events are empty. Identity update does dispatch.

Image-record create, unlink, or destroy is not this event. That is ProductVariantImagesChanged. Snapshot imageUrls (Airtable attachments copied onto a Design row) may still refresh when this event fires for other display-field changes; Supabase publish and delete do not ride this event.

Consumers

RefreshProjectProductVariantsOnCatalogChangedHandler in design-management. Refreshes existing project product rows only (never creates).