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.
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_orderandproduct_order_items. Steward actions go through the throwaway AT-A1 tableui_product_order_actions, run byproduct_order_actions.js. Create Order runs frombom_line_actions.js. - Billing facts: project number, name, address and tax rate are read from
projects_sync, a sync of Party Directoryprojects. - 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
| Concept | Persistence today | Badge |
|---|---|---|
SiteImage | Airtable site_images (domain_id = SiteImageId, ADR-0048) | Implemented in api-v1 |
ReportDate | Value object on Site Image (YYYY-MM-DD) | Implemented in api-v1 |
SiteImageAttachment | Value object — exactly one http(s) image | Implemented in api-v1 |
ProjectId | Reference only (projects.domain_id; product orders resolve projects_sync.domain_id) | Implemented in api-v1 |
ProductOrder | Airtable product_order (domain_id = ProductOrderId) | Implemented in api-v1 |
ProductOrderItem | Airtable product_order_items (entity inside ProductOrder) | Implemented in api-v1 |
InvoiceTotals, TaxRate, ShippingCharge, BillTo, InvoiceNumber, InvoiceDocument | Value objects frozen on the product_order header at issue | Implemented in api-v1 |
OrderQuantity, Coverage, ProductSnapshot, ProductDetails, SourceStatus | Value objects on the item | Implemented 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
GETcollection of product orders: the Airtable Interface is the list view - Change orders, RFIs, submittals, budgets, or other construction execution aggregates
- A
Projectaggregate owned by this domain - A
SiteImageBatchaggregate (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(moduleGLOSSARY.mdonly 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.
| Method | Path | OpenAPI operationId | Behavior |
|---|---|---|---|
POST | /site-images/actions/bulk-upload | queueBulkUploadSiteImages | Accept N photos; HTTP 202; poll status |
GET | /operations/:operationId | getSiteImageBulkUploadOperation | Poll { 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:
| Method | Path | OpenAPI operationId |
|---|---|---|
POST | /projects/:projectId/product-orders | createProductOrder |
GET | /projects/:projectId/product-orders/:orderId | getProductOrder |
POST | /projects/:projectId/product-orders/:orderId/actions/issue | issueProductOrder |
POST | /projects/:projectId/product-orders/:orderId/actions/mark-paid | markProductOrderPaid |
POST | /projects/:projectId/product-orders/:orderId/actions/cancel | cancelProductOrder |
POST | /projects/:projectId/product-orders/:orderId/actions/set-shipping | setProductOrderShipping |
POST | /projects/:projectId/product-orders/:orderId/actions/print-invoice | printProductOrderInvoice |
POST | /projects/:projectId/product-orders/actions/refresh-source-status | refreshProductOrderSourceStatus |
POST | /projects/:projectId/product-orders/actions/update-from-bom | updateProductOrdersFromBom |
POST | /projects/:projectId/product-orders/bulk-delete | bulkDeleteProductOrderContents |
Do not invent additional operation ids. Link generated OpenAPI rather than copying request/response schemas.
Documentation Links
- api-v1 service:
/docs/services/api-v1 - Ubiquitous language:
/docs/domains/construction-management/language - Backend platform:
/docs/systems/backend-platform - ADRs:
docs/adr/README.md
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
Projectaggregate lives in this domain,project-coordination, or another owner. Today this module only storesProjectId.