Skip to Content
Deals Pipeline — Engineering Deep Dive

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
5. Key Gotchas
View all
New Content
Deals Pipeline — Engineering Deep Dive
4-Tier Discount Matching Algorithm
New Content
Deals Pipeline — Engineering Deep Dive
Dutchie to Redis Sync Pipeline

10-Minute BullMQ Sync Cycle

The inventory sync runs every 10 minutes on the mintinvsvc Railway service as a BullMQ repeating job with 6 phases:

  1. Phase 1: Inventory Sync — Fetches product inventory from Dutchie Backoffice API per store
  2. Phase 2: Product Enrichment — Adds Odoo images and metadata to products
  3. Phase 3: Discount Sync — Fetches discounts from Dutchie POS/Backoffice API
  4. Phase 3b: Odoo Discount Sync — (conditional) Pushes discounts to Odoo mint.discount records
  5. Phase 4: Cache Refresh — Writes PostgreSQL data to Redis
  6. Phase 5: Odoo Sync — Pushes inventory data back to Odoo (if enabled)

Phase 3 Detail: Discount Sync

Two API strategies tried in order:

  1. POS API (primary): GET api.pos.dutchie.com/discounts/v2/list with per-LSP Basic auth key (env var: DUTCHIE_POS_API_KEY_{lspId}). Used for AZ stores.
  2. Backoffice API (fallback): POST v2/discount/get-discount via 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
New Content
Deals Pipeline — Engineering Deep Dive
Dutchie POS Backoffice (Primary Source)
New Content
Deals Pipeline — Engineering Deep Dive
Static CSV (Daily Deals Page)
New Content
Deals Pipeline — Engineering Deep Dive
Three Customer-Facing Deal Pages
New Content
Deals Pipeline — Engineering Deep Dive
PTL Webhook Pipeline

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_id in request body.
  • POST /api/locations/:id/discounts/bulk — called by ptl-match-products.mjs script. 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
New Content
Deals Pipeline — Engineering Deep Dive
Key Gotchas and Troubleshooting
New Content
Deals Pipeline — Engineering Deep Dive
PTL — Promotional Template Library (Odoo)