Files
wyndham-ARR/opera-daily-channel-report/references/business-rules.md
2026-07-29 16:38:05 +08:00

96 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `KB100/晚/间)`; 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 `KB100/晚/间)` 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.