# Business Rules

## Durable Rules

- Human access to the desktop Finance workspace, detailed health, generic business APIs, uploads, traces and downloads
  requires an authenticated ARR Web session. The H5 mobile dashboard is an intentional anonymous read-only exception:
  only its page/assets, `/api/public/h5/months`, `/api/public/h5/analytics` and no-detail `/healthz` are public; the H5
  projection contains aggregate channel/room metrics and omits source hashes and operational metadata.
- The single-operator login identity is injected as `ARR_WEB_USERNAME` / `ARR_WEB_PASSWORD`; real credentials are never stored in source or returned/logged. Missing credentials fail Web startup closed.
- Login failures use a generic message and bounded attempt blocking. Logout revokes the server-side session; authenticated mutations continue to require CSRF.
- The user uploads XML; the user does not submit a report year, report month, cutoff date, or separate monthly-generation request in the primary workflow.
- Daily history and task trace show the validated browser-uploaded XML basename. Canonical internal source name
  `source.xml` is not user-facing; historical rows with no recorded basename display an unknown marker rather than a
  guessed filename.
- Daily, monthly-publication and company-generation histories have independent viewing months and default to the most
  recent month containing that record type. Viewing-month state is read-only: it resets list paging but never changes
  monthly derivation or a company generation request. Company `生成月份` and history `查看月份` are separate inputs.
- ARR's frozen processor generates complete structured data, which must be independently validated and successfully committed to the database before downstream monthly processing starts.
- A non-empty set of errors made entirely of `PRICE_UNMATCHED` is the sole exception to immediate failure: it becomes a pending manual-price review and remains active without a Finance version, daily download, failure event or monthly trigger.
- The review key is normalized company + Rate Code + Opera price. Staff may set only its non-negative integer price
  (including explicit `0`); they may not change XML/company/rate/Opera fields or the fixed reference table. ARR stores
  the accepted integer as exact `numeric(18,2)` and freezes canonical `.00` manifest text for deterministic replay.
- Finalization freezes one canonical manifest bound to job, case, source SHA, business date, processor/rule identity and the complete key set, then replays original XML plus the fixed table. Only independently accepted final replay creates `manual_review` retained facts and one daily-commit event.
- If the processor/rule identity changes while a case is open, staff must cancel and re-upload. Retryable infrastructure errors preserve the frozen manifest; deterministic final errors formally fail the case/run.
- The production XML path must not require Agent, MCP, prompts, model-generated JSON, fetch grants, or a public source URL.
- A successful database commit automatically triggers the monthly-report processing program through the business system.
- Automatic monthly processing runs in a dedicated worker process, not inside the Web request process.
- The worker derives the affected month from retained `ARRIVAL` facts belonging to the committed daily version.
- The monthly `as_of_date`/“更新至” is the greatest `ARRIVAL` actually included in the current monthly snapshot. The XML filename and wall-clock date are never authorities for it.
- Year, month, cutoff/as-of date, and similar fields are internal derived execution/audit values. Their presence in code or metadata does not make them user inputs.
- The monthly output contains the exact business dimension/field `TOTAL PRICE`.
- Each monthly XLSX data-row `TOTAL PRICE` cell is a formula whose business meaning is `REAL PRICE × NIGHTS × NO_OF_ROOMS` (current layout example: `=R2*C2*G2`).
- A stored database `total_price` may be used for integrity checking, but it does not replace the required formula in the exported workbook.
- Monthly business/guest rows remain derived from current Finance facts. PostgreSQL persists only publication identity, lineage, channel counts and artifact metadata.
- The outbox event is marked published only after a report is active/superseded and both workbook/result artifacts are registered; replay of the same snapshot must not create a duplicate publication.
- A published monthly version automatically appears in the open monthly list; the primary workflow has no manual list-refresh step.
- Company-channel detail generation is allowed for any period in the selected current month and for historical months. A
  future report month is rejected so the system does not create an intentionally empty future workbook.
- Company-channel C/O periods keep fixed report cutoffs: `01-10` uses the 10th, `11-20` uses the 20th and
  `21-month-end` uses the selected month's natural month-end. The workbook uses the current committed Finance snapshot
  through that cutoff even when the calendar period has not ended; later-arriving facts require a rerun to be included.
- The company-channel UI labels calendar status as `周期未结束` or `周期已结束`. These labels describe whether the
  selected period has reached its Bangkok completion boundary; they are not generation permission states.
- In a company-channel detail workbook, a Finance fact with no normalized Group Code remains visible as its own row;
  `RES_COMMENT` and `Booking Room` are blank, and this expected blank does not create a warning or duplicate highlight.
- A Finance stay segment with a consistent nonblank Group Code keeps that Group Code and its normal aggregation. When
  no current accepted Booking room can be resolved, only `Booking Room` is blank; the lookup miss does not create an
  error or warning and does not block the company's workbook.
- The company-report Booking allocation model consists of normalized Group Code, room type and quantity. A raw intake
  adapter may derive these fields from a source workbook, but raw workbook columns and normalized result dimensions are
  separate contracts; worksheet, physical row, artifact hash and parse version remain provenance.
- A raw Booking workbook is recognized only on a worksheet containing exactly one approved Tour Code/Group Code header
  and exactly one approved hotel-detail header on the same row. Approved Tour Code labels are `Tour Code`, `Group Code`,
  `Gourp Code`, `团号`, `团队代码` and `Tour Code / 主团号`; approved hotel-detail labels are `โรงแรม`,
  `วางข้อมูลที่นี่ / 酒店明细` and `Raw Hotel Detail / วางข้อมูลที่นี่ / 酒店明细`. Header cells are
  Unicode/case/whitespace/punctuation-normalized before whole-cell exact comparison; substring, fuzzy, filename,
  month and worksheet-name matching are forbidden. Tour Codes are Unicode-normalized and all whitespace is removed
  before matching.
- For repeated Tour Codes, the physically last source row replaces earlier rows. A final row with an explicit textual
  cancellation removes the Tour Code; cell color or other formatting alone never means cancellation.
- Room extraction reads parenthesized hotel segments. A `【label】` token takes the following integer as quantity and
  defaults to one when no following integer exists. Multiple tokens become separate items even when they normalize to
  the same room type.
- Numeric suffix variants of `U-TWN` and `U-DBL` normalize to `U-TWN` and `U-DBL`; `高级房TWN`/`高级房DBL` normalize
  to `TWN`/`DBL`. Unknown bracket labels retain their raw text and parsed/default quantity as pending. Recognized
  unbracketed names such as `6+4`, `Family Suite`, `Junior Suite` and `Superior Garden` also remain pending because the
  source omitted brackets. Surcharge, child-breakfast and guide-room tokens are not guest-room allocations.
- Pending items do not contribute accepted Booking room quantity. Automatic and manual rows are both editable and
  deletable; activation requires zero pending items and at least one confirmed item. Re-extraction rebuilds the review
  draft from source and intentionally discards draft-only edits/deletions.
- The review UI pages at 50 records. A delete request contains 1-50 unique item IDs and is all-or-nothing: every item
  must still be undeleted, belong to the same reviewing draft and be deletable, otherwise the transaction rolls back.
- An open Booking review displays the draft artifact's validated browser-uploaded filename directly below
  `Booking记录提取`. Accepted historical-source readiness is not shown as provenance for draft extraction records.
- Once accepted, a manually uploaded Booking workbook replaces the complete current source rather than appending
  silently to the previous workbook. Migrations 014/015 and the HTTP GET/POST/PATCH/DELETE adapter represent this
  boundary; the first real draft activation remains a controlled business acceptance action.
- Booking normalizes the complete Group Code and globally aggregates repeated `(Group Code, room type)` rows by summed
  quantity. Worksheet, company/channel and stay dates are not current Booking join dimensions. This contract therefore
  requires one Group Code to identify one room allocation across those scopes; a different business reality requires a
  schema/view/report-join change, not merely extra unused Excel columns.
- Contradictory Group Code source fields and invalid Finance dates, nights or price details remain strict company-report
  errors; the blank fallback applies only to absent or unresolved Booking enrichment.

## Last Reviewed

2026-08-11
