Skip to main content

Shipping Aggregator — Complete Architecture & Functional Reference

Interactive Guide

This architectural document includes interactive components like the Carrier Payload Simulator and tabbed code blocks to help you understand the integration boundary.

Scope: Moqui shipping-aggregator component as deployed in ADOC
Last updated: 2026-02-20
Primary source: moqui-copy/moqui-framework/runtime/component/shipping-aggregator/


Table of Contents

  1. System Overview
  2. Moqui ↔ OFBiz Integration Boundary
  3. Core Architecture: The Plugin Pattern
  4. Entity Model & Configuration Data
  5. REST API Surface
  6. Authentication & Token Management
  7. Label Generation — Full Lifecycle
  8. Rate Shopping — Full Lifecycle
  9. Label Voiding (Refund) — Full Lifecycle
  10. Webhook / Order Status Callback
  11. Carrier Integration Details
  12. Error Handling & Logging
  13. Configuration Reference

1. System Overview

The Shipping Aggregator is a Moqui framework component that acts as a carrier-agnostic shipping gateway hub. It receives shipping requests from OFBiz OMS (via REST HTTP calls), resolves the correct carrier and configuration from the requesting user's identity, then invokes a carrier-specific implementation, normalizes the response, and returns it to OFBiz.

┌───────────────────────┐         HTTP REST          ┌────────────────────────────────────────────┐
│ │ ─────────────────────────► │ Moqui: shipping-aggregator component │
│ OFBiz OMS │ │ │
│ (order management, │ ◄───────────────────────── │ ┌──────────────────────────────────────┐ │
│ fulfillment, │ JSON response map │ │ GenericShippingServices.xml │ │
│ label printing) │ │ │ - get#ShippingRate │ │
│ │ │ │ - request#ShippingLabel │ │
└───────────────────────┘ │ │ - refund#ShippingLabel │ │
│ │ - get#Manifest │ │
│ │ - post#Order / delete#Order │ │
│ │ - get#Departments / Municipalities │ │
│ │ - post#OrderStatus (webhook in) │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ get#ShippingGatewayDetails │ │
│ │ (resolves carrier from user identity)│ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ Carrier Service │ │
│ │ (dynamic dispatch via │ │
│ │ ShippingGatewayConfig)│ │
│ └──────────┬────────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ GenericHandlerService │ │
│ │ (optional normalizer) │ │
│ └───────────────────────┘ │
└────────────────────────────────────────────┘

┌────────────┬────────────────────┼──────────────┐
▼ ▼ ▼ ▼
FORZA C807 TERMINAL_EXPRESS MOOVIN / DRIVIN / etc.
(Guatemala) (Honduras,SV) (Costa Rica) (other carriers)

Key Design Decisions

DecisionImplementation
Carrier routingBased on user's partyIdPartyRelationshipShippingGatewayConfigId setting
Service dispatchDynamic — service names stored in DB (ShippingGatewayConfig, ShippingGatewayOption)
AuthenticationCentralized get#Token with distributed cache (no re-auth per request)
Response normalizationOptional per-carrier handler via ShippingGatewayOption (service.name.handle.get.label, etc.)
Multi-tenancyEach tenant (ADOC, ADOC_HN, ADOC_SV) has separate PartyRelationship and settings

2. Moqui ↔ OFBiz Integration Boundary

OFBiz and Moqui are separate applications that communicate over HTTP REST. OFBiz acts as the API consumer; Moqui acts as the shipping service provider.

How OFBiz Calls Moqui

OFBiz constructs a REST POST request with a JSON body and an Authorization: Basic <base64> header. The request is sent to one of Moqui's shipping.rest.xml endpoints.

Authentication header: OFBiz authenticates using Moqui user credentials. Each ADOC instance has a dedicated Moqui UserAccount (e.g., adoc, adochn, adocsv) whose partyId determines the carrier lookup.

Key Moqui user accounts per instance:

Moqui UserAccountUsernameParty IDInstance
ADOCadocADOCGuatemala/main
ADOC_HNadochnADOC_HNHonduras
ADOC_SVadocsvADOC_SVEl Salvador
Critical Identity Logic

The partyId of the logged-in Moqui user is the anchor for all carrier resolution. This is set at the time of the REST call via the Authorization header.

Data OFBiz Sends to Moqui

OFBiz enriches the request with data pulled from its own entities:

Field CategoryFields SentSource in OFBiz
Origin addresstoName, address1, address2, city, stateOrProvinceCode, countryCode, phoneNumber, emailAddress, warehouseIdFacility postal address
Destination addresstoName, address1, address2, city, stateOrProvinceCode, province, canton, district, countryCode, phoneNumber, emailAddress, stateNameOrder ship-to address + order attributes
Parcelsweight, weightUnit, weightUomId, length, width, height, currency, fragileShipment packages
Order infoorderId, orderName, orderNumber, orderDate, ticketNumber, dateOfSaleOrder header
Business rulescod (boolean), validShipmentTotal, paymentStatusId, shipmentMethodTypeIdOrder payment + fulfillment
Carrier hintcarrierPartyId (optional)Carrier assignment

Data Moqui Returns to OFBiz

Moqui always returns a responseMap JSON object. Successful responses include:

{
"success": true,
"shippingLabelMap": {
"referenceNumber": "123456",
"packages": [
{ "trackingIdNumber": "TRK001" }
]
},
"artifacts": [ ... ]
}

Error responses:

{
"success": false,
"errorMessages": "No carrier found"
}

3. Core Architecture: The Plugin Pattern

Every top-level generic service follows the same 5-step pipeline:

1. get#ShippingGatewayDetails   → Resolve carrier from user identity
2. validate#RequiredParameters → Validate mandatory inputs
3. ShippingGatewayConfig lookup → Find the right carrier service name
4. Dynamic carrier service call → Invoke carrier-specific implementation
5. GenericHandlerServices call → Optional carrier-specific response normalization

Step 1 — get#ShippingGatewayDetails

This is the carrier resolution keystone service. It is allow-remote="false" (internal only).

<!-- Pseudocode of the logic -->
userPartyId = ec.user.userAccount.partyId // from HTTP Basic Auth

if (carrierPartyId is null):
lookup PartyRelationship WHERE:
fromPartyId = userPartyId
fromRoleTypeId = "Client"
toRoleTypeId = "Carrier"
relationshipTypeEnumId = "DefaultCarrier"
else:
lookup PartyRelationship WHERE:
fromPartyId = userPartyId
fromRoleTypeId = "Client"
toPartyId = carrierPartyId
toRoleTypeId = "Carrier"
relationshipTypeEnumId = "ClientCarrier"

lookup PartyRelationshipSetting WHERE:
partyRelationshipId = partyRelationship.partyRelationshipId
partySettingTypeId = "ShippingGatewayConfigId"

return: {
success: true,
userPartyId, carrierPartyId,
partyRelationshipId,
shippingGatewayConfigId: partyRelationshipSetting.settingValue
}

Entity flow diagram:

UserAccount.partyId


PartyRelationship ──────────────────────────────────────────────────────────────────────────────────
fromPartyId = userPartyId (Client role) │
toPartyId = carrierPartyId (Carrier role) │
relationshipTypeEnumId = "ClientCarrier" (or "DefaultCarrier") │
│ │
▼ yields partyRelationshipId │
PartyRelationshipSetting │
partySettingTypeId = "ShippingGatewayConfigId" │
settingValue = "FORZA" / "C807" / "TERMINAL_EXPRESS" / ... ──────────────────────────────┘


ShippingGatewayConfig.shippingGatewayConfigId = settingValue

Step 3 — Service Name Resolution via ShippingGatewayConfig

For label requests, the service name comes from ShippingGatewayConfig.requestLabelsServiceName:

<ShippingGatewayConfig shippingGatewayConfigId="FORZA"
requestLabelsServiceName="co.hotwax.shippingAggregator.forza.ForzaServices.request#ForzaShippingLabel"
getRateServiceName="co.hotwax.shippingAggregator.forza.ForzaServices.get#ForzaShippingRate"
refundLabelsServiceName="co.hotwax.shippingAggregator.forza.ForzaServices.refund#ForzaShippingLabel"/>

For extended operations (manifest, postOrder, deleteOrder, departments, municipalities), the service name comes from ShippingGatewayOption:

<ShippingGatewayOption
shippingGatewayConfigId="C807"
optionEnumId="service.name.get.manifest"
optionValue="co.hotwax.shippingAggregator.c807.C807Services.get#C807Manifest"/>

Step 4 — Dynamic Dispatch

Moqui's service engine resolves service names at runtime:

<service-call name="${shippingGatewayConfig.requestLabelsServiceName}" out-map="responseMap" in-map="context"/>

The entire context (all input parameters) is passed through to the carrier service. No filtering — carriers pick what they need.

Step 5 — Handler Normalization

After the carrier call completes, a handler service is optionally invoked using a ShippingGatewayOption:

Handler Option EnumPurpose
service.name.handle.get.labelNormalizes label response
service.name.handle.get.rateNormalizes rate response
service.name.handle.void.labelNormalizes void response
service.name.handle.post.orderNormalizes post-order response
service.name.handle.delete.orderNormalizes delete-order response
service.name.handle.get.manifestNormalizes manifest response
service.name.handle.get.departmentsNormalizes departments response
service.name.handle.get.municipalitiesNormalizes municipalities response
service.name.handle.webhook.responseHandles inbound carrier webhook

If no handler option is configured, the raw carrier response is returned as-is.


4. Entity Model & Configuration Data

Core Mantle Entities Used

EntityKey FieldsPurpose
mantle.party.PartypartyId, partyTypeEnumIdRepresents clients (ADOC) and carriers (FORZA, C807, etc.)
mantle.party.PartyRelationshipfromPartyId, toPartyId, relationshipTypeEnumId, partyRelationshipIdLinks a client party to a carrier party
mantle.party.PartyRelationshipSettingpartyRelationshipId, partySettingTypeId, settingValueStores per-relationship credential/config key-value pairs
mantle.shipment.carrier.ShippingGatewayConfigshippingGatewayConfigId, requestLabelsServiceName, getRateServiceName, refundLabelsServiceNameMaps a carrier config ID to its service implementations
mantle.shipment.carrier.ShippingGatewayOptionshippingGatewayConfigId, optionEnumId, optionValueStores carrier-specific endpoint URLs and handler service names
mantle.shipment.carrier.ShippingGatewayCarriershippingGatewayConfigId, carrierPartyIdLinks a gateway config to a carrier party (used for token cache key)
moqui.security.UserAccountuserId, username, partyIdMoqui user that represents each OFBiz tenant instance

PartySettingType Values

These are the keys used in PartyRelationshipSetting. Each is loaded by TypeData.xml:

Setting KeyDescriptionUsed By
ShippingGatewayConfigIdRequired — resolves which gateway config to useAll carriers
UsernameAPI usernameC807, TerminalExpress, Moovin
PasswordAPI passwordC807, TerminalExpress, Moovin
ClientIdAPI client IDC807, TerminalExpress, Forza, CargoTrans
ClientSecretKeyOAuth2 client secret or HMAC keyForza, CargoTrans
CodAppForza-specific "app" identifierForza
AccountNumberCarrier account numberForza
CodeOfReferenceDefault facility code (overridable by facilityIdentification)Forza
ReverseLogisticsY/N flag for return shipmentTerminalExpress
ApiKeyRaw API keyCitiExpress
ApiTokenBearer-style tokenDrivin
EndPointOverride base URL for the carrier APIC807, (others optional)
AuthTypeBASIC_AUTH or OAUTH2Used by get#Token
SendSharedSecretKeyRefresh token for OAuth2 token refreshOAuth2 flow
ClientUrlOFBiz base URL (for webhook callback)C807 (SV, HN)
ClientOrderEndpointOFBiz endpoint path for status callbacksC807 (SV, HN)
ClientAuthKeyBase64 Basic auth for callback to OFBizC807 (SV, HN)
LabelTypeLabel format typevaries
PaymentTypePayment type codevaries

ShippingGatewayOption optionEnumId Values

These control endpoint URLs and extended service names:

Option Enum IDTypical Value TypeExample
endPointBase URL of carrier APIhttps://api.forza.com/
endPoint.accessTokenToken endpoint path/oauth/token
endPoint.shipment.rateRate query path/rates
endPoint.shipments.labelsLabel creation path/labels or Paquetes/crearOrden/
endPoint.shipments.voidVoid label path/labels/{id}/void
endPoint.shipments.manifestManifest path/manifests
endPoint.municipalitiesMunicipality lookup path/municipalities
endPoint.departmentsDepartment lookup path/departments
endPoint.post.orderPost order path/orders
endPoint.delete.orderDelete order path/orders/{id}
service.name.handle.get.labelHandler service for label response...handle#C807LabelResponseHandler
service.name.handle.get.rateHandler service for rate response...handle#ForzaRateResponseHandler
service.name.handle.void.labelHandler service for void response...handle#C807VoidLabelResponseHandler
service.name.handle.webhook.responseHandler for inbound webhooks...handle#C807WebhookResponseHandler
LabelRequestTemplateTemplate enumFTL template path

5. REST API Surface

File: service/shipping.rest.xml
All endpoints use require-authentication="anonymous-all" — meaning OFBiz passes credentials via Authorization: Basic <base64(user:pass)>.

HTTP MethodPathMoqui Service
POST/rest/s1/shipping/shippingRateGenericShippingServices.get#ShippingRate
POST/rest/s1/shipping/shippingLabelGenericShippingServices.request#ShippingLabel
POST/rest/s1/shipping/manifestGenericShippingServices.get#Manifest
POST/rest/s1/shipping/refundShippingLabelGenericShippingServices.refund#ShippingLabel
POST/rest/s1/shipping/postOrderGenericShippingServices.post#Order
POST/rest/s1/shipping/deleteOrderGenericShippingServices.delete#Order
POST/rest/s1/shipping/getDepartmentsGenericShippingServices.get#Departments
POST/rest/s1/shipping/getMunicipalitiesGenericShippingServices.get#Municipalities
POST/rest/s1/shipping/orderStatusGenericShippingServices.post#OrderStatus

Party REST (party.rest.xml): Standard Mantle party management endpoints (find/create parties, roles) — not shipping-specific.


6. Authentication & Token Management

Overview

Carriers use different authentication mechanisms. The system centralizes token acquisition in HelperServices.get#Token.

get#Token Flow

1. Check distributed cache:
key = "${partyRelationshipId}_accessToken"
cache name = carrierPartyId

2. Cache HIT + isRefresh=false → return cached token immediately (no API call)

3. Cache MISS or isRefresh=true:
a. Load ShippingGatewayConfigOptions (endPoints) and PartyRelationshipSettings (credentials)
b. Read settingsMap['AuthType']:
- "BASIC_AUTH" → restClient.basicAuth(username, password)
- otherwise (OAuth2):
if SendSharedSecretKey present:
grant_type=refresh_token, refresh_token=<SendSharedSecretKey>
elif password present:
grant_type=password, username=..., password=...
else:
grant_type=client_credentials, client_id=..., client_secret=...
c. POST to: (settingsMap.EndPoint OR optionsMap.endPoint) + optionsMap['endPoint.accessToken']
d. On 2xx: cache access_token in distributed cache (putIfAbsent)
e. Return { success: true, access_token: "..." }

Carrier Auth Patterns

Auth Method: HMAC-SHA256 signature (LauValue header) + Base64 payload encoding Cache Strategy: No token needed — signature computed per request


7. Label Generation — Full Lifecycle

Entry Point

OFBiz POSTs to /rest/s1/shipping/shippingLabel with a JSON body containing shipment data.

Required Input Fields

FieldTypeRequiredSource
originAddress.toNameStringYesFacility name
originAddress.address1StringYesFacility address
originAddress.cityStringYesFacility city
originAddress.stateOrProvinceCodeStringYes (Forza)Facility state code
originAddress.countryCodeStringYesFacility country
originAddress.phoneNumberStringYes (some)Facility phone
originAddress.warehouseIdStringYes (TerminalExpress)Carrier warehouse ID
destAddress.toNameStringYesCustomer name
destAddress.address1StringYesShipping address
destAddress.cityStringYesCustomer city
destAddress.stateOrProvinceCodeStringYesState code
destAddress.provinceStringYes (TerminalExpress)Province name
destAddress.cantonStringYes (TerminalExpress)Canton
destAddress.districtStringYes (TerminalExpress)District
destAddress.phoneNumberStringYesCustomer phone
destAddress.emailAddressStringRec.Customer email
destAddress.stateNameStringYes (C807)State name for C807 lookup
parcelsListYesArray of {weight, weightUnit, length, width, height}
weightAmountDecimalYesTotal shipment weight
dateOfSaleStringYes (most)Shipment date (YYYY-MM-DD)
orderIdStringOptionalOFBiz order ID
orderNameStringYes (C807)Order reference name
orderNumberStringYes (Forza, digits only)Carrier order number
codString "true"/"false"YesCash on delivery flag
validShipmentTotalDecimalCOD onlyCOD amount
paymentStatusIdStringYes (C807)PAYMENT_NOT_RECEIVED etc.
shipmentMethodTypeIdStringYes (C807)SHIP_TO_STORE etc.
facilityIdentificationStringOptionalOverrides CodeOfReference

Complete Call Sequence

Interactive Payload Simulator

Use the simulator below to explore how the Generic payload structures adapt into Custom Carrier payloads for a Label Request.

Label Payload Simulator

Select a carrier to view their required payload structure and authentication method.

Authentication: HMAC-SHA256 signature (LauValue header) + Base64 payload encoding
Key Differences: Requires State/Province code extraction and custom Municipality/Township resolution. Returns an order number that must be digits only.
FORZA (Guatemala) Label Request Payload
{
"Method": "/Paquetes/Crear",
"Params": {
"DateOfSale": "2026-03-10",
"ContentDescription": "Apparel Order",
"from_address": {
"name": "Warehouse GT",
"phone": "0000-0000",
"email": "warehouse@adoc.com",
"address1": "Zona 1, Ciudad",
"headerCodeTownship": "10101",
"city": "Guatemala"
},
"to_address": {
"name": "Juan Perez",
"phone": "1111-2222",
"email": "juan@example.com",
"address1": "Calle Principal",
"headerCodeTownship": "10101",
"city": "Guatemala"
},
"Parcels": [
{
"Weight": 2.5,
"Length": 10,
"Width": 10,
"Height": 5
}
],
"Order_Number": "2024001",
"IdCountry": "GT",
"CountPieces": 1,
"CodeOfReference": "357506",
"TotalWeight": 2.5,
"TotalValue": 150,
"Currency": "USD"
}
}

Per-Carrier Label Request Construction

Forza (Guatemala)

Validation:

  • shippingGatewayConfigId, dateOfSale, weightAmount, countryId, parcels, destAddress, originAddress must be present
  • orderNumber must be digits only
  • originAddress.stateOrProvinceCode and destAddress.stateOrProvinceCode must be present

Municipality resolution:

  1. Extract department code from stateOrProvinceCode (split on -, take index [1])
  2. Call get#ForzaMunicipality(departmentHeaderCode, countryId, city) for both origin and destination
  3. Retrieve HeaderCodeTownship for each city

Request payload (Base64-encoded, wrapped in PayLoad):

{
"Method": "/<endPoint.shipments.labels value>",
"Params": {
"DateOfSale": "2026-02-20",
"ContentDescription": "...",
"from_address": { "name", "phone", "email", "address1", "headerCodeTownship", "city" },
"to_address": { "name", "phone", "email", "address1", "headerCodeTownship", "city" },
"Parcels": [...],
"Order_Number": "12345",
"Ticket_Number": "...",
"IdCountry": "GT",
"CountPieces": 1,
"CodeOfReference": "357506",
"TotalWeight": 2.5,
"TotalValue": 100.0,
"Collected": false,
"Currency": "USD",
"COD": { "CashOnDelivery": false, "AmmountCashOnDelivery": 0, "CashOnDeliveryCurrency": "USD" }
}
}

Security: HMAC-SHA256 signature of the full request map using ClientSecretKey, sent as LauValue header. Payload is Base64-encoded using ShippingAggregatorHelper.mapToBase64(requestMap).

Response decoding: ShippingAggregatorHelper.decodePayLoad(responseMap) decodes the Base64 response.


C807 (Honduras, El Salvador)

Authentication: Bearer token via get#Token (OAuth2 password or client_credentials flow, cached per partyRelationshipId).

Department/Municipality resolution:

  1. get#C807MatchingDepartment(stateProvinceName) → fuzzy-match against C807's department list → departmentId
  2. get#C807MatchingMunicipality(cityName, departmentId)municipalityId

COD Logic:

if (cod == "true" && paymentStatusId == "PAYMENT_NOT_RECEIVED" && shipmentMethodTypeId != "SHIP_TO_STORE") {
tipo_servicio = "CCE"
monto_cce = validShipmentTotal
} else {
tipo_servicio = "SER"
}
STS Logic Override

STS (Ship-To-Store) Orders always get "SER" (non-COD) regardless of payment status.

Request payload:

{
"recolecta_fecha": "2026-02-20 19:00",
"tipo_entrega": "<shipmentMethod>",
"provisional": false,
"sede": "<facilityIdentification (optional)>",
"guias": [{
"orden": "<orderName>-<orderDate>",
"nombre": "<destAddress.toName>",
"direccion": "<address1>, <address2>",
"telefono": "<phoneNumber>",
"correo": "<emailAddress>",
"departamento_id": 5,
"municipio_id": 23,
"tipo_servicio": "SER",
"detalle": [{ "peso": 2.5, "contenido": "Package Weight", "unidad_medida": "LB" }]
}]
}

Artifact fetch: After label creation, get#C807Artifacts(trackingIds) is called to retrieve the label PDF/image.


TerminalExpress (Costa Rica)

Authentication: HTTP Basic Auth (username, password from settings — per request, no token caching).

Required fields in destAddress: phoneNumber, province, canton, district, toName
Required in originAddress: warehouseId

Request payload:

{
"PROVINCIA": "<destAddress.province>",
"CANTON": "<destAddress.canton>",
"DISTRITO": "<destAddress.district>",
"PESO": 2.5,
"CLIENTE_ID": "1506",
"BODEGA_ID": "<originAddress.warehouseId>",
"NOM_CLIENTE_FINAL": "<destAddress.toName>",
"TEL_CLIENTE_FINAL": "<destAddress.phoneNumber>",
"DIR_CLIENTE_FINAL": "<full address string>",
"LOGISTICA_INVERSA": "N"
}

Endpoint: POST {endPoint}{endPoint.shipments.labels}https://sandboxlte.laterminalexpress.com/api/Paquetes/crearOrden/


8. Rate Shopping — Full Lifecycle

Entry Point

POST /rest/s1/shipping/shippingRate

Required Input Fields

Same address fields as label generation, plus:

  • parcels (with weight/dimensions)
  • weightAmount
  • currencyUomId

Optional:

  • shipmentMethod, packagingType, pickupType, locationId, includeAlternativeRates

Flow

Notes on Rate Shopping

  • No rate aggregation across carriers — each REST call targets a single carrier (determined by user's PartyRelationship)
  • Rate selection logic is handled by OFBiz, not Moqui — Moqui returns available rates; OFBiz selects
  • Forza rate endpoint uses same HMAC/Base64 pattern as labels
  • Rate caching is not implemented in the current codebase — each call hits the carrier API live

9. Label Voiding (Refund) — Full Lifecycle

Entry Point

POST /rest/s1/shipping/refundShippingLabel

Required Input Fields

FieldRequiredNotes
trackingNumberYes (validated)The label tracking number to void
trackingIdsOptional (List)For batch void
carrierPartyIdOptionalIf not provided, uses DefaultCarrier

Flow

OFBiz → POST /refundShippingLabel
→ get#ShippingGatewayDetails (resolve carrier)
→ validate#RequiredParameters(['trackingNumber'])
→ find ShippingGatewayConfig (refundLabelsServiceName)
→ dynamic call to refundLabelsServiceName
e.g. ForzaServices.refund#ForzaShippingLabel
e.g. C807Services.refund#C807ShippingLabel
→ GenericHandlerServices.handle#VoidShippingLabel
(looks up option: service.name.handle.void.label)
→ return responseMap to OFBiz

Per-Carrier Void Notes

Forza (refundShippingLabel.groovy):

  • Validates shippingGatewayConfigId, partyRelationshipId, trackingNumber
  • Uses same HMAC-SHA256 authentication
  • Calls endPoint.shipments.void endpoint
  • Response decoded with decodePayLoad

C807 (refundShippingLabel.groovy):

  • Validates trackingNumber and optional trackingIds
  • Gets Bearer token via get#Token
  • Calls C807's label cancellation endpoint
  • Handler normalizes response via handle#C807VoidLabelResponseHandler

Void eligibility: Determined entirely by the carrier API — there is no time-window check in the Moqui layer. If the carrier returns an error (e.g., "label already shipped"), the error is propagated in errorMessages.

Idempotency: Not explicitly implemented — a second void call with the same tracking number is forwarded to the carrier API again. The carrier API is expected to handle duplicate void requests gracefully (typically by returning a success or "already voided" message).

Financial adjustments: Not handled by Moqui. OFBiz is responsible for any refund or financial reversal once it receives a successful void response.


10. Webhook / Order Status Callback

Inbound Flow (Carrier → Moqui → OFBiz)

POST /rest/s1/shipping/orderStatus

This endpoint accepts delivery status updates pushed by carriers (e.g., C807 webhooks).

Carrier ──POST──► Moqui /orderStatus

├── Read headers: Partyid, Carrierid
├── Find PartyRelationship (validates client, carrier)
├── Load settings: ShippingGatewayConfigId, ClientUrl, ClientOrderEndpoint, ClientAuthKey
├── Look up option: service.name.handle.webhook.response
├── Call handler service (parses carrier-specific webhook payload)
│ e.g. C807Services.handle#C807WebhookResponseHandler
│ → returns normalized responseMap

└── POST normalized responseMap to OFBiz:
URL = settingsMap.ClientUrl + settingsMap.ClientOrderEndpoint
Header: Authorization: Basic <ClientAuthKey>
Body: JSON responseMap

Key data flow:

  • Partyid header → identifies the tenant (e.g., ADOC_SV)
  • Carrierid header → identifies the carrier (e.g., C807)
  • The webhook body is read raw via ec.web.getRequestBodyText()
  • The response is forwarded to OFBiz at: ClientUrl + ClientOrderEndpoint
    • For ADOC_SV: https://adoc-sv-uat.hotwax.io/api/service/orderDeliveryStatus
    • For ADOC_HN: https://adoc-hn-uat.hotwax.io/api/service/orderDeliveryStatus

11. Carrier Integration Details

Carrier Inventory

Carrier IDDisplay NameCountryAuthRateLabelVoidManifest
FORZAForzaGTHMAC+Base64
C807C807HN, SVOAuth2 Bearer
TERMINAL_EXPRESSTerminal ExpressCRBasic Auth
MOOVINMoovinGTBasic/OAuth2
DRIVINDrivinGT, SVStatic Bearer
CARGOTRANSCargoTransGTOAuth2
CITI_EXPRESSCitiExpress-API Key

ShippingGatewayConfig per Carrier

Forza:

shippingGatewayConfigId = "FORZA"
getRateServiceName = "co.hotwax.shippingAggregator.forza.ForzaServices.get#ForzaShippingRate"
requestLabelsServiceName = "co.hotwax.shippingAggregator.forza.ForzaServices.request#ForzaShippingLabel"
refundLabelsServiceName = "co.hotwax.shippingAggregator.forza.ForzaServices.refund#ForzaShippingLabel"

Options:
endPoint = <Forza API base URL>
endPoint.accessToken = <token endpoint path>
endPoint.shipments.labels = <label path>
endPoint.shipments.void = <void path>
endPoint.shipment.rate = <rate path>
service.name.handle.get.label = ForzaServices.handle#ForzaLabelResponseHandler
service.name.handle.get.rate = ForzaServices.handle#ForzaRateResponseHandler
service.name.handle.void.label = ForzaServices.handle#ForzaVoidLabelResponseHandler

C807:

shippingGatewayConfigId = "C807"
requestLabelsServiceName = "co.hotwax.shippingAggregator.c807.C807Services.request#C807ShippingLabel"
refundLabelsServiceName = "co.hotwax.shippingAggregator.c807.C807Services.refund#C807ShippingLabel"

Options:
endPoint = <C807 API base URL> (settingsMap.EndPoint per-relationship, OR optionsMap.endPoint)
endPoint.accessToken = <token path>
endPoint.shipments.labels = <label path>
endPoint.shipments.void = <void path>
endPoint.shipments.artifacts = <artifact/pdf path>
service.name.handle.get.label = C807Services.handle#C807LabelResponseHandler
service.name.handle.void.label = C807Services.handle#C807VoidLabelResponseHandler
service.name.handle.webhook.response = C807Services.handle#C807WebhookResponseHandler

TerminalExpress:

shippingGatewayConfigId = "TERMINAL_EXPRESS"
requestLabelsServiceName = "co.hotwax.shippingAggregator.terminalExpress.TerminalExpressServices.request#TerminalExpressShippingLabel"

Options:
endPoint = "https://sandboxlte.laterminalexpress.com/api/"
endPoint.shipments.labels = "Paquetes/crearOrden/"
service.name.handle.get.label = TerminalExpressServices.handle#TerminalExpressResponseHandler

Moovin:

shippingGatewayConfigId = "MOOVIN"
postOrderServiceName = "co.hotwax.shippingAggregator.moovin.MoovinServices.post#MoovinOrder"
deleteOrderServiceName = "co.hotwax.shippingAggregator.moovin.MoovinServices.delete#MoovinOrder"

Options:
endPoint = <Moovin API base URL>
endPoint.post.order = <post order path>
service.name.handle.post.order = MoovinServices.handle#MoovinHandlePostOrder
service.name.handle.delete.order = MoovinServices.handle#MoovinHandleDeleteOrder
service.name.handle.get.rate = MoovinServices.handle#MoovinHandleRateResponse

Note: Moovin uses the postOrder/deleteOrder paradigm instead of requestShippingLabel.

Drivin:

shippingGatewayConfigId = "DRIVIN"
postOrderServiceName = "co.hotwax.shippingAggregator.drivIn.DrivInServices.post#DrivInpostOrder"
deleteOrderServiceName = "co.hotwax.shippingAggregator.drivIn.DrivInServices.delete#DrivInDeleteOrder"

Options:
endPoint = <Drivin API base URL>
endPoint.post.order = <post order path>
service.name.handle.post.order = DrivInServices.handle#DrivInHandlePostOrder
service.name.handle.delete.order = DrivInServices.handle#DrivInHandleDeleteOrder

Note: Uses an ApiToken via the X-API-Key header and dynamically queries for a schema_code on post order.

CargoTrans:

shippingGatewayConfigId = "CARGOTRANS"
getRateServiceName = "co.hotwax.shippingAggregator.cargoTrans.CargoTransServices.get#CargoTransShippingRate"
requestLabelsServiceName = "co.hotwax.shippingAggregator.cargoTrans.CargoTransServices.request#CargoTransShippingLabel"

Options:
endPoint = <CargoTrans API base URL>
endPoint.shipments.labels = <label creation path>
businessCategory.default = <category ID>
service.name.handle.get.label = CargoTransServices.handle#CargoTransLabelResponseHandler
service.name.handle.get.rate = CargoTransServices.handle#CargoTransRateResponseHandler

Note: Requires a ClientId parameter in the payload body and a ClientSecretKey for the X-API-Key auth header. Also queries for a municipalityId.

CitiExpress:

shippingGatewayConfigId = "CITI_EXPRESS"
postOrderServiceName = "co.hotwax.shippingAggregator.citiExpress.CitiExpressServices.post#CitiExpressOrder"
deleteOrderServiceName = "co.hotwax.shippingAggregator.citiExpress.CitiExpressServices.delete#CitiExpressOrder"

Options:
endPoint = <CitiExpress API base URL>
endPoint.post.order = <post order path>
service.name.handle.post.order = CitiExpressServices.handle#CitiExpressHandlePostOrder
service.name.handle.delete.order = CitiExpressServices.handle#CitiExpressHandleDeleteOrder

Note: Passes the ApiKey setting directly inside the JSON payload logic. Requires precise coordinate attributes (job_pickup_latitude / longitude).

FedEx:

shippingGatewayConfigId = "FEDEX"
getRateServiceName = "co.hotwax.shippingAggregator.Fedex.FedexServices.get#FedexShippingRate"
requestLabelsServiceName = "co.hotwax.shippingAggregator.Fedex.FedexServices.request#FedexShippingLabel"
refundLabelsServiceName = "co.hotwax.shippingAggregator.Fedex.FedexServices.refund#FedexShippingLabel"

Options:
endPoint = <FedEx API base URL>
endPoint.accessToken = <token path>
endPoint.shipments.labels = <label path>
endPoint.shipment.rate = <rate path>
service.name.handle.get.label = FedexServices.handle#FedexLabelResponseHandler
service.name.handle.get.rate = FedexServices.handle#FedexRateResponseHandler
service.name.handle.void.label = FedexServices.handle#FedexVoidLabelResponseHandler

Note: Implements standard OAuth2 (client_credentials) and caches the accessToken. Distinct from others, deeply standardized shipping fields (e.g. LabelStockType, dimensional units).

ADOC Client → Carrier Relationships

Relationship IDClientCarrierAuth Setting
ADOC_FORZAADOCFORZAClientId=53489, ClientSecretKey=..., CodApp=..., AccountNumber=...
ADOC_MOOVINADOCMOOVINUsername=ADOC, Password=...
ADOC_CARGOADOCCARGOTRANSClientId=2587, ClientSecretKey=...
ADOC_TERMINAL_EXPRESSADOCTERMINAL_EXPRESSClientId=1506, Username, Password, ReverseLogistics=N
ADOC_CITI_EXPRESSADOCCITI_EXPRESSApiKey=...
ADOC_DRIVINADOCDRIVINApiToken=...
ADOC_C807ADOCC807Username, Password, EndPoint=https://app.c807.com/, AuthType=BASIC_AUTH
ADOC_SV_C807ADOC_SVC807same + ClientUrl, ClientOrderEndpoint, ClientAuthKey
ADOC_SV_DRIVINADOC_SVDRIVINApiToken=...
ADOC_HN_C807ADOC_HNC807EndPoint=https://devapp.c807.com/, ClientUrl=https://adoc-hn-uat.hotwax.io

12. Error Handling & Logging

Validation Layer

HelperServices.validate#RequiredParameters (backed by validateRequiredParameters.groovy):

  • Accepts a list of required field names and a parameters map
  • Returns {success: true} or {success: false, errorMessage: "Missing: field1, field2"}
  • Called at the generic level (address required) and at the carrier level (carrier-specific fields)

Error Propagation Pattern

Carrier service returns: { success: false, errorMessages: [...] }
→ parent service returns: { success: false, errorMessages: [...] }
→ GenericShippingServices returns: responseMap with success=false
→ OFBiz receives JSON: { "success": false, "errorMessages": "..." }

Forza-specific: Wraps restClient.call() in try/catch:

catch (Exception e) {
ec.logger.error("DIAGNOSTIC: Error calling Forza Label API: ${e.getMessage()}", e)
errorMessages.add("Unable to make request to Forza. Error: ${e.getMessage()}")
return [success: false, errorMessages: errorMessages]
}

Logging Conventions

Log LevelWhen Used
warnDiagnostic logs (currently active in most carrier files)
warnHTTP non-2xx response codes
infoService call tracking ("Calling service ...", "Response returned for ...")
errorExceptions caught in try/catch

Diagnostic logging (added during debugging, currently active):

  • DIAGNOSTIC: Terminal Express start for configId: ...
  • DIAGNOSTIC: C807 Request Body: ...
  • DIAGNOSTIC: Forza Label Response Code: ...

⚠️ These WARN-level diagnostic logs are verbose. They should be removed or changed to DEBUG before production.

HTTP Status Handling

All carrier services follow this pattern:

if (restResponse.statusCode < 200 || restResponse.statusCode >= 300) {
ec.logger.warn("Unsuccessful with status code: ${restResponse.statusCode} and response: ${restResponse.text()}")
}
// response is still parsed — carrier may include error details in 4xx body
Map responseMap = restResponse.jsonObject()
return responseMap

The carrier service does not throw exceptions for HTTP errors — it returns the parsed JSON body (which may contain carrier-specific error fields). The handler and OFBiz are responsible for interpreting these.


13. Configuration Reference

How to Add a New Carrier

  1. Create Party: Add mantle.party.Party with partyTypeEnumId="PtyOrganization" and roleTypeId="Carrier"

  2. Create ShippingGatewayConfig:

    <ShippingGatewayConfig shippingGatewayConfigId="NEW_CARRIER"
    shippingGatewayTypeEnumId="ShGtwyTrdPrty"
    requestLabelsServiceName="co.hotwax.shippingAggregator.newcarrier.NewCarrierServices.request#NewCarrierLabel"/>
  3. Add ShippingGatewayOptions: Endpoints + handler service names

  4. Create PartyRelationship: Link client party (e.g., ADOC) to the new carrier

  5. Add PartyRelationshipSettings: Credentials + ShippingGatewayConfigId

  6. Implement Groovy service: Create service/co/hotwax/shippingAggregator/newcarrier/ with label/rate/refund groovy scripts

  7. Register in XML: Create NewCarrierServices.xml wrapping the groovy scripts

Endpoint URL Resolution (Priority Order)

For most carriers, the base URL is determined as follows:

def baseUrl = settingsMap.containsKey("EndPoint") ? settingsMap.EndPoint : optionsMap.endPoint

settingsMap.EndPoint (from PartyRelationshipSetting) overrides optionsMap.endPoint (from ShippingGatewayOption). This allows per-relationship URL customization without changing the gateway config (e.g., production vs. dev endpoints for different ADOC instances).

Key Service Dependencies

Utility ClassLocationPurpose
co.hotwax.helper.ShippingAggregatorHelpersrc/ directorygetShippingGatewayConfigOptions(), getPartyRelationshipSettings(), getFieldsMapFromList(), mapToBase64(), decodePayLoad()
co.hotwax.helper.LauValueGeneratorsrc/ directoryHMAC-SHA256 signature generation for Forza
org.moqui.util.RestClientMoqui frameworkHTTP client for all carrier API calls