diff --git a/project-ideas/unigate/README.md b/project-ideas/unigate/README.md index d49e8103..aab7af1f 100644 --- a/project-ideas/unigate/README.md +++ b/project-ideas/unigate/README.md @@ -5,7 +5,9 @@ Unigate is a **multi-tenant API gateway** built on the Moqui Enterprise Framewor - **[UniMail](./unimail/readme.md)** — Email delivery and lifecycle event tracking (Klaviyo, Mayur, and future providers) - **[UniShip](./uniship/readme.md)** — Shipping rates, label generation, and label refunds (FedEx, Purolator, Canada Post, ShipHawk, C807, DrivIn) -Callers never deal with provider-specific APIs, authentication schemes, or payload formats. They authenticate once as a tenant, pass a gateway auth ID, and get a normalized response back regardless of which carrier or email provider sits behind it. +Callers never deal with provider-specific APIs, authentication schemes, or payload formats. They authenticate once as a tenant, pass a gateway auth ID, and get a normalized response back regardless of which installed carrier or email adapter sits behind it. + +External communication partners can keep their implementation outside Unigate by hosting an HTTPS receiver. HotWax enables and configures the outbound adapter; the partner does not deploy code to Unigate. See the [external order email integration guide](./unimail/external-order-email-integration.md). --- @@ -49,7 +51,7 @@ graph LR end %% Flow - Client -- "POST /email/*\nPOST /shipment/*" --> Filter + Client -- "POST /communication/*\nPOST /shipment/*" --> Filter Filter -- "tenant_Id" --> CommRouter Filter -- "tenant_Id" --> ShipRouter @@ -70,7 +72,7 @@ graph LR ## Package Layout -``` +```text co.hotwax.unigate ← Core authentication and routing ├── TenantAuthFilter.groovy ← Servlet filter (api_key / tenant_Id auth) ├── helper/ @@ -100,6 +102,7 @@ co.hotwax.unigate ← Core authentication and routing Every request is scoped to a **tenant** — a `Party` record of type `PtyOrganization`. Tenants authenticate with a hashed API key stored in `UserLoginKey`, and their carrier credentials live in separate `CommGatewayAuth` / `ShippingGatewayAuth` records linked by `tenantPartyId`. This means: + - A single tenant can have credentials for multiple carriers simultaneously - A single carrier config (`ShippingGatewayConfig`) can serve many tenants with different credentials - Adding a carrier for a new tenant is a data operation — no code change required @@ -110,7 +113,7 @@ See [Tenant Onboarding](./tenant-onboarding.md) for how to provision a new tenan ## Key Design Decisions -**Database-driven routing** — which service handles a request is read from `CommGatewayConfig.sendEmailServiceName` or `ShippingGatewayConfig.getRateServiceName`. There are no `if/else` chains on carrier names in the routing layer; adding a carrier only requires a new service and a database record. +**Database-driven routing** — which service handles a request is read from `CommGatewayConfig.sendEmailServiceName` or `ShippingGatewayConfig.getRateServiceName`. There are no `if/else` chains on provider names in the routing layer. Switching between installed adapters is a data change; introducing a new in-process adapter still requires a deployed service and a database record. **API keys are hashed at rest** — Unigate stores the SHA-based hash of the API key, never the plaintext. The plaintext is returned only once at generation time. @@ -131,7 +134,8 @@ See [Tenant Onboarding](./tenant-onboarding.md) for how to provision a new tenan | [ShippingGatewayAuth](./entity/ShippingGatewayAuth.md) | Tenant shipping credentials | | [ShippingGatewayConfig](./entity/ShippingGatewayConfig.md) | Gateway service routing config | | [UniMail](./unimail/readme.md) | Email gateway — APIs, routing, entities | -| [Add Email Gateway](./unimail/add-email-gateway.md) | How to integrate a new email provider | +| [External Order Email Integration](./unimail/external-order-email-integration.md) | Partner-owned webhook contract and certification | +| [Add In-Process Email Gateway](./unimail/add-email-gateway.md) | Internal provider adapter implementation | | [send#EmailCommunication](./unimail/services/send-email-communication.md) | Email sending service design | | [create#EmailFlow](./unimail/services/create-email-flow.md) | Automated flow provisioning design | | [get#EmailFlow](./unimail/services/get-email-flow.md) | Flow status retrieval design | @@ -142,5 +146,3 @@ See [Tenant Onboarding](./tenant-onboarding.md) for how to provision a new tenan | [get#ShippingRate](./uniship/services/get-shipping-rate.md) | Rate service design | | [request#ShippingLabels](./uniship/services/requestShippingLabel.md) | Label request service design | | [refund#ShippingLabels](./uniship/services/refundShippingLabels.md) | Label refund service design | - - diff --git a/project-ideas/unigate/entity/CommGatewayAuth.md b/project-ideas/unigate/entity/CommGatewayAuth.md index ff649bda..43c14207 100644 --- a/project-ideas/unigate/entity/CommGatewayAuth.md +++ b/project-ideas/unigate/entity/CommGatewayAuth.md @@ -1,156 +1,91 @@ # `CommGatewayAuth` -## 1. Overview +`CommGatewayAuth` stores a tenant's endpoint and credentials for an installed UniMail adapter. -The `CommGatewayAuth` record stores tenant-specific endpoint and credential data for communication gateways used by Unigate, such as Klaviyo and other future email or event providers. - ---- - -## 2. Purpose - -* Allows each tenant to configure one or more communication gateway accounts they intend to use. -* Enables the Unigate engine to route email or event API requests using tenant-specific credentials. -* Stores endpoint, authentication type, and credential values needed to call external communication providers. - ---- - -## 3. Entity: `CommGatewayAuth` +## Current Entity ```xml - + - - + - - + + - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - ``` -### Notes - -* Credential fields use `encrypt="true"` so Moqui can store them encrypted at rest. -* `authHeaderName` supports providers like Klaviyo that expect token values in a named HTTP header. -* `extraConfigJson` gives flexibility for provider-specific settings without changing the entity schema. - ---- - -## 4. Setup Workflow - -### Step-by-Step Instructions - -1. **Tenant Manager Logs In** - A privileged user, such as a tenant admin or Unigate support user, logs into the Unigate tenant manager interface. - -2. **Navigate to Communication Gateway Setup Page** - Admin chooses to configure communication gateway access for the tenant. - -3. **Choose Gateway Type** - Select from predefined options such as Klaviyo or another supported communication provider. These options are sourced from the `CommGatewayConfig` master entity. +## Fields -4. **Enter Credentials** - Provide base URL, auth type, and the relevant credentials for the provider, such as API key, bearer token, basic auth values, or OAuth client credentials. +| Field | Required | Purpose | +| --- | --- | --- | +| `commGatewayAuthId` | Yes | Stable identifier passed in each email API request. | +| `commGatewayConfigId` | Yes | Selects an installed adapter and its service-name routing. | +| `tenantPartyId` | Yes | Tenant that owns the endpoint and credentials. | +| `modeEnumId` | No | Environment or operating mode, such as sandbox or production. | +| `authTypeEnumId` | No | Describes the configured authentication method. Adapter code must still apply it. | +| `baseUrl` | No | Provider or partner endpoint base URL. | +| `authHeaderName` | No | Header used when sending `publicKey`. | +| `username` | No | Username for adapters that use username/password authentication. | +| `password` | No | Encrypted password field. | +| `publicKey` | No | API key, bearer value, or other public credential consumed by the adapter. | +| `description` | No | Operator-facing label. | -5. **Save Configuration** - A record is created in the `CommGatewayAuth` entity scoped to the `tenantPartyId`. +## Relationship to `CommGatewayConfig` ---- +`CommGatewayAuth` does not make an arbitrary URL callable on its own. Its `commGatewayConfigId` points to a `CommGatewayConfig` record whose fields contain deployed Moqui service names: -## 5. Example Use Cases +- `sendEmailServiceName` +- `createEventServiceName` +- `createFlowServiceName` +- `getFlowServiceName` -A retailer configures one communication provider for marketing events and another for transactional communication workflows. +Switching a tenant between adapters that are already installed is a data change. Adding a new in-process provider implementation still requires its service and template to be deployed. External partners should use the [partner-owned endpoint model](../unimail/external-order-email-integration.md) after HotWax enables a provider-neutral outbound adapter. -* **Klaviyo:** +## Example - * `modeEnumId`: `Production` - * `authTypeEnumId`: `ApiKeyHeader` - * `baseUrl`: `https://a.klaviyo.com/api` - * `authHeaderName`: `Authorization` - * `apiKey`: `Klaviyo-API-Key pk_live_xxxxx` - - These values support the current Klaviyo implementation, which needs `baseUrl`, `authHeaderName`, and `apiKey` to submit event payloads. - -* **Mayur:** - - * `modeEnumId`: `Production` - * `authTypeEnumId`: `NoAuth` - * `baseUrl`: `https://example.mayur.internal/api` - - These values support the current Mayur implementation, which only needs the base URL to construct and send requests. - ---- - -## 6. Security Considerations - -* Credential fields such as `apiKey`, `password`, `clientSecret`, `accessToken`, and `refreshToken` should be encrypted at rest using Moqui field encryption. -* Access to view or edit communication gateway credentials should be restricted through artifact authorization. -* Raw secrets should never be exposed through logs, responses, or admin screens without masking. - ---- - -## 7. Internal Entity Relationship - -* `tenantPartyId` -> references `Party` and identifies the tenant owning the gateway credentials -* `commGatewayConfigId` -> references the predefined communication gateway definition -* Enum IDs (`modeEnumId`, `authTypeEnumId`) -> map to values defined in `moqui.basic.Enumeration` - ---- - -## 8. Admin Tools & Future Enhancements - -* Admin UI for editing existing communication gateway configurations -* Support for token refresh workflows using `refreshToken` -* Support for storing provider-specific metadata in `extraConfigJson` -* Validation or ping test to verify a tenant's gateway configuration before activation +```http +POST /rest/s1/unigate/commGatewayAuth +Content-Type: application/json +api_key: +tenant_Id: +``` ---- +```json +{ + "commGatewayAuthId": "PARTNER_TEST", + "tenantPartyId": "ACME_001", + "commGatewayConfigId": "EXTERNAL_WEBHOOK", + "authTypeEnumId": "ApiKeyHeader", + "modeEnumId": "Sandbox", + "baseUrl": "https://partner.example.com/hotwax/events", + "authHeaderName": "Authorization", + "publicKey": "Bearer test-token", + "description": "Partner test endpoint" +} +``` -## 9. Related Entities +`EXTERNAL_WEBHOOK` is illustrative. Use the configuration ID that HotWax confirms is installed in the target environment. -| Entity Name | Purpose | -| --- | --- | -| `Party` | Identifies the tenant | -| `CommGatewayConfig` | Identifies supported communication gateway integrations | -| `Enumeration` | Stores enum values like auth type and mode | +## Security ---- +- Treat `password`, `publicKey`, and any bearer value as secrets even when the entity field is not marked `encrypt="true"`. +- The current source encrypts `password`; it does not mark `publicKey` as encrypted. +- Do not include credentials in screenshots, logs, documentation examples, or support messages. +- Restrict entity reads and writes to authorized tenant or platform administrators. +- Use separate test and production records and rotate credentials independently. +- Audit records contain endpoint and credential-field updates; never place secret values in descriptions. -## 10. Developer Tips +## Related Documents -* Cache sensitive fields minimally; `cache="false"` is appropriate for this entity. -* Keep provider-specific request logic in services, and use `CommGatewayAuth` only for credential and endpoint data. +- [UniMail overview](../unimail/readme.md) +- [External partner integration](../unimail/external-order-email-integration.md) +- [Tenant onboarding](../tenant-onboarding.md) +- [Entity model](./entity-model.md) diff --git a/project-ideas/unigate/tenant-onboarding.md b/project-ideas/unigate/tenant-onboarding.md index d73e060e..4bebbb83 100644 --- a/project-ideas/unigate/tenant-onboarding.md +++ b/project-ideas/unigate/tenant-onboarding.md @@ -22,7 +22,7 @@ Steps 1 and 2 are performed by an administrator. Step 3 can be done by either th Call `co.hotwax.unigate.UnigateTenantServices.create#UnigateTenant`. -**Input** +### Tenant Input | Parameter | Type | Required | Description | |---|---|---|---| @@ -31,7 +31,7 @@ Call `co.hotwax.unigate.UnigateTenantServices.create#UnigateTenant`. **What it does internally:** -``` +```text create#UnigateTenant → create#co.hotwax.unigate.Party (partyTypeEnumId = 'PtyOrganization', organizationName = ) @@ -43,7 +43,7 @@ create#UnigateTenant The `UNIGATE_API` user group controls which API endpoints the tenant can reach. All tenants are added to it automatically. -**Output** +### Tenant Output ```json { "partyId": "ACME_001" } @@ -57,7 +57,7 @@ The returned `partyId` is the tenant's permanent identifier — it will appear a Call `co.hotwax.unigate.UnigateTenantServices.create#UserLoginKey`. -**Input** +### API Key Input | Parameter | Type | Required | Description | |---|---|---|---| @@ -65,7 +65,7 @@ Call `co.hotwax.unigate.UnigateTenantServices.create#UserLoginKey`. **What it does internally:** -``` +```text create#UserLoginKey → loginKey = StringUtilities.getRandomString(40) → hashedKey = ec.ecfi.getSimpleHash(loginKey, "", LoginKeyHashType, false) @@ -74,7 +74,7 @@ create#UserLoginKey → return plaintext loginKey to caller ``` -**Output** +### API Key Output ```json { "loginKey": "a7f2c9...40-char-random-string..." } @@ -94,7 +94,7 @@ Before the tenant can call UniMail or UniShip, an administrator must create at l Create a `CommGatewayAuth` record: -``` +```text POST /rest/s1/unigate/commGatewayAuth (or via admin UI) { @@ -112,7 +112,7 @@ The returned `commGatewayAuthId` is what the tenant passes in their email API ca Create a `ShippingGatewayAuth` record: -``` +```text POST /rest/s1/unigate/shippingGatewayAuth (or via admin UI) { @@ -129,7 +129,7 @@ POST /rest/s1/unigate/shippingGatewayAuth (or via admin UI) The returned `shippingGatewayAuthId` is what the tenant passes in their shipping API calls. -For scenarios where a single tenant needs **separate credentials per facility**, see [Carrier Account Management](../uniship/CarrierAccountManagement.md). +For scenarios where a single tenant needs **separate credentials per facility**, see [Carrier Account Management](./uniship/carrier-account-management.md). --- @@ -149,7 +149,7 @@ Both subscreens rely on a single `SystemMessageRemote` record in Maarg with `sys |---|---| | `internalId` | The Unigate `tenantPartyId` (the `partyId` created in Step 1 above) | | `publicKey` | The plaintext API key returned from Step 2 (stored here for use by all OMS services) | -| `sendUrl` | The Unigate instance base URL (e.g. `https://unigate.hotwax.co`) | +| `sendUrl` | The Unigate API root (for example, `https://unigate.example.com/rest/s1/unigate`). OMS appends paths such as `communication/email`. | The screen reads `UNIGATE_CONFIG` on load. If it is missing or incomplete, both subscreens are locked and show an error: *"UNIGATE_CONFIG is incomplete. Please finish configuring Tenant ID, API Key, and Instance URL."* @@ -168,6 +168,7 @@ Once `UNIGATE_CONFIG` is set, this screen manages the full email integration set This section lists all `CommGatewayAuth` records for the active tenant, fetched directly from Unigate via `UnigateServices.get#CommGatewayAuths`. Use **Add Comm Auth** to create a new credential record. The dialog collects: + - `commGatewayConfigId` — the provider to use (populated from `CommGatewayConfig` records in Unigate) - `commGatewayAuthId` — a unique identifier you assign (e.g. `KLAVIYO_PROD`) - `baseUrl`, `publicKey`, `username`, `password`, `authHeaderName` — provider-specific auth fields @@ -176,11 +177,14 @@ On submit, this calls `UnigateServices.create#CommGatewayAuth`, which proxies th Existing records can be edited or deleted inline. +For an external partner endpoint, HotWax must first confirm that a provider-neutral outbound adapter is installed. The partner supplies its HTTPS endpoint and authentication requirements; it does not add a `CommGatewayConfig` service name or deploy code to Unigate. See [External Partner Integration for Order Email Events](./unimail/external-order-email-integration.md). + #### 2. Product Store Email Settings This section manages `ProductStoreEmailSetting` records — the OMS entity that ties an email type (e.g. `ORDER_COMPLETION`, `READY_FOR_PICKUP`) to a specific Unigate auth and email template. Use **Add Email Setting** to create a new mapping. The dialog collects: + - `productStoreId` — which product store this setting applies to - `emailType` — the email event type (from the `PRDS_EMAIL` enumeration) - `fromAddress`, `subject` — sender and subject line @@ -203,6 +207,7 @@ This screen manages the shipping side of the integration. It has three sections: Lists all `ShippingGatewayAuth` records for the active tenant, fetched from Unigate via `UnigateServices.get#ShippingGatewayAuths`. Use **Add Ship Auth** to create a new credential record. The dialog collects: + - `shippingGatewayConfigId` — the carrier config to use (e.g. `FEDEX_CONFIG`; populated from Unigate's `ShippingGatewayConfig` records) - `shippingGatewayAuthId` — a unique identifier you assign (e.g. `SMUS_FEDEX_01`) - `baseUrl` — carrier API base URL (sandbox or production) @@ -222,9 +227,9 @@ Lists `ShippingCarrierBillingConfig` records — billing overrides per carrier a ## Related Documents -- [TenantAuthFilter](../TenantAuthFilter.md) — how the API key is validated per request -- [Entity Model](../entity/entity-model.md) — full entity definitions -- [CommGatewayAuth](../entity/CommGatewayAuth.md) — email credential entity -- [ShippingGatewayAuth](../entity/ShippingGatewayAuth.md) — shipping credential entity -- [Carrier Account Management](../uniship/CarrierAccountManagement.md) — multi-facility credential pattern - +- [TenantAuthFilter](./tenant-auth-filter.md) — how the API key is validated per request +- [Entity Model](./entity/entity-model.md) — full entity definitions +- [CommGatewayAuth](./entity/CommGatewayAuth.md) — email credential entity +- [ShippingGatewayAuth](./entity/ShippingGatewayAuth.md) — shipping credential entity +- [External Partner Integration](./unimail/external-order-email-integration.md) — partner-owned order email endpoint +- [Carrier Account Management](./uniship/carrier-account-management.md) — multi-facility credential pattern diff --git a/project-ideas/unigate/unimail/add-email-gateway.md b/project-ideas/unigate/unimail/add-email-gateway.md index 2c5cfe7c..88bbedc1 100644 --- a/project-ideas/unigate/unimail/add-email-gateway.md +++ b/project-ideas/unigate/unimail/add-email-gateway.md @@ -1,19 +1,21 @@ -# Adding a New Email Gateway to UniMail +# Adding an In-Process Email Gateway Adapter to UniMail -This guide walks through integrating a new email provider into UniMail. The integration pattern is consistent across all providers — you implement two things (a service and a template), then register one database record. +This internal guide covers a provider adapter that is compiled and deployed with Unigate. It is intended for HotWax platform developers, not external partners. + +External partners should host their own HTTPS receiver and use the [external order email integration guide](./external-order-email-integration.md). They must not need access to this repository or deploy code to Unigate. --- ## The Pattern -UniMail's routing layer (`CommunicationServices`) delegates to provider implementations through a database-configured service name. To add a new provider, you need to: +UniMail's routing layer (`CommunicationServices`) delegates to installed provider implementations through a database-configured service name. To add an in-process provider, you need to: 1. Implement the service interface 2. Create the FreeMarker request template(s) 3. Register the provider in `CommGatewayConfig` 4. Create a `CommGatewayAuth` record for each tenant using it -No changes to the routing layer (`CommunicationServices.xml`) are needed. +No changes to the routing layer (`CommunicationServices.xml`) are needed, but the provider service and template still require a Unigate build and deployment. Database routing is configuration-driven; adapter availability is not. --- @@ -48,7 +50,7 @@ Create `service/co/hotwax/communication/{provider}/{ProviderName}Services.xml`. - Always load `CommGatewayAuth` via `commGatewayAuthId` (passed in context by the router) - The `response` and `requestBody` out-parameters are defined in the interface — populate both - Use `commGatewayAuth.baseUrl` for the endpoint base — never hardcode URLs -- Use `commGatewayAuth.authHeaderName` + `commGatewayAuth.publicKey` / `apiKey` for auth — these are what the tenant configures +- Use `commGatewayAuth.authHeaderName` plus `commGatewayAuth.publicKey`, or `username` and encrypted `password`, for authentication. These are the credential fields in the current entity. --- @@ -122,15 +124,19 @@ Only populate the service name fields for operations you actually implement. Lea Each tenant using this provider needs a `CommGatewayAuth` record: -```json +```http POST /rest/s1/unigate/commGatewayAuth +Content-Type: application/json +``` + +```json { "tenantPartyId": "TENANT_001", "commGatewayConfigId": "MY_PROVIDER", "authTypeEnumId": "ApiKeyHeader", "baseUrl": "https://api.myprovider.com", "authHeaderName": "X-Api-Key", - "apiKey": "tenant-api-key-here", + "publicKey": "tenant-api-key-here", "modeEnumId": "Production", "description": "Tenant 001 - My Provider production" } @@ -146,7 +152,7 @@ Before considering an integration complete: - [ ] Service implements `co.hotwax.unigate.ApiInterfaceServices.send#EmailCommunication` - [ ] Service reads `commGatewayAuth.baseUrl` and does not hardcode any URLs -- [ ] Auth header is driven by `commGatewayAuth.authHeaderName` / `apiKey` / `publicKey` +- [ ] Auth header is driven by `commGatewayAuth.authHeaderName` and `publicKey`, or by the configured username/password fields - [ ] FreeMarker template renders valid JSON for the provider - [ ] `CommGatewayConfig` record created with correct service name - [ ] At least one `CommGatewayAuth` provisioned for a test tenant @@ -156,6 +162,7 @@ Before considering an integration complete: ## Related Documents -- [UniMail README](./README.md) — how routing and the existing Mayur integration work +- [External partner integration](./external-order-email-integration.md) — partner-owned webhook model with no Unigate deployment +- [UniMail overview](./readme.md) — how routing and the installed implementations work - [CommGatewayAuth entity](../entity/CommGatewayAuth.md) — full field reference for auth config -- [ApiInterfaceServices](../../service/co/hotwax/unigate/ApiInterfaceServices.xml) — the interface contract your service must implement +- [`send#EmailCommunication` API](./services/send-email-communication.md) — the interface contract your service must implement diff --git a/project-ideas/unigate/unimail/external-order-email-integration.md b/project-ideas/unigate/unimail/external-order-email-integration.md new file mode 100644 index 00000000..31e5e0ba --- /dev/null +++ b/project-ideas/unigate/unimail/external-order-email-integration.md @@ -0,0 +1,237 @@ +# External Partner Integration for Order Email Events + +This guide is for partners that want to consume HotWax order lifecycle events and trigger transactional email or marketing automation in their own platform. Partners host the integration endpoint; they do not add services, templates, or deployment artifacts to Unigate. + +## Integration Model + +```mermaid +sequenceDiagram + participant OMS as HotWax OMS + participant Unigate as Unigate + participant Adapter as HotWax outbound adapter + participant Partner as Partner endpoint + participant Provider as Partner platform + + OMS->>Unigate: POST /communication/email + Unigate->>Adapter: Canonical order email event + Adapter->>Partner: HTTPS POST + Partner->>Provider: Create event / trigger automation + Provider-->>Partner: Accepted + Partner-->>Adapter: 2xx + Adapter-->>Unigate: Normalized gateway response + Unigate-->>OMS: Delivery result +``` + +The partner owns the receiver and provider-specific mapping. HotWax owns tenant setup, event routing, and the outbound adapter inside Unigate. + +> **Platform prerequisite:** `CommGatewayConfig` routes requests to service names that are already deployed in Unigate. Database configuration can switch between installed adapters, but it cannot call an arbitrary partner endpoint by itself. HotWax must confirm that a provider-neutral outbound adapter is enabled for the target environment before certification. The partner must not be asked to fork or deploy Unigate. + +## Responsibilities + +| Owner | Responsibility | +| --- | --- | +| Partner | Host an HTTPS endpoint, authenticate HotWax requests, validate the event, map it to the partner API, and return a meaningful HTTP status. | +| HotWax | Provision a test tenant, enable the outbound adapter, configure endpoint credentials, map product-store email types, and provide test events. | +| Retailer | Approve the event catalog, sender identity, templates or automations, and production credentials. | + +## Endpoint Contract + +The partner endpoint must: + +- accept `POST` requests with `Content-Type: application/json` over HTTPS; +- support the authentication scheme agreed during onboarding; +- return a `2xx` status only after the event has been accepted for processing; +- return `400` or `422` for a permanently invalid payload; +- return `401` or `403` for invalid credentials; +- return `429` with `Retry-After` when rate limited; +- return `5xx` for a temporary partner-side failure; +- tolerate retries and duplicate deliveries; and +- ignore unknown JSON fields so additive contract changes remain compatible. + +The response body is optional. When present, this shape is recommended: + +```json +{ + "accepted": true, + "externalEventId": "evt_01JEXAMPLE" +} +``` + +## Canonical Event + +The outbound adapter should preserve the canonical `send#EmailCommunication` envelope. A representative order event is shown below. + +```json +{ + "commGatewayAuthId": "PARTNER_TEST", + "emailType": "READY_FOR_PICKUP", + "subject": "Your order is ready for pickup", + "emailAddress": "alex@example.com", + "messageData": { + "orderId": "10001", + "orderName": "WEB-10001", + "orderDate": "2026-08-11T08:10:00Z", + "completedDatetime": "", + "orderStatusUrl": "https://example.com/orders/WEB-10001", + "orderUpdateUrl": "", + "grandTotal": 108.25, + "firstName": "Alex", + "lastName": "Morgan", + "brandName": "Example Store", + "sandbox": true, + "isStorePickup": true, + "facilityAddress": { + "toName": "Example Store Downtown", + "facilityName": "Downtown", + "address1": "100 Main Street", + "address2": "", + "city": "New York", + "stateProvinceGeoId": "NY", + "postalCode": "10001", + "countryGeoId": "USA", + "latitude": "40.7505", + "longitude": "-73.9934", + "phoneNumber": "+12125550100" + }, + "shippingAddress": {}, + "billingAddress": {}, + "items": [ + { + "productId": "SKU-RED-M", + "shipGroupSeqId": "00001", + "color": "Red", + "size": "M", + "quantity": 1, + "unitPrice": 100.0, + "itemDescription": "Red shirt", + "productName": "Classic Shirt", + "productImageUrl": "https://example.com/images/red-shirt-m.jpg", + "parentProductImageUrl": "https://example.com/images/red-shirt.jpg", + "itemStatus": "ITEM_APPROVED", + "itemUrl": "https://example.com/products/classic-shirt", + "shipmentMethodTypeId": "STOREPICKUP", + "trackingUrl": "", + "trackingCode": "", + "shipFromAddress": {}, + "adjustments": [ + { + "orderAdjustmentTypeId": "EXT_PROMO_ADJUSTMENT", + "amount": -10.0, + "comments": "Welcome discount" + } + ] + } + ], + "allItems": [ + { + "productId": "SKU-RED-M", + "shipGroupSeqId": "00001", + "color": "Red", + "size": "M", + "quantity": 1, + "unitPrice": 100.0, + "itemDescription": "Red shirt", + "productName": "Classic Shirt", + "productImageUrl": "https://example.com/images/red-shirt-m.jpg", + "parentProductImageUrl": "https://example.com/images/red-shirt.jpg", + "itemStatus": "ITEM_APPROVED", + "itemUrl": "https://example.com/products/classic-shirt", + "shipmentMethodTypeId": "STOREPICKUP", + "trackingUrl": "", + "trackingCode": "", + "shipFromAddress": {}, + "adjustments": [] + } + ], + "adjustments": [ + { + "orderAdjustmentTypeId": "SHIPPING_CHARGES", + "amount": 8.25, + "comments": "" + } + ], + "additionalFields": {} + } +} +``` + +### Top-Level Fields + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `commGatewayAuthId` | string | Yes | HotWax routing configuration. Partners should treat it as opaque. | +| `emailType` | string | Yes | Stable event key configured for the retailer, such as `READY_FOR_PICKUP` or `CANCEL_BOPIS_ORDER`. | +| `subject` | string | Yes | Configured email subject or event label. Do not use it as the sole event identifier. | +| `emailAddress` | string | Yes | Customer identity associated with the event. | +| `messageData` | object | Yes | Order, customer, facility, item, and adjustment data. | + +### `messageData` Fields + +| Field | Type | Notes | +| --- | --- | --- | +| `orderId`, `orderName` | string | Internal and customer-facing order identifiers. | +| `orderDate`, `completedDatetime` | string | Timestamps serialized by OMS. Empty when the lifecycle time does not apply. | +| `orderStatusUrl`, `orderUpdateUrl` | string | Customer links when configured; may be empty. | +| `grandTotal` | number | Total for the items represented by the event, including applicable adjustments. | +| `firstName`, `lastName`, `brandName` | string | Customer and retailer presentation fields. | +| `sandbox`, `isStorePickup` | boolean | Environment and fulfillment context. | +| `facilityAddress`, `shippingAddress`, `billingAddress` | object | Optional address maps. Missing data may appear as an empty object. | +| `items` | array | Items in scope for this event. For shipment-scoped events this can be a subset of the order. | +| `allItems` | array | All order items, including items outside the event subset. Use `itemStatus` to distinguish their lifecycle state. | +| `adjustments` | array | Order-level charges, discounts, and taxes. Item-level adjustments are nested on each item. | +| `additionalFields` | object | Event-specific extension data. Its keys vary by `emailType`. | + +Numbers are JSON numbers and booleans are JSON booleans. Do not coerce either to strings. Address and extension fields are optional; receivers must handle missing keys and empty objects. + +## Event Catalog + +`emailType` comes from the retailer's product-store email configuration, not a closed enum in the partner contract. Agree on the exact catalog during onboarding. Common order lifecycle examples include: + +| `emailType` | Typical meaning | +| --- | --- | +| `READY_FOR_PICKUP` | Store-pickup items are ready for the customer. | +| `REJECT_BOPIS_ORDER` | One or more pickup items could not be fulfilled. | +| `CANCEL_BOPIS_ORDER` | Pickup items or the order were cancelled. | +| `HANDOVER_BOPIS_ORDER` | The pickup order was handed to the customer. | + +Partners must safely reject or quarantine an unknown `emailType`; they must not silently map it to an unrelated automation. + +## Delivery Semantics + +The caller and gateway HTTP clients can retry transport failures. Treat delivery as **at least once** and make processing idempotent. + +The current canonical envelope does not provide a platform-generated delivery ID. Until one is added, use a partner-side deduplication key based on the fields that identify the business event, for example `emailType`, `orderId`, `emailAddress`, and the relevant event timestamp. Shipment-scoped events may repeat for the same order, so `orderId + emailType` alone is not sufficient. If strict deduplication is required, make a delivery ID an explicit onboarding blocker. + +## Omnisend Mapping Example + +For an Omnisend integration, the partner receiver can translate the canonical envelope to Omnisend's current `POST https://api.omnisend.com/api/events` API: + +| Unigate field | Omnisend field | +| --- | --- | +| `emailType` | Custom `eventName`, using a documented stable mapping. | +| `emailAddress` | `contact.email`. | +| `completedDatetime` or `orderDate` | `eventTime`, normalized to RFC 3339. | +| Partner-generated delivery UUID | `eventID` when the chosen Omnisend flow can use historical deduplication. | +| `messageData` | `properties`, transformed to the schema agreed with Omnisend. | + +Use a dedicated `origin` for an app or platform integration. Omnisend's current API requires the `Omnisend-Version: 2026-03-15` header and supports API-key or OAuth authentication. See the official [Events API](https://api-docs.omnisend.com/reference/post_events), [events overview](https://api-docs.omnisend.com/reference/events-overview), and [authentication guide](https://api-docs.omnisend.com/reference/authentication). + +Do not send the retailer's Omnisend secret to the partner-facing webhook. Store downstream credentials only in the system that calls Omnisend. + +## Certification Checklist + +1. Partner supplies test and production endpoint URLs, authentication requirements, supported event types, and rate limits. +2. HotWax supplies a test tenant, API root, canonical sample payloads, and a controlled test order. +3. HotWax confirms the provider-neutral outbound adapter is enabled and configures the partner endpoint. +4. Partner verifies schema validation, PII handling, duplicate handling, and unknown-event behavior. +5. HotWax triggers each agreed event with `sandbox: true` and records the Unigate response. +6. Partner provides the received event ID and downstream provider result for the same test. +7. Both sides test a retryable `5xx`, a permanent `4xx`, invalid authentication, and a duplicate event. +8. Production credentials and endpoint are configured only after the test matrix passes. + +## Related Documents + +- [UniMail overview](./readme.md) +- [`send#EmailCommunication` API](./services/send-email-communication.md) +- [Tenant onboarding](../tenant-onboarding.md) +- [Internal in-process adapter guide](./add-email-gateway.md) diff --git a/project-ideas/unigate/unimail/readme.md b/project-ideas/unigate/unimail/readme.md index 101c0836..3b1b2a1c 100644 --- a/project-ideas/unigate/unimail/readme.md +++ b/project-ideas/unigate/unimail/readme.md @@ -1,7 +1,8 @@ # UniMail — Uniform Email Gateway -UniMail is the email-side of Unigate. It gives callers a single API surface for sending transactional emails and tracking lifecycle events, regardless of which email provider the tenant has configured. The routing from abstract operation to concrete provider is driven entirely by database configuration — no code changes are needed to switch providers or add new ones. +UniMail is the email side of Unigate. It gives callers a single API surface for sending transactional emails and tracking lifecycle events, regardless of which installed email provider the tenant has configured. Switching between installed adapters is database configuration. Adding a new in-process adapter still requires provider service code and a Unigate deployment. +External partners use a different model: they host an HTTPS receiver while HotWax configures a provider-neutral outbound adapter. See [External Partner Integration for Order Email Events](./external-order-email-integration.md). --- @@ -24,7 +25,7 @@ sequenceDiagram participant Impl as MayurServices participant API as Mayur API - OMS->>Filter: POST /email/send\n api_key + tenant_Id headers + OMS->>Filter: POST /communication/email\n api_key + tenant_Id headers Filter->>Filter: hash api_key, query UserLoginKeyAndParty Filter-->>OMS: 401 if invalid Filter->>Router: set tenantPartyId, pass request @@ -55,13 +56,13 @@ Defines which services handle each abstract operation for a given provider. One ### `CommGatewayAuth` Per-tenant credential and endpoint data for a specific provider. One record per tenant+provider combination. -See the [CommGatewayAuth entity doc](../entity/comm-gateway-auth.md) for the full field list, encryption details, and setup workflow. +See the [CommGatewayAuth entity doc](../entity/CommGatewayAuth.md) for the current field list and setup workflow. --- ## APIs -### `POST /email/send` — Send an Email +### `POST /rest/s1/unigate/communication/email` — Send an Email Routes to `CommunicationServices.send#EmailCommunication`, which delegates to the provider's `sendEmailServiceName`. @@ -80,7 +81,7 @@ See the [send#EmailCommunication](./services/send-email-communication.md) servic --- -### `POST /email/flow` — Create an Email Flow +### `POST /rest/s1/unigate/communication/flow` — Create an Email Flow Routes to `CommunicationServices.create#EmailFlow`, which delegates to `createFlowServiceName`. See the [services directory](./services/) for detailed explanations of all email APIs. @@ -89,7 +90,8 @@ Routes to `CommunicationServices.create#EmailFlow`, which delegates to `createFl ## Related Documents - [CommGatewayAuth](../entity/CommGatewayAuth.md) — credential entity reference -- [Add Email Gateway](./add-email-gateway.md) — integrating a new email provider +- [External Partner Integration](./external-order-email-integration.md) — partner-owned receiver and order event contract +- [Add In-Process Email Gateway](./add-email-gateway.md) — internal adapter implementation - [send#EmailCommunication](./services/send-email-communication.md) — email sending service design - [create#EmailFlow](./services/create-email-flow.md) — automated flow provisioning design - [get#EmailFlow](./services/get-email-flow.md) — flow status retrieval design diff --git a/project-ideas/unigate/unimail/services/send-email-communication.md b/project-ideas/unigate/unimail/services/send-email-communication.md index 8cb05d1f..c3525bfa 100644 --- a/project-ideas/unigate/unimail/services/send-email-communication.md +++ b/project-ideas/unigate/unimail/services/send-email-communication.md @@ -1,34 +1,161 @@ # `send#EmailCommunication` -This is the primary service interface for sending an email via a communication gateway. +This is the canonical UniMail API for sending an order transactional email or triggering a provider workflow. + +## REST Endpoint + +```http +POST /rest/s1/unigate/communication/email +Content-Type: application/json +api_key: +tenant_Id: +``` + +`api_key` authenticates the caller. `tenant_Id` scopes the request to the tenant that owns the referenced `CommGatewayAuth` record. + +```bash +curl --fail-with-body --request POST \ + "${UNIGATE_API_ROOT}/communication/email" \ + --header "Content-Type: application/json" \ + --header "api_key: ${UNIGATE_API_KEY}" \ + --header "tenant_Id: ${UNIGATE_TENANT_ID}" \ + --data '{ + "commGatewayAuthId": "PARTNER_TEST", + "emailType": "READY_FOR_PICKUP", + "subject": "Your order is ready for pickup", + "emailAddress": "alex@example.com", + "messageData": { + "orderId": "10001", + "orderName": "WEB-10001", + "orderDate": "2026-08-11T08:10:00Z", + "completedDatetime": "", + "grandTotal": 108.25, + "firstName": "Alex", + "lastName": "Morgan", + "brandName": "Example Store", + "sandbox": true, + "isStorePickup": true, + "facilityAddress": {}, + "shippingAddress": {}, + "billingAddress": {}, + "items": [], + "allItems": [], + "adjustments": [], + "additionalFields": {} + } + }' +``` + +`UNIGATE_API_ROOT` is the API root, for example `https://unigate.example.com/rest/s1/unigate`. ## Interface Definition **Implements:** `co.hotwax.unigate.ApiInterfaceServices.send#EmailCommunication` -### Input Parameters +### Top-Level Input | Parameter | Type | Required | Description | -|---|---|---|---| -| `commGatewayAuthId` | String | Yes | Identifier for the `CommGatewayAuth` record that stores credentials and baseUrl. | -| `emailType` | String | Yes | The type of email being sent (e.g., `ORDER_COMPLETION`, `READY_FOR_PICKUP`). | -| `subject` | String | Yes | The subject line of the email. | -| `emailAddress` | String | Yes | The recipient's email address. | -| `messageData` | Map | Yes | A comprehensive map containing all order, customer, facility, shipping, billing, item, and adjustment data to be rendered in the email template. | +| --- | --- | --- | --- | +| `commGatewayAuthId` | string | Yes | Identifies the tenant's endpoint and credential record. | +| `emailType` | string | Yes | Configured order event key, such as `READY_FOR_PICKUP`. | +| `subject` | string | Yes | Configured email subject or provider event label. | +| `emailAddress` | string | Yes | Recipient or customer identity for the event. | +| `messageData` | object | Yes | Canonical order, customer, address, item, and adjustment data. | -**`messageData` Details:** -Includes fields like `orderId`, `orderName`, `grandTotal`, `firstName`, `lastName`, and nested maps/lists for `facilityAddress`, `shippingAddress`, `billingAddress`, `items` (with `adjustments`), and order-level `adjustments`. +### Order and Customer Data -### Output Parameters +| `messageData` field | Type | Description | +| --- | --- | --- | +| `orderId` | string | Internal order identifier. | +| `orderName` | string | Customer-facing order name or number. | +| `orderDate` | string | Serialized order timestamp. | +| `completedDatetime` | string | Serialized completion timestamp when applicable. | +| `orderStatusUrl` | string | Customer order-status link when configured. | +| `orderUpdateUrl` | string | Customer self-service update link when configured. | +| `grandTotal` | number | Event-scope order total. | +| `firstName`, `lastName` | string | Customer name. | +| `brandName` | string | Retailer display name. | +| `sandbox` | boolean | Whether the event is a test event. | +| `isStorePickup` | boolean | Whether the event includes a store-pickup item. | +| `additionalFields` | object | Event-specific extension data; keys vary by `emailType`. | -| Parameter | Type | Required | Description | -|---|---|---|---| -| `response` | Map | Yes | The raw response object/map returned by the provider API. | -| `requestBody` | Map | No | The processed request body sent to the provider, useful for logging and debugging. | +### Addresses + +`facilityAddress`, `shippingAddress`, and `billingAddress` are optional maps. Address maps can contain: + +| Field | Applies to | +| --- | --- | +| `toName`, `address1`, `address2`, `city`, `stateProvinceGeoId`, `postalCode`, `countryGeoId`, `phoneNumber` | All address types | +| `facilityName`, `latitude`, `longitude` | Facility address | + +Missing address data can be omitted or represented as an empty object. + +### Items + +`items` is the event-scoped subset. `allItems` contains the complete order item list and is useful for split-shipment or partial-cancellation templates. + +Each item can contain: + +| Field | Type | Description | +| --- | --- | --- | +| `productId`, `shipGroupSeqId` | string | Product and fulfillment group identifiers. | +| `color`, `size` | string | Variant attributes. | +| `quantity`, `unitPrice` | number | Item quantity and unit price. | +| `itemDescription`, `productName` | string | Display descriptions. | +| `productImageUrl`, `parentProductImageUrl`, `itemUrl` | string | Product links. | +| `itemStatus` | string | OMS item status ID. | +| `shipmentMethodTypeId` | string | Fulfillment or shipment method. | +| `trackingUrl`, `trackingCode` | string | Shipment tracking data when available. | +| `shipFromAddress` | object | Origin facility address for this item. | +| `adjustments` | array | Item-level charge, tax, and discount records. | + +### Adjustments + +Order-level `messageData.adjustments` and item-level `adjustments` use the same shape: + +| Field | Type | Description | +| --- | --- | --- | +| `orderAdjustmentTypeId` | string | OMS adjustment type ID. | +| `amount` | number | Signed adjustment amount. | +| `comments` | string | Optional label or source detail. | + +## Response + +The service returns the gateway response and may include the rendered provider request: + +```json +{ + "response": { + "statusCode": 202, + "response": "" + }, + "requestBody": {} +} +``` + +The inner response body is provider-specific. Callers must inspect `response.statusCode`; an outer REST `2xx` does not by itself prove that the downstream provider accepted the event. + +## Routing and Error Handling + +1. `TenantAuthFilter` validates `api_key` and `tenant_Id`. +2. `CommunicationServices.send#EmailCommunication` loads `CommGatewayAuth` by `commGatewayAuthId`. +3. It loads `CommGatewayConfig` and resolves `sendEmailServiceName`. +4. The configured in-process adapter transforms and sends the request. +5. The adapter returns its downstream status and response. + +Stable routing errors include: + +| Condition | Result | +| --- | --- | +| Missing or invalid authentication headers | HTTP `401` | +| Unknown `commGatewayAuthId` | `No valid gateway auth config found for tenant` | +| Missing gateway configuration | `Email gateway configuration not found` | +| Missing `sendEmailServiceName` | `Gateway config is missing sendEmailServiceName service name` | + +HTTP clients in the OMS and installed adapters can retry transport failures. Receivers must tolerate duplicate events. -## Implementation Responsibility +## Related Documents -1. **Load Credentials:** Fetch `CommGatewayAuth` using the provided `commGatewayAuthId`. -2. **Template Rendering:** Render the provider-specific request payload using FreeMarker and the provided `messageData`. -3. **API Call:** Call the provider's API endpoint (e.g., `/send`) to dispatch the email. -4. **Error Handling:** Handle API errors and set `response` accordingly. +- [External partner integration](../external-order-email-integration.md) +- [UniMail overview](../readme.md) +- [Tenant authentication](../../tenant-auth-filter.md)