entity

Project Product Variant

Project-scoped product variant aggregate. Stock rows reference a Marketplace Product Variant; graph assignments point here by domain_id.

Entity(Aggregate Root)Status: Implemented in api-v1Aggregate RootAirtable-backed (no OCC)

Purpose

A Project Product Variant is the project-local identity a steward assigns on the definition graph or references from a ProjectCollectionItem. Catalog SoR stays in product-marketplace.

  • Stock rows copy identity and display snapshot from a Marketplace Product Variant (catalog_variant_domain_id).
  • Made-to-order rows are materialized for one project via materializeFromModel. They store a version-1 configuration snapshot and Design is_mto. They do not reference a catalog variant.

Boundary with PDG

Product Assignment is a value object on PDG Node, not this aggregate. Postgres stores pdg_product_assignment.project_product_variant_domain_id as TEXT with no FK to this Airtable table. Commands re-read variant facts from ForStoringProjectProductVariants.getByDomainId.

A Design BOM Line groups resolved contributions by this aggregate’s domain_id. The line does not own the variant.

Reads

GET /api/v1/design/projects/{projectId}/project-product-variants lists this aggregate for one project. view=vertical_card (default) or simple changes the representation only. limit and offset page the list. objectClassDomainId filters with eq or in. objectClassName filters with eq. Stock rows are hydrated from product marketplace. Made-to-order rows stay on the list with null catalog display fields.

GET /api/v1/design/projects/{projectId}/project-product-variants/{id} (getProjectProductVariant) returns { data } for one row. The id is this aggregate’s domain id. Another project is 404. A made-to-order id returns configurationSnapshot and selections. That read parses the stored snapshot. It does not load the write aggregate. A snapshot that cannot be parsed is 500 MTO_CONFIGURATION_UNREADABLE. A missing row stays 404.

GET .../{id}/product-documents and GET .../{id}/product-documents/{docId} return durable PDF URLs. Stock calls product marketplace and does not write inside the query. An empty stock doc_url is published by the controller, then read again. Made-to-order documents stay 501 MTO_QUERY_NOT_IMPLEMENTED.

Commands

Made-to-order writes are POST only. There is no PATCH or DELETE on this resource.

MethodPathOperationResult
POST/api/v1/design/projects/{projectId}/project-product-variantsmaterializeMadeToOrderProjectVariant201 and Location
POST.../{id}/actions/updatereviseMadeToOrderProjectVariant200. Another project is 404
POST.../{id}/actions/deletewithdrawMadeToOrderProjectVariant200. Another project is 404

Domain Events

Emits (this aggregate owns the fact):

  • ProjectProductVariantProjected — a catalog variant was written onto project_product_variants (first Add to Project persist, or a later catalog-snapshot refresh). Product Marketplace publishes missing Supabase product-image URLs and missing PDF doc URLs.
  • ProjectProductVariantReleased — the row was destroyed because no collection on that project still references the catalog variant. Product Marketplace deletes Supabase objects for that variant’s images only when no remaining project_product_variants row uses the same catalogVariantDomainId.
  • ProjectProductVariantMaterialized — a made-to-order configuration was created for one project. Dispatched after the Airtable save succeeds. No handler is registered.
  • ProjectProductVariantConfigurationRevised — the made-to-order selection set changed. Dispatched after the save succeeds. An unchanged set records nothing.
  • ProjectProductVariantWithdrawn — a made-to-order row was deleted. Dispatched after the delete succeeds. No handler is registered. This is not Release.

Listens: ProductVariantCatalogChanged → RefreshProjectProductVariantsOnCatalogChangedHandler. That refresh copies Design catalog fields (including snapshot imageUrls attachment links). It does not publish or delete Supabase objects. Image-record create/delete on a catalog variant that is currently on this table is ProductVariantImagesChanged, owned by product-marketplace.