🍗 PCK ZRA Middleware Wiki

Menu Synchronization

Products must exist in Chrilan's catalog before invoices can reference them. The menu sync pipeline keeps every store's Chrilan product catalog aligned with its Oracle Simphony menu — automatically, every 60 seconds.

The pipeline

MenuSyncScheduler (every 60 seconds)
  │
  ├── Load active stores with menuSyncEnabled = true
  │
  └── For each store: MenuSyncService.syncMenuForStore(store)
        │
        ├── 1. Validate config
        │     oracleMenuPath and chrilanApiKey must be present
        │
        ├── 2. Fetch menu items from Oracle STS
        │     GET /menus/{menuPath}  (paginated; loops while hasMore)
        │
        ├── 2.5. Fetch tax definitions from Oracle STS
        │     GET /taxes?OrgShortName={}&LocRef={}&RvcRef={}
        │     Builds taxClassId → Chrilan vatCat lookup map
        │     On failure → graceful fallback to STANDARD_RATED
        │
        ├── 3. Convert Oracle items → Chrilan products
        │     Skip items with no menuItemId or no name
        │     Extract price (priceSequence == 1)
        │     Resolve vatCat from the item's taxClassRef
        │
        ├── 4. Push to Chrilan
        │     POST /product/createBulk (per-store API key)
        │
        └── 5. Log the summary (total / converted / skipped)

Oracle STS authentication (PKCE)

Oracle STS requires a full OAuth 2.0 authorization-code flow with PKCE, executed programmatically:

  1. Generate a code_verifier (random 43-char string) and code_challenge (SHA-256, base64url)
  2. GET /authorize with client_id, challenge and redirect URI — establishes a session
  3. POST /signin with username/password/orgname against that session
  4. Extract the authorization code from the redirect
  5. POST /token exchanging code + verifier for tokens
  6. Use the id_token as Bearer token for all STS calls

The token is cached in memory and reused until 60 seconds before expiry.

Price extraction

Oracle items carry multiple price levels. The middleware selects the dine-in/standard price (priceSequence == 1):

"prices": [
  {"priceSequence": 1, "price": 49, "name": "St Down",  "level": 1},  ← selected
  {"priceSequence": 2, "price": 49, "name": "Takeaway", "level": 2},
  {"priceSequence": 3, "price": 55, "name": "Yango",    "level": 3}
]

If no sequence-1 price exists, the item syncs with price 0 and a debug dump for investigation — it is not skipped.

Tax class → VAT category resolution

Oracle tax definitions come back as taxClasses[] (with activeTaxRateRefs) and taxRates[] (with percentage as a decimal fraction and taxType). The resolution chain per item:

menuItem.definitions[0].taxClassRef            (e.g. 1)
  → taxClasses[taxClassId = 1].activeTaxRateRefs   (e.g. [1])
    → taxRates[taxRateId = 1].taxType / percentage
      → taxType == "disabled"        → EXEMPT
      → percentage == 0              → ZERO_RATED_BY_NATURE
      → percentage > 0               → STANDARD_RATED
Oracle taxTypePercentageChrilan vatCat
disabledanyEXEMPT
includedPercent / addonPercent / addonBreakpoint / surcharge0ZERO_RATED_BY_NATURE
same types> 0STANDARD_RATED
(no active rates on the class)EXEMPT
(no taxClassRef / lookup miss)STANDARD_RATED (fallback)
Decimal fractions Oracle returns percentage as a decimal fraction (0.16 = 16%), not a whole number. Comparisons use BigDecimal against zero, which handles this correctly.

Verified PCK production data (March 2026)

Chrilan product construction

Chrilan fieldValue
pluOracle menuItemId (string)
nameResolved item name
typesingleItem
vatCatResolved via tax map (fallback STANDARD_RATED)
departmentStore default department (default 50203301)
countryOfOriginZM
userAUTOMATED
sellingInclSequence-1 price
qty0
archivefalse

Operational notes