96 lines
7.8 KiB
Markdown
96 lines
7.8 KiB
Markdown
# 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.
|