iRewardify — IRW
Voucher vendor adapter — lazy bearer login, custom unsigned webhook, two-phase async delivery with dual code/link formats.
Quick facts
Code IRW · Line voucher · Auth bearer (login → 23h in-memory token) · Model async (create → poll → deliver) · Webhook custom, unsigned · Delivery card code+pin or claim link+challenge · Adapter services/external_vendors/vouchers/irewardify_vendor.go · Tests orchestration + mocky (no contract test)
IRewardifyVendor embeds BaseVendor + auth state (irewardify_vendor.go:22); registered in factory.go:58. Seeded IsAsync:true, HasWebhook:true, MaxQuantity:1. Host already includes /api/v1.
Auth
Auth → POST {Host}/customer/login {email:username, password} → token stored in memory with a synthetic now+23h expiry (the vendor gives no expiry). getAuthConfig refreshes lazily when the token is empty/expired and returns a bearer config. RedoAuth clears memory + removes a Valkey key, then re-auths.
The "cached auth token" is aspirational. The Valkey key irewardify:auth_token:<username> is removed in RedoAuth but never written anywhere — real caching is in-process only, so each new instance re-authenticates. Don't rely on cross-pod token sharing.
vendor_attributes: host, username (used as email), password. No api_key/timeout/delay.
Catalog
GetCatalogBatched only. Two-step: GET /customer/products (listing) → a 10-worker pool fetches GET /customer/products/{_id} per product. Each product's variants[] → one Product with fixed denomination (min==max==variant_price). CreateOrder needs both VendorProductID (Mongo _id) and SKU (variant.item_id).
Discount/markup come from the listing API, not the detail (detail returns 0) — the code carries them forward. discounted && discount>0 → use discount; else markup>0 → negative discount = markup/premium.
Order create
POST {Host}/checkout — a single-item order with hard-coded N/A shipping/billing filler (digital). Async: returns no vouchers, StatusAccepted + ShouldRetry; captures orderId for polling. Create errors inspect the message: "external ID already exists" → duplicate, no retry; "Insufficient funds" → retry.
Order status / poll
GET {Host}/order-details/{orderId}. 404/400 or body "not found" → ErrShouldReCreate (orchestrator re-creates). Status map: Rewards Delivered→complete; Partial Rewards Delivered→partial; Funding Received/Processing→processing; Failed→failed + ShouldReCreate; anything else→processing (keep polling). Vouchers are not returned here.
Webhook
Overrides NormalizeWebhookPayload for the custom shape {orderId, event, status, message}. Delivered to POST /webhooks/vendor/IRW.
Unsigned. Production webhooks carry no signature; with no webhook_secret the factory uses a NoopWebhookVerifier. Idempotency key = orderId:status (one webhook per status transition is deduped).
Delivery
GET {Host}/order/delivery/{orderId}; message "not ready yet" → keep polling (no error). parseDeliveryItem auto-detects two formats:
- Card (
cardCodepresent):Code=cardCode,Pin=pin(polymorphic string/number), expiry2006-01-02or now+365d default. - Link (
ClaimLink/Idpresent):Code=ClaimLinkChallengeAnswer,ClaimURL=ClaimLink, expiry now+1yr.
Reference numbers are IRW_<id>.
Balance
GET {Host}/customer/wallet → three named floats (walletOne/Two/Three) → three BalanceResponse rows (VendorWalletID = "WalletOne"…). No vendor currency — uses the passed currencyCode or defaults USD.
Quirks
- Two-phase async delivery: create →
/order-details(poll) →/order/delivery(codes). ErrShouldReCreateon 404/400/"not found"/vendorFailed.- Dual delivery formats auto-detected in one endpoint.
CancelVoucherposts to/order/{id}/cancel(or/order/cancelby reference);ProductAvailabilitystubbed totrue.- Reference codes
IRW-<base>-<unix>(uniqueness matters — duplicate external ID is non-retryable).
Tests, mock, docs
Mocky route frontend/mocky-balboa/src/routes/irewardify.ts (login/products/checkout/order-details/delivery/wallet + _mock/complete|auto-fulfill|webhook-url|refund-balance). Orchestration test test/clientapi/irewardify_orchestration_test.go covers create→poll→deliver, retries, invalid-creds, insufficient-funds, out-of-stock, and the unsigned webhook POST. No dedicated Go contract test. Postman collection under docs/iRewardify/.
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.
NeoCurrency — NEO
Voucher vendor adapter — lazy OAuth2 bearer (24h JWT, reactive 401-retry), synchronous inline delivery, link-only vouchers, and heavy spec drift.