domain

Construction Management

api-v1 bounded context for construction execution. Owns Site Image (field site-visit photos) and Product Order (orders drafted from BOM lines, with an invoice lifecycle and printed invoices). Estimates, schedules, packages and purchase orders are not modeled yet.

DomainStatus: Implemented in api-v1Airtable-backed

Overview

Construction Management in apps/api-v1/src/modules/construction-management owns two aggregates:

  • SiteImage: one site-visit photo for a field report. It is create-only, and its write path is Bulk Upload Site Images.
  • ProductOrder, with its Product Order Items. A steward selects BOM lines and runs Create Order, and one draft order is created for that project. The order tracks how its BOM lines drift, issues and freezes an invoice, is marked paid or cancelled, and prints a branded invoice PDF. In the MVP the Product Order is the procurement and invoice record (ADR-0133).

There is no nested System and no new deployable. The module is served by api-v1. It reads the BOM from project-delivery-graph. It reads variants from their owners: product-marketplace for stock, design-management for made-to-order. Invoices are rendered by document-generation. Every cross-module call goes through an ACL adapter.

Estimates, schedules, material requirements, procurement packages and purchase orders are not modeled yet. The BOM itself belongs to project-delivery-graph; this module only reads it.

Project is not an aggregate in this domain. Site Image references a construction project by ProjectId (projects.domain_id). The API resolves projects by domain_id only and never accepts Airtable rec… ids as projectId. Do not invent a Project entity here.

Implementation Status

Status: Implemented in api-v1

Airtable construction base (AIRTABLE_CONSTRUCTION_BASE_ID, default appQV2WVHMyLGkNx0). Persistence is Airtable site_images; bulk-upload status lives on ui_site_images_bulk_upload. There is no outbox. After persist, the use case drains SiteImageRecorded and dispatches in-process to DomainEventAuditLogger. Dispatch failure must not roll back persisted site images.

OCC is omitted (glossary deviation): create-only Airtable records have no compare-and-set.

Product orders:

  • Tables: product_order and product_order_items. Steward actions go through the throwaway AT-A1 table ui_product_order_actions, run by product_order_actions.js. Create Order runs from bom_line_actions.js.
  • Billing facts: project number, name, address and tax rate are read from projects_sync, a sync of Party Directory projects.
  • Invoice PDFs: stored in a private Supabase bucket (SUPABASE_STORAGE_INVOICES_BUCKET) and reached only through one-hour signed URLs.
  • Events: Product Order events are internal and audit-logged, like Site Image’s.
  • OCC: omitted. Commands serialize per project with an in-process lock (glossary deviation).

Aggregates and supporting types

ConceptPersistence todayBadge
SiteImageAirtable site_images (domain_id = SiteImageId, ADR-0048)Implemented in api-v1
ReportDateValue object on Site Image (YYYY-MM-DD)Implemented in api-v1
SiteImageAttachmentValue object — exactly one http(s) imageImplemented in api-v1
ProjectIdReference only (projects.domain_id; product orders resolve projects_sync.domain_id)Implemented in api-v1
ProductOrderAirtable product_order (domain_id = ProductOrderId)Implemented in api-v1
ProductOrderItemAirtable product_order_items (entity inside ProductOrder)Implemented in api-v1
InvoiceTotals, TaxRate, ShippingCharge, BillTo, InvoiceNumber, InvoiceDocumentValue objects frozen on the product_order header at issueImplemented in api-v1
OrderQuantity, Coverage, ProductSnapshot, ProductDetails, SourceStatusValue objects on the itemImplemented in api-v1

room, notes, and issueNumber exist on the Airtable table and the aggregate but are not part of the bulk-upload form.

Not modeled yet

Do not invent catalog pages for:

  • Estimates
  • Bill of materials (BOM): owned by project-delivery-graph and only read here
  • Schedules
  • Material requirements, procurement packages and purchase orders (the enterprise workflow, PRD 05 §3A; ADR-0133)
  • The Part 05 invoice aggregate (due, partially paid, void), which is deferred
  • A GET collection of product orders: the Airtable Interface is the list view
  • Change orders, RFIs, submittals, budgets, or other construction execution aggregates
  • A Project aggregate owned by this domain
  • A SiteImageBatch aggregate (N images is a use-case concern)

Primary Code References

  • Module root: apps/api-v1/src/modules/construction-management
  • Glossary: docs/api-v1/construction-management/GLOSSARY.md (module GLOSSARY.md only points there)
  • Aggregate: apps/api-v1/src/modules/construction-management/core/domain/site-image/SiteImage.ts
  • Use case: apps/api-v1/src/modules/construction-management/core/application/use-cases/QueueBulkUploadSiteImagesUseCase.ts
  • OpenAPI: apps/api-v1/src/modules/construction-management/adapters/inbound/rest/site-images/siteImages.openapi.ts
  • Product Order aggregate: apps/api-v1/src/modules/construction-management/core/domain/product-order/ProductOrder.ts
  • Product Order use cases: apps/api-v1/src/modules/construction-management/core/application/use-cases/product-order/
  • Product Order OpenAPI: apps/api-v1/src/modules/construction-management/adapters/inbound/rest/product-orders/productOrders.openapi.ts
  • Airtable scripts: apps/airtable-front-end/current/construction-management/{bom_line_actions,product_order_actions}.js
  • Composition: apps/api-v1/src/modules/construction-management/composition/construction-management.composition.ts
  • Airtable base id: appQV2WVHMyLGkNx0 (AIRTABLE_CONSTRUCTION_BASE_ID)

HTTP surface

Canonical mount: /api/v1/construction.

MethodPathOpenAPI operationIdBehavior
POST/site-images/actions/bulk-uploadqueueBulkUploadSiteImagesAccept N photos; HTTP 202; poll status
GET/operations/:operationIdgetSiteImageBulkUploadOperationPoll { operationId, status, message }

Idempotency-Key is required on the POST. The 202 body includes operationId and statusUrl. requestRecordId is HTTP/adapter-only (the Airtable ui-row id) and must not appear on SiteImage. Polling requires requestRecordId on the original POST so there is a ui row to look up.

Product orders, under /projects/:projectId/product-orders:

MethodPathOpenAPI operationId
POST/projects/:projectId/product-orderscreateProductOrder
GET/projects/:projectId/product-orders/:orderIdgetProductOrder
POST/projects/:projectId/product-orders/:orderId/actions/issueissueProductOrder
POST/projects/:projectId/product-orders/:orderId/actions/mark-paidmarkProductOrderPaid
POST/projects/:projectId/product-orders/:orderId/actions/cancelcancelProductOrder
POST/projects/:projectId/product-orders/:orderId/actions/set-shippingsetProductOrderShipping
POST/projects/:projectId/product-orders/:orderId/actions/print-invoiceprintProductOrderInvoice
POST/projects/:projectId/product-orders/actions/refresh-source-statusrefreshProductOrderSourceStatus
POST/projects/:projectId/product-orders/actions/update-from-bomupdateProductOrdersFromBom
POST/projects/:projectId/product-orders/bulk-deletebulkDeleteProductOrderContents

Do not invent additional operation ids. Link generated OpenAPI rather than copying request/response schemas.

Open Questions

  • When estimates, schedules or the enterprise procurement workflow are modeled, they belong in this bounded context. They are not documented here until domain types exist.
  • Product Order defaults still awaiting confirmation: the order-name format, and the rule that an issued invoice’s project address needs a US postal code.
  • Whether a future Project aggregate lives in this domain, project-coordination, or another owner. Today this module only stores ProjectId.