Purchase Order Orchestration
Compose scheduled API input, synchronous API calls, broker request-response, triggered and scheduled aggregation, shipment events, and generated operational views.
Exampleintegrationorchestrationapibrokerrequest-responseaggregationcontextuidashboardscheduling
🌅 Horizon
Example at a Glance
Overview
Purchase orders often arrive through more than one interaction style. This example starts with a scheduled producer, gathers complementary orders through an API and a broker request-response topic, then routes the resulting business flow to Warehouse and Shipment independently. A second scheduled aggregate polls two purchase-order providers and delivers their collected orders to Supplier Service. Supplier records them, maps them to a shipment request, and publishes the request for Shipment Service through the broker. PO4 and PO5 use different provider contracts; the integration maps both into the canonical purchase-order model before aggregation.
Architecture
What It Demonstrates
@integration— an explicit transport-independent orchestration boundary withdirection: in/out.@serviceand a reusable schedule — PO Service 1 creates and pushes PO-1 every thirty seconds.@api— an inbound integration API, the PO-2 request-response API, and read APIs for monitoring.@broker— PO-3 broker request-response and the decoupledShipmentRequestedevent.aggregate— logical gathering of PO-1, PO-2, and PO-3 before the Warehouse handoff.- A scheduled
aggregate— every twenty-five seconds, PO-4 and PO-5 are collected and delivered to Supplier Service without an API response. usingmappers — the integration translatesPurchaseOrderFourandPurchaseOrderFiveinto the canonicalPurchaseOrderat its boundary.@context,@dashboard, and@ui— stateful operational views generated from DSL.
Expected Result
The model shows a clear orchestration contract: PO-1 is the trigger, PO-2 and PO-3 are gathered through different interaction styles, the aggregate reaches Warehouse, and Shipment receives only the dedicated shipment event. A separate scheduled aggregate maps provider-specific orders before it replenishes Supplier. The UI reads current state from each owning service.
🧭 Voyage
1. End-to-End Sequence
The sequence intentionally contrasts API request-response, broker request-response, and one-way event publication.
2. Problem and Constraints
The scenario must preserve ownership: services define their contracts, the integration defines the cross-contract flow, Warehouse owns warehouse state, and Shipment owns shipment state. The DSL must not expose a particular HTTP client, broker client, correlation-id implementation, or scheduler library.
3. Example Structure
0014-purchase-order-orchestration/
├── 00-poo-info.ocn
├── 01-poo-context-warehouse.ocn
├── 01-poo-context-shipment.ocn
├── 01-poo-context-supplier.ocn
├── 10-poo-datatype.ocn
├── 20-poo-api-ingress.ocn
├── 20-poo-api-purchase-order.ocn
├── 20-poo-api-purchase-order-4.ocn
├── 20-poo-api-purchase-order-5.ocn
├── 20-poo-api-warehouse.ocn
├── 20-poo-api-shipment-monitoring.ocn
├── 20-poo-api-supplier.ocn
├── 20-poo-broker.ocn
├── 30-poo-config.ocn
├── 40-poo-expression.ocn
├── expression/purchase-order-generator.go
├── 50-poo-service-1.ocn
├── 50-poo-service-2.ocn
├── 50-poo-service-3.ocn
├── 50-poo-service-4.ocn
├── 50-poo-service-5.ocn
├── 50-poo-service-warehouse.ocn
├── 50-poo-service-shipment.ocn
├── 50-poo-service-supplier.ocn
├── 50-poo-integration.ocn
├── 55-poo-dashboard.ocn
├── 56-poo-ui.ocn
├── 60-poo-deploy.ocn
├── example.md
└── example-info/example-info.html
4. Start with a Scheduled API Push
PO Service 1 owns the cadence. The integration starts when its host service receives PO-1 through the implemented ingress API.
schedule EveryThirtySeconds every 30s overlap skip
connect generate -> ingress.receivePurchaseOrder schedule EveryThirtySeconds
5. Compose API and Broker Request-Response
The integration uses PO Service 2 through an API and PO Service 3 through the broker's request-response topic. Correlation, reply routing, timeout enforcement, and protocol-specific implementation are runtime concerns; the DSL expresses only the logical dependency.
PurchaseOrderIntegration
direction: in/out
impl api PurchaseOrderIngressApi as ingress on cfg.apiConfig.port
use api PurchaseOrderServiceApi as po2
use broker PurchaseOrderBroker as orders
aggregate ProcessPurchaseOrders
trigger ingress.receivePurchaseOrder
result: List<PurchaseOrder>
collect trigger
collect po2.generatePurchaseOrder
collect orders.purchase.order.po3
deliver warehouse.receivePurchaseOrders
PurchaseOrderOrchestrator
impl integration PurchaseOrderIntegration as orchestration
6. Run an Aggregate on a Schedule
The same integration also owns a flow with no inbound API trigger. Every
twenty-five seconds it requests one order from each dedicated provider,
gathers the results, maps each provider contract to the canonical
purchase order, and delivers them to Supplier Service. Because the
activation is a schedule, the aggregate deliberately has no
respond clause. Supplier records the orders and then
publishes the existing shipment event through the broker.
schedule SupplierReplenishment every 25s
aggregate ReplenishSupplier
schedule SupplierReplenishment
result: List<PurchaseOrder>
collect po4.generatePurchaseOrder using mapPo4
collect po5.generatePurchaseOrder using mapPo5
deliver supplier.receivePurchaseOrders
SupplierService
use broker PurchaseOrderBroker as orders
use expression StoreSupplierPurchaseOrders as store
use expression PurchaseOrdersToShipmentRequest as shipmentRequest
connect api.receivePurchaseOrders -> store()
connect api.receivePurchaseOrders -shipmentRequest-> orders.shipment.request
7. Publish the Shipment Event
Shipment receives the business event from the broker and writes its own state. It does not read Warehouse's context or depend on a Warehouse API. This isolates shipment processing from the warehouse implementation.
connect orders.shipment.request -> record()
8. Monitor Each Owned State
The UI has three dashboards. Each table requests a paged, flat display record from the service that owns the state. This keeps nested purchase orders out of raw table cells while preserving service ownership.
connect warehouse.WarehouseOrders.listRows -> WarehouseApi.listPurchaseOrderRows
connect shipment.ShipmentRequests.listRows -> ShipmentMonitoringApi.listShipmentRequestRows
connect supplier.SupplierOrders.listRows -> SupplierApi.listPurchaseOrderRows
9. Runtime Boundary
PurchaseOrderIntegration is not deployed independently.
PurchaseOrderOrchestrator hosts the module and is deployed
like any other service. The generated host owns the API listener,
configuration, and process lifecycle, while the module owns the logical
API, broker, and aggregation flows.
10. Experiments
- Change the PO-1 interval and compare its overlap policy.
- Turn PO-2 into another broker request-response dependency and compare the model.
- Add a timeout or retry policy when the integration runtime model introduces those logical capabilities.
- Add an aggregated-order dashboard while keeping Warehouse as the sole owner of warehouse state.
- Change the supplier schedule and observe the generated scheduler configuration.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - integration
# - purchase-order
# - orchestration
# perspective:
# feature: purchase-order-orchestration
# @ocean-meta-end
@info
name: Purchase Order Orchestration
version: 1.0.0
title: PurchaseOrderOrchestration
subtitle: API, broker request-response, aggregation, and operational monitoring
shortDescription: A complete integration-oriented purchase-order scenario
description: PO Service 1 creates an order every thirty seconds. The integration gathers complementary orders from PO Service 2 through an API and PO Service 3 through a broker request-response topic, sends an aggregate to Warehouse, and publishes a shipment event. A second aggregate runs every twenty-five seconds, normalizes provider-specific orders from PO Services 4 and 5, and delivers them to Supplier Service.
# @ocean-meta-start
# tags:
# - context
# - shipment
# perspective:
# feature: purchase-order-orchestration
# service: shipment-service
# @ocean-meta-end
@context
ShipmentContext
shipmentRequests : List<ShipmentRequest>
# @ocean-meta-start
# tags:
# - context
# - supplier
# perspective:
# feature: purchase-order-orchestration
# service: supplier-service
# @ocean-meta-end
@context
SupplierContext
purchaseOrders : List<PurchaseOrder>
# @ocean-meta-start
# tags:
# - context
# - warehouse
# perspective:
# feature: purchase-order-orchestration
# service: warehouse-service
# @ocean-meta-end
@context
WarehouseContext
purchaseOrders : List<PurchaseOrder>
# @ocean-meta-start
# tags:
# - datatype
# - purchase-order
# - shipment
# perspective:
# feature: purchase-order-orchestration
# service: shared
# @ocean-meta-end
@datatype
PurchaseItem
id pattern PI-UUID
orderName : String
quantity : Int
PurchaseOrder
id pattern PO-UUID
orders : List<PurchaseItem>
source : String
destination : String
# Provider-specific contracts are deliberately different from the canonical
# purchase-order model. PurchaseOrderIntegration normalizes them before
# aggregation and delivery to Supplier Service.
PurchaseItemFour
supplierSku : String
units : Int
PurchaseOrderFour
reference : String
lineItems : List<PurchaseItemFour>
origin : String
shipTo : String
PurchaseItemFive
productCode : String
amount : Int
PurchaseOrderFive
confirmationId : String
products : List<PurchaseItemFive>
dispatchFrom : String
deliveryTo : String
ShipmentRequest
id pattern SR-UUID
source : String
destination : String
purchaseOrders : List<PurchaseOrder>
WarehouseOverview
warehouseId : String
timestamp : DateTime
purchaseOrders : List<PurchaseOrder>
ShipmentOverview
timestamp : DateTime
shipmentRequests : List<ShipmentRequest>
SupplierOverview
supplierId : String
timestamp : DateTime
purchaseOrders : List<PurchaseOrder>
# Flat, presentation-oriented records used by the generated operational
# tables. They deliberately avoid nesting List values inside a table cell.
PurchaseOrderDashboardRow
id : String
source : String
destination : String
itemCount : Int
itemsSummary : String
ShipmentRequestDashboardRow
id : String
source : String
destination : String
purchaseOrderCount : Int
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - ingress
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-orchestrator
# @ocean-meta-end
@api
PurchaseOrderIngressApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /purchase-orders
generateSwagger = true
post /order receivePurchaseOrder(order:PurchaseOrder) : String
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - purchase-order
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-4
# @ocean-meta-end
@api
PurchaseOrderService4Api style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /purchase-orders-4
generateSwagger = true
get /next generatePurchaseOrder() : PurchaseOrderFour
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - purchase-order
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-5
# @ocean-meta-end
@api
PurchaseOrderService5Api style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /purchase-orders-5
generateSwagger = true
get /next generatePurchaseOrder() : PurchaseOrderFive
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - purchase-order
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-2
# @ocean-meta-end
@api
PurchaseOrderServiceApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /purchase-orders
generateSwagger = true
get /next generatePurchaseOrder() : PurchaseOrder
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - shipment
# perspective:
# feature: purchase-order-orchestration
# service: shipment-service
# @ocean-meta-end
@api
ShipmentMonitoringApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /shipments
generateSwagger = true
get /overview getShipmentOverview() : ShipmentOverview
get /requests/{offset}/{limit} listShipmentRequestRows(offset:Int, limit:Int) : List<ShipmentRequestDashboardRow>
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - supplier
# perspective:
# feature: purchase-order-orchestration
# service: supplier-service
# @ocean-meta-end
@api
SupplierApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /supplier
generateSwagger = true
post /orders receivePurchaseOrders(orders:List<PurchaseOrder>) : Void
get /overview getSupplierOverview() : SupplierOverview
get /orders/{offset}/{limit} listPurchaseOrderRows(offset:Int, limit:Int) : List<PurchaseOrderDashboardRow>
# @ocean-meta-start
# tags:
# - api
# - rest-api
# - warehouse
# perspective:
# feature: purchase-order-orchestration
# service: warehouse-service
# @ocean-meta-end
@api
WarehouseApi style:rest
engine = gin
configType = ApiConfig
version = 1.0.0
basePath = /warehouse
generateSwagger = true
post /orders receivePurchaseOrders(orders:List<PurchaseOrder>) : Void
get /overview getWarehouseOverview() : WarehouseOverview
get /orders/{offset}/{limit} listPurchaseOrderRows(offset:Int, limit:Int) : List<PurchaseOrderDashboardRow>
# @ocean-meta-start
# tags:
# - broker
# - nats
# - messaging
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-broker
# @ocean-meta-end
@broker
PurchaseOrderBroker
@perspectives: version:0.1.0, lifestyle:stable
engine = nats
configType = PurchaseOrderBrokerConfig
tags = primary, shared
purchase.order.po3 : _-PurchaseOrder as request-response with timeout:5s
shipment.request : ShipmentRequest as event
# @ocean-meta-start
# tags:
# - configuration
# - integration
# perspective:
# feature: purchase-order-orchestration
# service: shared
# @ocean-meta-end
@config
@import config O.broker.nats.config@1.0.0 as BrokerConfig
@import config O.log.config@1.0.0 as LogConfig
ApiConfig
port : Int (default=8080)
CommonServiceConfig
apiConfig : ApiConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
PurchaseOrderBrokerConfig
brokerConfig : BrokerConfig
logConfig : LogConfig
UiConfig
port : Int (default=8080)
logConfig : LogConfig
# @ocean-meta-start
# tags:
# - expression
# - mapping
# - aggregation
# perspective:
# feature: purchase-order-orchestration
# service: shared
# @ocean-meta-end
@expression
GeneratePurchaseOrder
input: _
output: order:PurchaseOrder
external
source: purchase-order-generator.go
method: GeneratePurchaseOrder
GeneratePurchaseOrderFour
input: _
output: order:PurchaseOrderFour
external
source: purchase-order-generator.go
method: GeneratePurchaseOrderFour
GeneratePurchaseOrderFive
input: _
output: order:PurchaseOrderFive
external
source: purchase-order-generator.go
method: GeneratePurchaseOrderFive
MapPurchaseOrderFour
input: source:PurchaseOrderFour
output: result:PurchaseOrder
external
source: purchase-order-generator.go
method: MapPurchaseOrderFour
MapPurchaseOrderFive
input: source:PurchaseOrderFive
output: result:PurchaseOrder
external
source: purchase-order-generator.go
method: MapPurchaseOrderFive
AppendPurchaseOrders
input: existing:List<PurchaseOrder> & incoming:List<PurchaseOrder>
output: result:List<PurchaseOrder>
external
source: purchase-order-generator.go
method: AppendPurchaseOrders
AppendShipmentRequest
input: existing:List<ShipmentRequest> & incoming:ShipmentRequest
output: result:List<ShipmentRequest>
external
source: purchase-order-generator.go
method: AppendShipmentRequest
PurchaseOrdersToShipmentRequest
input: orders:List<PurchaseOrder>
output: request:ShipmentRequest
external
source: purchase-order-generator.go
method: PurchaseOrdersToShipmentRequest
PurchaseOrdersToAcknowledgement
input: orders:List<PurchaseOrder>
output: acknowledgement:String
external
source: purchase-order-generator.go
method: PurchaseOrdersToAcknowledgement
ToPurchaseOrderDashboardRows
input: orders:List<PurchaseOrder> & offset:Int & limit:Int
output: rows:List<PurchaseOrderDashboardRow>
external
source: purchase-order-generator.go
method: ToPurchaseOrderDashboardRows
ToShipmentRequestDashboardRows
input: requests:List<ShipmentRequest> & offset:Int & limit:Int
output: rows:List<ShipmentRequestDashboardRow>
external
source: purchase-order-generator.go
method: ToShipmentRequestDashboardRows
StorePurchaseOrders
context: ctx:*WarehouseContext
input: orders:List<PurchaseOrder>
logic
ctx.purchaseOrders = AppendPurchaseOrders(ctx.purchaseOrders, orders)
StoreSupplierPurchaseOrders
context: ctx:*SupplierContext
input: orders:List<PurchaseOrder>
logic
ctx.purchaseOrders = AppendPurchaseOrders(ctx.purchaseOrders, orders)
RecordShipment
context: ctx:*ShipmentContext
input: shipment:ShipmentRequest
logic
ctx.shipmentRequests = AppendShipmentRequest(ctx.shipmentRequests, shipment)
GetWarehouseOverview
context: ctx:*WarehouseContext
input: _
output: result:WarehouseOverview
logic
var overview WarehouseOverview
overview.warehouseId = "Warehouse-01"
overview.timestamp = NowUTC()
overview.purchaseOrders = ctx.purchaseOrders
result = overview
GetSupplierOverview
context: ctx:*SupplierContext
input: _
output: result:SupplierOverview
logic
var overview SupplierOverview
overview.supplierId = "Supplier-01"
overview.timestamp = NowUTC()
overview.purchaseOrders = ctx.purchaseOrders
result = overview
GetShipmentOverview
context: ctx:*ShipmentContext
input: _
output: result:ShipmentOverview
logic
var overview ShipmentOverview
overview.timestamp = NowUTC()
overview.shipmentRequests = ctx.shipmentRequests
result = overview
ListWarehousePurchaseOrderRows
context: ctx:*WarehouseContext
input: offset:Int & limit:Int
output: rows:List<PurchaseOrderDashboardRow>
logic
rows = ToPurchaseOrderDashboardRows(ctx.purchaseOrders, offset, limit)
ListSupplierPurchaseOrderRows
context: ctx:*SupplierContext
input: offset:Int & limit:Int
output: rows:List<PurchaseOrderDashboardRow>
logic
rows = ToPurchaseOrderDashboardRows(ctx.purchaseOrders, offset, limit)
ListShipmentRequestRows
context: ctx:*ShipmentContext
input: offset:Int & limit:Int
output: rows:List<ShipmentRequestDashboardRow>
logic
rows = ToShipmentRequestDashboardRows(ctx.shipmentRequests, offset, limit)
# @ocean-meta-start
# tags:
# - integration
# - orchestration
# - aggregation
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-orchestrator
# @ocean-meta-end
# This is a reusable logical orchestration module. A service hosts it and
# provides the process/deployment boundary for its ingress API.
@integration
PurchaseOrderIntegration
direction: in/out
use config CommonServiceConfig as cfg
impl api PurchaseOrderIngressApi as ingress on cfg.apiConfig.port
use api PurchaseOrderServiceApi as po2
use api PurchaseOrderService4Api as po4
use api PurchaseOrderService5Api as po5
use api WarehouseApi as warehouse
use api SupplierApi as supplier
use expression MapPurchaseOrderFour as mapPo4
use expression MapPurchaseOrderFive as mapPo5
use broker PurchaseOrderBroker as orders
use expression PurchaseOrdersToShipmentRequest as shipmentRequest
use expression PurchaseOrdersToAcknowledgement as acknowledgement
# PO-1 arrives through the ingress API. PO-2 is fetched synchronously and
# PO-3 through the broker's request-response topic. The three orders are
# collected into one list, stored by Warehouse, then published as a
# ShipmentRequest event.
aggregate ProcessPurchaseOrders
trigger ingress.receivePurchaseOrder
result: List<PurchaseOrder>
collect trigger
collect po2.generatePurchaseOrder
collect orders.purchase.order.po3
deliver warehouse.receivePurchaseOrders
deliver orders.shipment.request using shipmentRequest
respond using acknowledgement
schedule SupplierReplenishment every 25s
# A scheduled aggregate has no API response. It polls two independent
# providers and sends their collected orders to Supplier Service.
aggregate ReplenishSupplier
schedule SupplierReplenishment
result: List<PurchaseOrder>
collect po4.generatePurchaseOrder using mapPo4
collect po5.generatePurchaseOrder using mapPo5
deliver supplier.receivePurchaseOrders
# @ocean-meta-start
# tags:
# - service
# - purchase-order
# - scheduling
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-1
# @ocean-meta-end
@service
PurchaseOrderService1
use config CommonServiceConfig as svcCfg
use api PurchaseOrderIngressApi as ingress
use expression GeneratePurchaseOrder as generate
schedule EveryThirtySeconds every 30s overlap skip
connect generate -> ingress.receivePurchaseOrder schedule EveryThirtySeconds
# @ocean-meta-start
# tags:
# - service
# - purchase-order
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-2
# @ocean-meta-end
@service
PurchaseOrderService2
use config CommonServiceConfig as svcCfg
impl api PurchaseOrderServiceApi as api on svcCfg.apiConfig.port
use expression GeneratePurchaseOrder as generate
connect api.generatePurchaseOrder -> generate
# @ocean-meta-start
# tags:
# - service
# - purchase-order
# - messaging
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-3
# @ocean-meta-end
@service
PurchaseOrderService3
use config CommonServiceConfig as svcCfg
use broker PurchaseOrderBroker as orders
use expression GeneratePurchaseOrder as generate
connect orders.purchase.order.po3 -> generate
# @ocean-meta-start
# tags:
# - service
# - purchase-order
# - supplier
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-4
# @ocean-meta-end
@service
PurchaseOrderService4
use config CommonServiceConfig as svcCfg
impl api PurchaseOrderService4Api as api on svcCfg.apiConfig.port
use expression GeneratePurchaseOrderFour as generate
connect api.generatePurchaseOrder -> generate
# @ocean-meta-start
# tags:
# - service
# - purchase-order
# - supplier
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-service-5
# @ocean-meta-end
@service
PurchaseOrderService5
use config CommonServiceConfig as svcCfg
impl api PurchaseOrderService5Api as api on svcCfg.apiConfig.port
use expression GeneratePurchaseOrderFive as generate
connect api.generatePurchaseOrder -> generate
# @ocean-meta-start
# tags:
# - service
# - integration-host
# - orchestration
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-orchestrator
# @ocean-meta-end
@service
# The service is intentionally thin: it hosts the reusable integration module
# and supplies the process/deployment boundary for its ingress API.
PurchaseOrderOrchestrator
impl integration PurchaseOrderIntegration as orchestration
# @ocean-meta-start
# tags:
# - service
# - shipment
# - messaging
# perspective:
# feature: purchase-order-orchestration
# service: shipment-service
# @ocean-meta-end
@service
ShipmentService
use config CommonServiceConfig as svcCfg
impl api ShipmentMonitoringApi as api on svcCfg.apiConfig.port
use broker PurchaseOrderBroker as orders
use context ShipmentContext as shipmentCtx
use expression RecordShipment as record
use expression GetShipmentOverview as overview
use expression ListShipmentRequestRows as rows
connect orders.shipment.request -> record()
connect api.getShipmentOverview -> overview()
connect api.listShipmentRequestRows -> rows
# @ocean-meta-start
# tags:
# - service
# - supplier
# - scheduling
# perspective:
# feature: purchase-order-orchestration
# service: supplier-service
# @ocean-meta-end
@service
SupplierService
use config CommonServiceConfig as svcCfg
impl api SupplierApi as api on svcCfg.apiConfig.port
use broker PurchaseOrderBroker as orders
use context SupplierContext as supplierCtx
use expression StoreSupplierPurchaseOrders as store
use expression PurchaseOrdersToShipmentRequest as shipmentRequest
use expression GetSupplierOverview as overview
use expression ListSupplierPurchaseOrderRows as rows
connect api.receivePurchaseOrders -> store()
connect api.receivePurchaseOrders -shipmentRequest-> orders.shipment.request
connect api.getSupplierOverview -> overview()
connect api.listPurchaseOrderRows -> rows
# @ocean-meta-start
# tags:
# - service
# - warehouse
# - aggregation
# perspective:
# feature: purchase-order-orchestration
# service: warehouse-service
# @ocean-meta-end
@service
WarehouseService
use config CommonServiceConfig as svcCfg
impl api WarehouseApi as api on svcCfg.apiConfig.port
use context WarehouseContext as warehouseCtx
use expression StorePurchaseOrders as store
use expression GetWarehouseOverview as overview
use expression ListWarehousePurchaseOrderRows as rows
connect api.receivePurchaseOrders -> store()
connect api.getWarehouseOverview -> overview()
connect api.listPurchaseOrderRows -> rows
# @ocean-meta-start
# tags:
# - dashboard
# - operations-monitoring
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-operations-ui
# @ocean-meta-end
@dashboard
WarehouseDashboard
title: Warehouse Monitor
layout: SingleColumnLayout
Widget WarehouseGuide of type Text
title = Aggregated purchase orders
subtitle = Latest state received by Warehouse Service
content = <p>Purchase orders are rendered as operational rows; nested line items are summarized for quick scanning.</p>
Widget WarehouseOrders of type Table
title = Warehouse purchase orders
datatype = PurchaseOrderDashboardRow
columns:
- id
- source
- destination
- itemCount
- itemsSummary
sortable = true
limit = 10
ShipmentDashboard
title: Shipment Monitor
layout: SingleColumnLayout
Widget ShipmentGuide of type Text
title = Shipment events
subtitle = Latest ShipmentRequest event consumed from the broker
content = <p>Shipment requests are shown as rows with their route and purchase-order count.</p>
Widget ShipmentRequests of type Table
title = Shipment requests
datatype = ShipmentRequestDashboardRow
columns:
- id
- source
- destination
- purchaseOrderCount
sortable = true
limit = 10
SupplierDashboard
title = Supplier Monitor
layout: SingleColumnLayout
Widget SupplierGuide of type Text
title = Scheduled supplier replenishment
subtitle = Orders collected every 25 seconds from Purchase Order Services 4 and 5
content = <p>Orders delivered by the scheduled aggregate are rendered as searchable operational rows.</p>
Widget SupplierOrders of type Table
title = Supplier purchase orders
datatype = PurchaseOrderDashboardRow
columns:
- id
- source
- destination
- itemCount
- itemsSummary
sortable = true
limit = 10
# @ocean-meta-start
# tags:
# - ui
# - operations-monitoring
# perspective:
# feature: purchase-order-orchestration
# service: purchase-order-operations-ui
# @ocean-meta-end
@ui
PurchaseOrderOperationsUi
@perspectives: version:1.0.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Warehouse dashboard = WarehouseDashboard
nav Shipment dashboard = ShipmentDashboard
nav Supplier dashboard = SupplierDashboard
header title = Purchase Order Operations
header subtitle = Monitor warehouse, shipment, and scheduled supplier flows
header align = center
footer title = Built with Ocean-lab
footer subtitle = Purchase-order integration example
footer align = center
use dashboard WarehouseDashboard as warehouse
use dashboard ShipmentDashboard as shipment
use dashboard SupplierDashboard as supplier
use api WarehouseApi
use api ShipmentMonitoringApi
use api SupplierApi
connect warehouse.WarehouseOrders.listRows -> WarehouseApi.listPurchaseOrderRows
connect shipment.ShipmentRequests.listRows -> ShipmentMonitoringApi.listShipmentRequestRows
connect supplier.SupplierOrders.listRows -> SupplierApi.listPurchaseOrderRows
# @ocean-meta-start
# tags:
# - deployment
# - orchestration
# perspective:
# feature: purchase-order-orchestration
# service: shared
# @ocean-meta-end
@deploy
Name: PurchaseOrderOrchestrationDeploy
@import service P.broker.nats.docker@1.0.0 as NATS
PurchaseOrderService1Deploy
service PurchaseOrderService1
replica 1
PurchaseOrderService2Deploy
service PurchaseOrderService2
replica 1
export 9092:PurchaseOrderService2.api
PurchaseOrderService4Deploy
service PurchaseOrderService4
replica 1
export 9095:PurchaseOrderService4.api
PurchaseOrderService5Deploy
service PurchaseOrderService5
replica 1
export 9096:PurchaseOrderService5.api
PurchaseOrderService3Deploy
service PurchaseOrderService3
replica 1
dependsOn Broker
PurchaseOrderOrchestratorDeploy
service PurchaseOrderOrchestrator
replica 1
export 9091:PurchaseOrderOrchestrator.ingress
dependsOn Broker, PurchaseOrderService2Deploy, PurchaseOrderService3Deploy, PurchaseOrderService4Deploy, PurchaseOrderService5Deploy, SupplierDeploy
WarehouseDeploy
service WarehouseService
replica 1
export 9093:WarehouseService.api
ShipmentDeploy
service ShipmentService
replica 1
export 9094:ShipmentService.api
dependsOn Broker
SupplierDeploy
service SupplierService
replica 1
export 9097:SupplierService.api
PurchaseOrderBrokerDeploy
service PurchaseOrderBroker
replica 1
dependsOn Broker
Broker
service NATS
PurchaseOrderOperationsUiDeploy
service PurchaseOrderOperationsUi
replica 1
export 8084:PurchaseOrderOperationsUi.config
dependsOn WarehouseDeploy, ShipmentDeploy, SupplierDeploy
package expression
import (
cryptorand "crypto/rand"
"fmt"
"math/rand/v2"
"strings"
)
var purchaseItemNames = []string{
"PO-Industrial-Fasteners",
"PO-Office-Supplies",
"PO-Workshop-Tools",
"PO-Safety-Equipment",
"PO-Packaging-Materials",
}
var purchaseSources = []string{
"Amsterdam Distribution Centre",
"Rotterdam Freight Terminal",
"Utrecht Fulfilment Hub",
}
var purchaseDestinations = []string{
"Eindhoven Assembly Plant",
"Groningen Regional Depot",
}
// GeneratePurchaseOrder creates a meaningful purchase order with one to three
// randomly selected items. It is intentionally independent of a producer
// service, allowing PO Services 1, 2, and 3 to reuse the same capability.
func GeneratePurchaseOrder() (PurchaseOrder, error) {
itemCount := rand.IntN(3) + 1
items := make([]PurchaseItem, 0, itemCount)
for range itemCount {
itemID, err := newPrefixedID("PI")
if err != nil {
return PurchaseOrder{}, err
}
items = append(items, PurchaseItem{
Id: itemID,
OrderName: purchaseItemNames[rand.IntN(len(purchaseItemNames))],
Quantity: rand.IntN(500) + 1,
})
}
orderID, err := newPrefixedID("PO")
if err != nil {
return PurchaseOrder{}, err
}
return PurchaseOrder{
Id: orderID,
Orders: items,
Source: purchaseSources[rand.IntN(len(purchaseSources))],
Destination: purchaseDestinations[rand.IntN(len(purchaseDestinations))],
}, nil
}
// GeneratePurchaseOrderFour represents a provider with its own line-item and
// route vocabulary. The integration maps this contract to PurchaseOrder.
func GeneratePurchaseOrderFour() (PurchaseOrderFour, error) {
reference, err := newPrefixedID("PO4")
if err != nil {
return PurchaseOrderFour{}, err
}
itemCount := rand.IntN(3) + 1
items := make([]PurchaseItemFour, 0, itemCount)
for range itemCount {
items = append(items, PurchaseItemFour{
SupplierSku: fmt.Sprintf("S4-%03d", rand.IntN(900)+100),
Units: rand.IntN(500) + 1,
})
}
return PurchaseOrderFour{
Reference: reference,
LineItems: items,
Origin: purchaseSources[rand.IntN(len(purchaseSources))],
ShipTo: purchaseDestinations[rand.IntN(len(purchaseDestinations))],
}, nil
}
// GeneratePurchaseOrderFive models a second provider whose names and payload
// structure intentionally differ from both PO4 and the canonical model.
func GeneratePurchaseOrderFive() (PurchaseOrderFive, error) {
confirmationID, err := newPrefixedID("PO5")
if err != nil {
return PurchaseOrderFive{}, err
}
itemCount := rand.IntN(3) + 1
products := make([]PurchaseItemFive, 0, itemCount)
for range itemCount {
products = append(products, PurchaseItemFive{
ProductCode: fmt.Sprintf("P5-%s", []string{"AX", "BX", "CX", "DX"}[rand.IntN(4)]),
Amount: rand.IntN(500) + 1,
})
}
return PurchaseOrderFive{
ConfirmationId: confirmationID,
Products: products,
DispatchFrom: purchaseSources[rand.IntN(len(purchaseSources))],
DeliveryTo: purchaseDestinations[rand.IntN(len(purchaseDestinations))],
}, nil
}
// MapPurchaseOrderFour normalizes the PO4 provider contract at the
// integration boundary, before the scheduled aggregate is assembled.
func MapPurchaseOrderFour(source PurchaseOrderFour) (PurchaseOrder, error) {
items := make([]PurchaseItem, 0, len(source.LineItems))
for _, item := range source.LineItems {
id, err := newPrefixedID("PI")
if err != nil {
return PurchaseOrder{}, err
}
items = append(items, PurchaseItem{Id: id, OrderName: item.SupplierSku, Quantity: item.Units})
}
id, err := newPrefixedID("PO")
if err != nil {
return PurchaseOrder{}, err
}
return PurchaseOrder{Id: id, Orders: items, Source: source.Origin, Destination: source.ShipTo}, nil
}
// MapPurchaseOrderFive normalizes the PO5 provider contract at the same
// boundary, retaining the business route while adapting field names.
func MapPurchaseOrderFive(source PurchaseOrderFive) (PurchaseOrder, error) {
items := make([]PurchaseItem, 0, len(source.Products))
for _, item := range source.Products {
id, err := newPrefixedID("PI")
if err != nil {
return PurchaseOrder{}, err
}
items = append(items, PurchaseItem{Id: id, OrderName: item.ProductCode, Quantity: item.Amount})
}
id, err := newPrefixedID("PO")
if err != nil {
return PurchaseOrder{}, err
}
return PurchaseOrder{Id: id, Orders: items, Source: source.DispatchFrom, Destination: source.DeliveryTo}, nil
}
// AppendPurchaseOrders preserves all orders received by Warehouse while
// returning a fresh combined list for assignment to its context.
func AppendPurchaseOrders(existing []PurchaseOrder, incoming []PurchaseOrder) ([]PurchaseOrder, error) {
return append(existing, incoming...), nil
}
// AppendShipmentRequest preserves all shipment requests consumed by Shipment.
func AppendShipmentRequest(existing []ShipmentRequest, incoming ShipmentRequest) ([]ShipmentRequest, error) {
return append(existing, incoming), nil
}
// PurchaseOrdersToShipmentRequest maps one collected purchase-order batch to
// the event consumed by Shipment Service. Every generated order has a source
// and destination; a batch is expected to contain orders for the same route.
func PurchaseOrdersToShipmentRequest(orders []PurchaseOrder) (ShipmentRequest, error) {
if len(orders) == 0 {
return ShipmentRequest{}, fmt.Errorf("cannot create shipment request from no purchase orders")
}
requestID, err := newPrefixedID("SR")
if err != nil {
return ShipmentRequest{}, err
}
return ShipmentRequest{
Id: requestID,
Source: orders[0].Source,
Destination: orders[0].Destination,
PurchaseOrders: orders,
}, nil
}
// PurchaseOrdersToAcknowledgement returns the response for the ingress API
// after the aggregate has completed all required deliveries.
func PurchaseOrdersToAcknowledgement(orders []PurchaseOrder) (string, error) {
return fmt.Sprintf("processed %d purchase order(s)", len(orders)), nil
}
// ToPurchaseOrderDashboardRows converts nested purchase orders into the flat,
// readable rows used by Warehouse and Supplier tables. The line-item summary
// intentionally keeps the dashboard compact while retaining the useful order
// name and quantity information.
func ToPurchaseOrderDashboardRows(orders []PurchaseOrder, offset, limit int) ([]PurchaseOrderDashboardRow, error) {
start, end := pageBounds(len(orders), offset, limit)
rows := make([]PurchaseOrderDashboardRow, 0, end-start)
for _, order := range orders[start:end] {
items := make([]string, 0, len(order.Orders))
for _, item := range order.Orders {
items = append(items, fmt.Sprintf("%s × %d", item.OrderName, item.Quantity))
}
rows = append(rows, PurchaseOrderDashboardRow{
Id: order.Id,
Source: order.Source,
Destination: order.Destination,
ItemCount: len(order.Orders),
ItemsSummary: strings.Join(items, ", "),
})
}
return rows, nil
}
// ToShipmentRequestDashboardRows provides the same flat display contract for
// broker-consumed shipment requests.
func ToShipmentRequestDashboardRows(requests []ShipmentRequest, offset, limit int) ([]ShipmentRequestDashboardRow, error) {
start, end := pageBounds(len(requests), offset, limit)
rows := make([]ShipmentRequestDashboardRow, 0, end-start)
for _, request := range requests[start:end] {
rows = append(rows, ShipmentRequestDashboardRow{
Id: request.Id,
Source: request.Source,
Destination: request.Destination,
PurchaseOrderCount: len(request.PurchaseOrders),
})
}
return rows, nil
}
// pageBounds normalizes UI pagination inputs and returns a safe half-open
// range. A non-positive limit uses the remaining records, which makes the
// mapper safe for direct API use as well as generated table requests.
func pageBounds(total, offset, limit int) (int, int) {
if offset < 0 {
offset = 0
}
if offset >= total {
return total, total
}
if limit <= 0 || limit > total-offset {
limit = total - offset
}
return offset, offset + limit
}
// newPrefixedID produces IDs compatible with the DSL's <prefix>-UUID pattern
// without taking a dependency on a third-party UUID package.
func newPrefixedID(prefix string) (string, error) {
bytes := make([]byte, 16)
if _, err := cryptorand.Read(bytes); err != nil {
return "", fmt.Errorf("generate %s identifier: %w", prefix, err)
}
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
return fmt.Sprintf("%s-%08x-%04x-%04x-%04x-%012x",
prefix,
bytes[0:4],
bytes[4:6],
bytes[6:8],
bytes[8:10],
bytes[10:16],
), nil
}