Deals Pipeline — Engineering Deep Dive
How deals flow from Dutchie POS and Odoo PTL through Redis to the frontend. Covers deal sources, sync pipeline, discount matching, and customer-facing pages.
| Responsible | Juan Palomino |
|---|---|
| Last Update | 04/06/2026 |
| Completion Time | 1 day 16 hours |
| Members | 1 |
4-Tier Discount Matching Algorithm
5. Key Gotchas
View all10-Minute BullMQ Sync Cycle
The inventory sync runs every 10 minutes on the mintinvsvc Railway service as a BullMQ repeating job with 6 phases:
- Phase 1: Inventory Sync — Fetches product inventory from Dutchie Backoffice API per store
- Phase 2: Product Enrichment — Adds Odoo images and metadata to products
- Phase 3: Discount Sync — Fetches discounts from Dutchie POS/Backoffice API
- Phase 3b: Odoo Discount Sync — (conditional) Pushes discounts to Odoo mint.discount records
- Phase 4: Cache Refresh — Writes PostgreSQL data to Redis
- Phase 5: Odoo Sync — Pushes inventory data back to Odoo (if enabled)
Phase 3 Detail: Discount Sync
Two API strategies tried in order:
- POS API (primary):
GET api.pos.dutchie.com/discounts/v2/listwith per-LSP Basic auth key (env var:DUTCHIE_POS_API_KEY_{lspId}). Used for AZ stores. - Backoffice API (fallback):
POST v2/discount/get-discountvia authenticated Backoffice session. Used for FL, IL, MI, MO, NV stores where POS API keys are not configured.
Code: packages/inventory-service/services/discountSync.js lines 411-436
Data Flow
Dutchie POS/Backoffice API
|
v
PostgreSQL (discounts table)
- Composite PK: {locationUUID}_{discountId}
- JSONB fields: products, brands, product_categories
- Day-of-week booleans: monday..sunday
- source column: dutchie or ptl
|
v (Phase 4: cacheSync.js)
Redis Cache
- Key: discounts:{locationUUID}
- Value: JSON array of all active discounts
|
v (HTTP GET /api/locations/:id/discounts)
Frontend (Cloudflare Pages)
Infrastructure
- Service: mintinvsvc on Railway (Nixpacks, Node 20)
- Domain:
mintinvsvc-production-6aa5.up.railway.app - Redis: Railway Redis at
nozomi.proxy.rlwy.net:18208 - PostgreSQL: Railway Postgres at
mainline.proxy.rlwy.net:40686 - Job System: BullMQ with concurrency=1 (prevents overlapping)
Key Code References
- Job schedule:
packages/inventory-service/jobs/queues.js:100 - Job processor:
packages/inventory-service/jobs/processors/inventorySync.js:50-142 - Discount sync:
packages/inventory-service/services/discountSync.js:398-485 - Cache refresh:
packages/inventory-service/services/cacheSync.js:10-44
Odoo PTL to Redis via Webhook
When marketing publishes a PTL day in Odoo, deals are pushed directly to Redis via webhook — bypassing the normal 10-minute sync cycle.
Flow
Odoo: mint.ptl.day → action_publish()
|
v
Creates/updates mint.discount records (source=ptl)
|
v
POST mintinvsvc/api/webhook/ptl-discount-sync
Body: { location_id, source: "ptl", discounts: [...] }
Header: X-API-Key
|
v
mergePtlDiscounts() — shared logic:
1. Ensures each discount has a ptl_-prefixed id
2. Reads existing Redis cache for the location
3. Filters out old ptl_ discounts (keeps Dutchie ones)
4. Merges Dutchie + new PTL discounts
5. Writes merged set back to Redis
Two Endpoints, Same Logic
- POST /api/webhook/ptl-discount-sync — called by Odoo action_publish(). Takes
location_idin request body. - POST /api/locations/:id/discounts/bulk — called by
ptl-match-products.mjsscript. Takes locationId in URL path.
Both call mergePtlDiscounts() which normalizes IDs with a ptl_ prefix so they can be distinguished from Dutchie discounts on the next merge.
PTL ID Format
PTL discounts use the format {locationUUID}_ptl_{discountId} for their id field. Dutchie discounts use {locationUUID}_{numericId}. The ptl_ substring is what the merge logic uses to strip old PTL deals before adding new ones.
Key Code Paths
- Odoo publish:
letsgomint/mint_command_center/models/ptl_day.py:132(action_publish) - Odoo webhook fire:
ptl_day.py:235(_push_discounts_to_redis) - Odoo payload builder:
ptl_day.py:294(_discount_to_webhook_payload) - Express webhook handler:
packages/inventory-service/api/server.js(POST /api/webhook/ptl-discount-sync) - Express bulk handler:
server.js(POST /api/locations/:id/discounts/bulk) - Shared merge logic:
server.js(mergePtlDiscounts function) - Match script:
scripts/ptl-match-products.mjs