OctoWiki
Vendor AdaptersVoucher Vendors

EpinForce — EPIN

Voucher vendor adapter — stateful shared cart with a per-token mutex, async with NO webhook (poll-only), and XLSX/ZIP file code delivery.

Quick facts

Code EPIN · Line voucher · Auth static bearer API token · Model async, poll-only (no webhook) · Cart stateful shared cart (clear→add→checkout, per-token mutex) · Delivery XLSX / ZIP files · Adapter services/external_vendors/vouchers/epinforce_vendor.go · Tests contract + mocky

Deliberately unusual on three axes — the adapter is shaped around them. Registered factory.go:60. Seeded IsBulk:true, MaxQuantity:100. See Order Lifecycle.

Auth

Static bearer token (apikt_…) in VendorConfig.APIKey (from the api_key attribute), sent on every call. No login/refresh. Auth/Ping probe GET /balance. vendor_attributes: host (includes /vendor-api), api_key.

Catalog

GetCatalogBatchedGET /products?page=N&limit=100. isFixed = DenominationType != "range". Category = BrandName. Terminates on collected >= total or short page.

Order create — the stateful shared cart (headline)

There is no atomic order-create. An order is produced by driving one server-side cart: clear → add → checkout. That cart is one resource per API token, so two concurrent orders on the same token would interleave and check out each other's items.

The concurrency model: a package-level sync.Map of mutexes keyed by cartKey() = Host + "|" + apiToken. CreateOrder takes cartMutexFor(cartKey()).Lock() around the entire clear→add→checkout section.

  • ClearGET /cart then DELETE /cart/{productId} per line (failure is logged and non-fatal).
  • AddPOST /cart {productId, quantity[, denomination]}; >=500 retryable, 4xx terminal validation.
  • CheckoutPOST /cart/checkout {}; >=500 or "insufficient" retryable. Returns orderId; StatusAccepted + ShouldRetry (poll for fulfilment). No vouchers at create.

The mutex guards a single process only — not distributed. Two Octopus instances sharing one token can still corrupt each other's carts. And checkout accepts no idempotency key, so a lost checkout response can't be de-duped — mitigated by always clearing the cart first and by persisting vendor_order_id (retries then route through GetOrderStatus, not CreateOrder).

Async — NO webhook — poll only

NormalizeWebhookPayload is overridden to hard-error ("EpinForce does not support webhooks; order fulfilment is poll-only"), so a misrouted payload fails loudly. GetOrderStatusGET /orders/{id}. 404 → ErrShouldReCreate. Status map: Confirmed→complete, PartiallyConfirmed→partial, Failed/CancelledShouldReCreate (keep retrying), unknown→processing (keep polling).

Delivery — XLSX / ZIP files

Codes come only as files: GET /orders/{id}/download (404 → not ready, keep polling). extractVouchersFromDownload opens the payload as a zip (an .xlsx is a zip) and branches: a [Content_Types].xml entry → single workbook; otherwise → ZIP bundle of .xlsx files (denomination parsed from {name}-{denom}.xlsx). parseVoucherWorkbook (excelize) does header-driven column detection (code required; serial/pin optional); reference = EPIN_<serial>, default expiry now+365d.

Balance

GET /balance{balance:"<string>"} → single BalanceResponse (VendorWalletID:"default", default USD).

Quirks

  • Cart-mutex concurrency (single-process only); no checkout idempotency key.
  • Error envelope {statusCode, message}; add/checkout treat ≥500 as retryable, 4xx terminal.
  • 404 differs by call: GetOrderStatus → recreate; GetVouchers → "not ready".
  • CancelVoucher unsupported; ProductAvailability optimistic true.
  • rawReq logs an {"add": …} envelope the vendor never receives (cosmetic).

Tests, mock, docs

Contract test epinforce_contract_test.go (build tag contract) — auth, fixed+range catalog, create→poll→deliver with serials, Failed/404 → ShouldReCreate, chaos. Mocky route frontend/mocky-balboa/src/routes/epinforce.ts models async auto-fulfil via waitUntil (webhookless delivery), _mock/complete|fail|balance|auto-fulfill. Docs: the vendor's own API docs are external (staging.api.epinforce.com/vendor-api/docs).

On this page