# Business Rules ## Processing order 1. Parse the fixed `RES_DETAIL` XML and require exactly one XML business date. 2. Create one in-memory source record for every reservation in XML order; no later stage may discard that audit record. 3. Require every reservation to contain `RATE_CODE` so whitelist membership is knowable. 4. Classify rows whose trimmed, uppercased `RATE_CODE` is not in the whitelist as `excluded_rate_code`. 5. Validate required fields on the remaining rows and classify invalid rows as `validation_failed`. 6. Deduplicate remaining valid rows by `DISP_ROOM_NO + ARRIVAL`; retain the first XML occurrence and classify later occurrences as `duplicate` with a pointer to the first source sequence. 7. Preserve the relative XML order of all retained rows and compute static non-negative integer `NIGHTS = DEPARTURE - ARRIVAL`. Same-day arrival and departure is a valid zero-night record; a departure before arrival is invalid. 8. Validate the entire bundled price table, then apply the approved zero-price exception or match and write static daily `REAL PRICE`; calculate static daily `TOTAL PRICE = REAL PRICE * NO_OF_ROOMS * NIGHTS`. Classify an unmatched row as `price_unmatched`. 9. Assign deterministic channel keys and conditional KB values, then generate the daily workbook. Only legacy `daily-monthly` mode also routes the same in-memory rows into a monthly workbook. 10. Write `result.json` and the all-source-record `structured-result.json`, then independently validate every requested XLSX output, artifact hash, derived key, outcome reconciliation, and structured payload before reporting success. Any row-level validation or price error fails the formal invocation. Rows that were valid candidates but could not reach a fully validated formal result become `validation_failed` with `BATCH_NOT_VALIDATED`; filtered, duplicate, and price-unmatched conclusions remain unchanged in the failed structured payload. ## Rate-code whitelist `GRPA1`, `GRPA2`, `GRPA3`, `GRPA4`, `GRP1`, `WHO1`, `WHO2`, `WHO3`, `WHO4`, `LTLT`, `LBLT`, `LBSM`, `LBMS`, `LBW1`, `LBKB`, `LBLS`, `WHKR2100B`, `GL2100B`, `GL2200KR`, `GLSPCB`. ## Multi-value 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. ## Price matching Use the bundled latest `references/价格对照.xlsx` as a fixed, non-user-editable rule source. - Normalize company identity on both the price-table side and the XML/output side before price matching or standard-channel routing: 1. trim, uppercase, and remove every space and punctuation character, retaining only `A-Z` and `0-9`; 2. use fixed substring keywords, not edit distance, to recognize `LIANTAI → LIAN TAI`, `QBD → QBD`, `RAINBOW → RAINBOW/AI`, `FENGRUN → FENGRUN`, `HANATOUR` or `HANA → HANA TOUR`, `HONGTAI → HONGTAI`, and `GUANGZHOUGOEASY` or `GOEASY → GUANGZHOU GO EASY`; 3. if no keyword group matches, use the compact normalized name as an exact identity; 4. if more than one keyword group matches, fail rather than guessing. - Match normalized `COMPANY_NAME + RATE_CODE + EFFECTIVE_RATE_AMOUNT` to normalized price-table `COMPANY'S NAME + RATE CODE + Opera展示的价格`. - Trim and uppercase rate codes. - Compare price amounts numerically, so `900` equals `900.00`. - Reject negative Opera display amounts or total prices in the fixed price table. - Before the three-key lookup, write numeric `REAL PRICE = 0` when and only when both conditions hold: - normalized company identity is `RAINBOW/AI` or `GUANGZHOU GO EASY`; and - normalized `RATE_CODE` is `LBMS` or `LBSM`. - The zero-price exception ignores `EFFECTIVE_RATE_AMOUNT`. It writes daily `REAL PRICE = 0` and therefore daily `TOTAL PRICE = 0`; the monthly row receives the same two numeric zeros. Other rate codes for those companies still require an exact normalized three-key price match. - Write the matched price-table `总价` directly as daily `REAL PRICE`, then calculate daily `TOTAL PRICE = REAL PRICE * NO_OF_ROOMS * NIGHTS`. - Validate the complete normalized three-field price key before processing. Any duplicate, including same-total duplicates unused by the current XML, fails the entire run. - Any unmatched retained reservation fails the entire run. ## Channel routing Standard routing uses the same normalized company identity described above. The original XML `COMPANY_NAME` remains unchanged in output cells. | Condition | Worksheet | |---|---| | Normalized identity `QBD` | `QBD` | | Normalized identity `LIAN TAI` and `RATE_CODE` in `{LBLT, LTLT}` | `LIANTAI-FIT` | | Normalized identity `LIAN TAI` and other whitelisted `RATE_CODE` | `LIANTAI-GROUP` | | Normalized identity `RAINBOW/AI` or `GUANGZHOU GO EASY` (all rate codes) | `DY-AI-Easy-KB` | | Normalized identity `FENGRUN` | `FENGRUN` | | Any other company | Sanitized actual company name | For other companies, trim the name, remove `: \\ / ? * [ ]`, and truncate to 31 characters. Reuse the existing sheet for the same actual company. If a different company collides after sanitizing/truncation, append `-2`, `-3`, and so on while shortening the base to keep the result at most 31 characters. ## Business date and filenames - Derive the date from the XML arrival/group date, not the runtime clock or upload filename. - Daily filename and sheet name: `M.D.xlsx` and `M.D` (for example `7.20.xlsx`). - Month key: XML `ARRIVAL` year and month. - Monthly filename: `各渠道情况-YYYY年MM月-更新至M.D.xlsx`. - `更新至` is the maximum `ARRIVAL` present anywhere in the monthly workbook, so an older backfill cannot move the filename backwards. ## Monthly update These rules apply only to legacy `daily-monthly` mode. The `daily` database-ingestion mode never reads or writes a monthly workbook. - When no monthly base is supplied, start from the bundled blank monthly template. - A supplied monthly base must already follow the current 19/20-column contract. Legacy 18-column monthly workbooks and the transitional 19-column KB layout are rejected rather than upgraded. - Keep different natural months in different workbooks. - Before inserting the current date, delete that `ARRIVAL` from every worksheet. - Insert the newly routed rows, then stably sort each sheet by `ARRIVAL` ascending. - Preserve original daily/XML order within the same date. - Preserve all other dates and their within-day order. - Existing sheets retain their formatting. New company sheets copy the standard channel-page header and style. - In every monthly row, write static `REAL PRICE = daily REAL PRICE` and static `TOTAL PRICE = daily TOTAL PRICE`. Independently enforce `TOTAL PRICE = REAL PRICE * NO_OF_ROOMS * NIGHTS` in both reports. - Only `DY-AI-Easy-KB` has `KB(100/晚/间)`; write it as static `NO_OF_ROOMS * 100`. Do not multiply KB by `NIGHTS`, and do not include KB in `TOTAL PRICE`. ## Output types and formatting - Use the daily and monthly templates independently; never copy columns by position between them. - Headers are in row 1 and data starts in row 2. - Store `ARRIVAL` and `DEPARTURE` as real Excel dates with display format `DD-MMM-YY`. - Store adults, children, room count, nights, rates, real prices, totals, and KB as numbers. - Store confirmation numbers, room numbers, codes, names, comments, and other identifiers as text. - Write `NIGHTS`, `REAL PRICE`, `TOTAL PRICE`, and `KB(100/晚/间)` as static values, never formulas. - Require `ADULTS >= 0`, `CHILDREN >= 0`, `NO_OF_ROOMS > 0`, `EFFECTIVE_RATE_AMOUNT >= 0`, and `DEPARTURE >= ARRIVAL` on every whitelist candidate. `DEPARTURE = ARRIVAL` produces `NIGHTS = 0`; `DEPARTURE < ARRIVAL` is invalid. - Preserve complete comment/trace text and leave allowed empty values blank. - Do not add filters, freezes, colors, or decorations absent from the templates.