# Business Rules ## Fixed processing order 1. Validate the invocation and parse one fixed `RES_DETAIL` XML. 2. Require exactly one XML business date and keep one audit record for every `G_RESERVATION` in XML order. 3. Require `RATE_CODE` so whitelist membership is knowable. 4. Classify a trimmed, uppercased rate code outside the whitelist as `excluded_rate_code`. 5. Validate every whitelist candidate; classify invalid rows as `validation_failed`. 6. Deduplicate valid candidates by `DISP_ROOM_NO + ARRIVAL`; retain the first XML occurrence and classify later occurrences as `duplicate` pointing to the first source sequence. 7. Compute integer `NIGHTS = DEPARTURE - ARRIVAL`. Zero is legal; negative is invalid. 8. Validate the complete fixed price table, then apply the approved zero-price exception or exact three-key match. Classify unmatched candidates as `price_unmatched`. 9. Assign `channel_key` and conditional `kb_amount` as row facts. 10. Generate the 19-column daily XLSX, `result.json`, and path-free `structured-result.json` from the same in-memory records. 11. Independently replay the XML, rules, prices, routing, workbook, artifacts, outcomes, and structured records before reporting success. Any row-level validation or price error fails the formal invocation. On failure, already excluded, duplicate, or price-unmatched outcomes remain explicit. Other candidates that did not reach a validated result become `validation_failed` with `BATCH_NOT_VALIDATED`. ## Rate-code whitelist `GRPA1`, `GRPA2`, `GRPA3`, `GRPA4`, `GRP1`, `WHO1`, `WHO2`, `WHO3`, `WHO4`, `LTLT`, `LBLT`, `LBSM`, `LBMS`, `LBW1`, `LBKB`, `LBLS`, `WHKR2100B`, `GL2100B`, `GL2200KR`, `GLSPCB`. ## Reservation text For reservation-level `RES_COMMENT` and `TRACE_TEXT`, take the first non-empty value in XML order. Leave blank when every candidate is empty. Never concatenate values or expand one reservation into multiple rows. ## Company normalization Normalize company identity for price matching and channel facts: 1. trim, uppercase, and keep only `A-Z` and `0-9`; 2. use fixed substring keywords: - `LIANTAI` → `LIAN TAI` - `QBD` → `QBD` - `RAINBOW` → `RAINBOW/AI` - `FENGRUN` → `FENGRUN` - `HANATOUR` or `HANA` → `HANA TOUR` - `HONGTAI` → `HONGTAI` - `GUANGZHOUGOEASY` or `GOEASY` → `GUANGZHOU GO EASY` 3. when no keyword matches, use the compact normalized name; 4. when more than one keyword group matches, fail instead of guessing. This is deterministic keyword matching, not edit distance. ## Pricing Use bundled `价格对照.xlsx` as a fixed, non-user-editable rule source. - Normalize `RATE_CODE` with `upper(trim(value))`. - Compare amounts numerically, so `900` equals `900.00`. - Reject negative reference amounts/totals, invalid rows, ambiguous companies, and every duplicate normalized `COMPANY + RATE_CODE + Opera amount` key. - Before the normal lookup, set numeric `REAL PRICE = 0` only when: - company identity is `RAINBOW/AI` or `GUANGZHOU GO EASY`; and - rate code is `LBMS` or `LBSM`. - The exception ignores `EFFECTIVE_RATE_AMOUNT`. - Every other retained row must exactly match normalized `COMPANY_NAME + RATE_CODE + EFFECTIVE_RATE_AMOUNT`. - Write reference `总价` as static `REAL PRICE`. - Write static `TOTAL PRICE = REAL PRICE * NO_OF_ROOMS * NIGHTS`. - An unmatched candidate fails the entire invocation. ## Channel facts Keep original XML `COMPANY_NAME`; assign only the derived `channel_key`. | Condition | `channel_key` | |---|---| | `QBD` | `QBD` | | `LIAN TAI` and rate in `{LBLT, LTLT}` | `LIANTAI-FIT` | | `LIAN TAI` and another whitelisted rate | `LIANTAI-GROUP` | | `RAINBOW/AI` or `GUANGZHOU GO EASY`, any rate | `DY-AI-Easy-KB` | | `FENGRUN` | `FENGRUN` | | any other company | sanitized actual company name | For other companies, trim, remove `: \ / ? * [ ]`, and truncate to 31 characters. Resolve different-company collisions with `-2`, `-3`, and so on. Set `kb_amount = NO_OF_ROOMS * 100` only for `DY-AI-Easy-KB`; do not multiply by nights and do not add it to `TOTAL PRICE`. ## Business date, XLSX, and types - Derive the business date only from XML group dates, never runtime time or filename. - Daily filename and sheet name: `M.D.xlsx` and `M.D`. - Preserve retained XML order. - Use the bundled daily template; do not add monthly or channel worksheets. - Headers occupy row 1; data begins row 2. - Write dates as real Excel dates with `DD-MMM-YY`. - Write counts and prices as static numbers, never formulas. - Write identifiers and descriptive fields as text. - Require `ADULTS >= 0`, `CHILDREN >= 0`, `NO_OF_ROOMS > 0`, `EFFECTIVE_RATE_AMOUNT >= 0`, and `DEPARTURE >= ARRIVAL`. - `DEPARTURE = ARRIVAL` yields `NIGHTS = 0` and `TOTAL PRICE = 0`. - Preserve allowed blank fields and complete selected comment/trace text. ## Prohibited behavior Do not access OSS, embed credentials, generate/update a monthly workbook, query/write a database, infer Group Code from `BLOCK_CODE`, or derive structured facts by reopening the generated XLSX.