enterprise-service is a Quarkus service that owns cXML PunchOut/PurchaseOrder connectors and ERP integration adapters. Multi-store organizational hierarchy is owned by store-service. PunchOut cart sessions are held in cart-service under a dedicated session type. cXML buyer credentials and ERP API keys are stored in Vault.
cXML PunchOut#
PunchOut enables business buyers to browse the SS3 catalog from within their eProcurement system (SAP Ariba, Coupa, Oracle, etc.) and return a populated cart for procurement approval.
Flow#
- The buyer’s procurement system sends a cXML PunchOut Setup Request to
POST /cxml/punchout/setup. - enterprise-service validates the request using the buyer’s shared secret (HMAC verification against
cxml_buyer_configs). - A
PUNCHOUTcart session is created in cart-service. A session token is stored in Redis with a 4-hour TTL. - A cXML PunchOut Setup Response is returned containing a tokenized storefront URL.
- The buyer browses the storefront. The
PUNCHOUTsession type signals the storefront to render in PunchOut mode. - When the buyer clicks “Return Cart”, the storefront calls
CompletePunchOut. enterprise-service builds a cXML OrderMessage from the cart contents and returns it to the storefront, which POSTs it back to the buyer’sBrowserFormPostURL. - The PunchOut session is expired. No SS3 order is created at this stage.
PunchOut sessions are held entirely in Redis (key: punchout_session:{token}, TTL = 4 hours). There is no database table for in-flight sessions.
cXML Purchase Orders#
After procurement approval, the buyer’s system sends a cXML PurchaseOrder to POST /cxml/order.
enterprise-service:
- Parses and authenticates the cXML document (shared secret)
- Inserts a
purchase_ordersrecord withstatus = RECEIVED - Maps buyer
SupplierPartIDvalues to SS3 catalog variant IDs usingpart_id_mappings - Calls order-service
CreateOrdervia gRPC with the translated line items - Updates the PO record with
status = ACCEPTEDand the resultingss3_order_id
Unmapped part IDs or order creation failures set status = REJECTED and return a cXML error response to the buyer.
ERP Integration#
ERP Adapter Interface#
interface ErpConnector {
void pushOrder(Order order);
void pushInvoice(Invoice invoice);
void pushCustomer(Customer customer);
List<InventoryUpdate> pullInventory(String cursor);
List<ProductUpdate> pullProducts(String cursor);
List<PurchaseOrder> pullPurchaseOrders(String cursor);
String connectorType();
}Built-in Adapters#
| Adapter | Notes |
|---|---|
SapConnector | SAP S/4HANA OData APIs |
NetSuiteConnector | NetSuite REST Record API |
DynamicsConnector | Microsoft Dynamics 365 Finance & Operations API |
OdooConnector | Odoo XML-RPC / JSON-RPC API |
Outbound Push#
Triggered by Kafka events. When order.placed, order.invoiced, or customer.created is consumed, the active ERP connector for the store is resolved and the relevant push method is called. Failures are retried with exponential backoff (3 attempts) before being logged as errors.
Inbound Pull (Scheduled)#
A scheduled job runs at a configurable interval per ERP config. It calls the pull method for each configured entity type (INVENTORY, PRODUCT, PURCHASE_ORDER), passes the stored delta cursor, and fans the results out to the appropriate downstream service:
| Pull entity | Target service |
|---|---|
INVENTORY | inventory-service BulkUpdateStock (gRPC) |
PRODUCT | catalog-service BulkUpdateProducts (gRPC) |
PURCHASE_ORDER | Inserted as enterprise purchase orders |
The cursor is updated atomically after each successful pull batch.
Data Model#
cxml_buyer_configs — config_id UUID PK, store_id, name, network_id, domain, is_active, settings JSONB (allowed IPs, timeout overrides). Shared secret in Vault.
purchase_orders — po_id UUID PK, store_id, buyer_po_number, buyer_network_id, ss3_order_id (after successful translation), cxml_payload TEXT (archived), status ENUM (RECEIVED / PROCESSING / ACCEPTED / REJECTED), received_at, processed_at, error_message.
part_id_mappings — mapping_id UUID PK, store_id, buyer_part_id, ss3_variant_id, is_active. UNIQUE on (store_id, buyer_part_id).
erp_configs — config_id UUID PK, store_id, erp_type, display_name, is_active, sync_interval_minutes, settings JSONB (non-sensitive: company ID, API endpoint, environment).
erp_sync_cursors — cursor_id UUID PK, config_id FK, entity_type, last_synced_at, last_cursor, error_count, last_error.
erp_sync_log — per-run record of direction, entity type, record count, status, and any error summary.
Interfaces#
HTTP (cXML)#
POST /cxml/punchout/setup — cXML PunchOut Setup Request/Response
POST /cxml/order — cXML PurchaseOrder receiptThe gateway routes /cxml/* without JWT validation. Authentication is handled by enterprise-service via cXML HMAC shared secrets.
gRPC#
| Method | Caller | Purpose |
|---|---|---|
CompletePunchOut(cartId, sessionToken) | storefront | Build and return cXML OrderMessage |
GetPurchaseOrder(poId) | admin tooling | Read a received PO record |
Kafka Topics#
Consumed#
| Topic | Action |
|---|---|
order.placed | Push order to ERP via ErpConnector |
order.invoiced | Push invoice to ERP |
customer.created | Push customer record to ERP if sync enabled |
Published#
| Topic | Payload |
|---|---|
enterprise.po.received | poId, storeId, buyerPoNumber |
enterprise.po.accepted | poId, storeId, ss3OrderId |
enterprise.po.rejected | poId, storeId, reason |
erp.sync.completed | configId, storeId, entityType, recordCount, status |
Vault Configuration#
| Key path | Purpose |
|---|---|
ss3/kv/enterprise-service/db.url | PostgreSQL JDBC URL |
ss3/kv/enterprise-service/db.username | Database username |
ss3/kv/enterprise-service/db.password | Database password |
ss3/kv/enterprise-service/cxml/{config_id}/shared-secret | Per-buyer cXML HMAC secret |
ss3/kv/enterprise-service/erp/{config_id}/api-key | Per-ERP API key |
ss3/kv/enterprise-service/erp/{config_id}/client-secret | Per-ERP OAuth client secret |
ss3/kv/shared/ | Shared Kafka bootstrap, OTel config, Redis URL |