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:
- Generate a
code_verifier(random 43-char string) andcode_challenge(SHA-256, base64url) GET /authorizewith client_id, challenge and redirect URI — establishes a sessionPOST /signinwith username/password/orgname against that session- Extract the authorization code from the redirect
POST /tokenexchanging code + verifier for tokens- Use the
id_tokenas 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 taxType | Percentage | Chrilan vatCat |
|---|---|---|
disabled | any | EXEMPT |
includedPercent / addonPercent / addonBreakpoint / surcharge | 0 | ZERO_RATED_BY_NATURE |
| same types | > 0 | STANDARD_RATED |
| (no active rates on the class) | — | EXEMPT |
| (no taxClassRef / lookup miss) | — | STANDARD_RATED (fallback) |
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)
- 1 tax class (id=1, activeTaxRateRefs=[1])
- 64 tax rates — only id=1 active (16% VAT,
includedPercent); ids 2–64 disabled - All 342 menu items have taxClassRef=1 → resolve to
STANDARD_RATED
Chrilan product construction
| Chrilan field | Value |
|---|---|
plu | Oracle menuItemId (string) |
name | Resolved item name |
type | singleItem |
vatCat | Resolved via tax map (fallback STANDARD_RATED) |
department | Store default department (default 50203301) |
countryOfOrigin | ZM |
user | AUTOMATED |
sellingIncl | Sequence-1 price |
qty | 0 |
archive | false |
Operational notes
- Both query parameters and
Simphony-*headers are required on Oracle tax/menu calls; omitting either yields HTTP 403. - The LocRef in the menu path must be the exact Oracle location reference (e.g.
PCK_3103), not an internal store code. - Skipped items (missing PLU or name) are counted and logged in each sync summary.
- To enable sync for a store, see the user guide.