# Structured Finance Output Contract

## Purpose and compatibility

Each invocation has two JSON contracts with different consumers:

- In legacy `daily-monthly` mode, `result.json` and `structured-result.json` use version `1.0`.
- In database-ingestion `daily` mode, both contracts use version `2.0`, require `processing_mode: "daily"`, and require the monthly output/artifact to be null.

Do not add database fields to `result.json` or derive database rows by reopening generated workbooks. The processor creates every requested workbook and every structured record from the same in-memory classified records. The structured file intentionally does not hash itself; it hashes `result.json` and the other requested artifacts, avoiding an impossible self-hash recursion.

## Batch contract

The payload records:

- `business_date`, `processor_version`, `rule_set_sha256`, and `result_schema_version`;
- the four database counters and a five-outcome reconciliation;
- deterministic channel/worksheet counts;
- absolute local artifact paths, SHA-256 values, sizes, MIME types, and database-compatible file kinds;
- every XML source reservation in original order;
- structured errors on failed runs.

`rule_set_sha256` is a deterministic manifest digest of the processor, independent validator, Skill contracts, fixed price table, and both workbook templates. `processor_version` changes when processor behavior or its payload contract changes.

`activation_eligible: true` means only that the file pipeline and independent validations passed. It is not proof that a database transaction committed, and it never switches `finance.current_daily_versions` by itself.

## Per-source-record contract

Every `/G_RESERVATION` produces exactly one record with a continuous one-based `source_sequence` and `source_location = reservation[N]`.

Direct Opera XML has no source Excel coordinate:

- `source_worksheet = null`
- `source_row_no = null`

The deterministic channel destination is `channel_key`, even when daily mode does not create a monthly workbook. `source_worksheet/source_row_no` are reserved for an explicitly approved workbook-backfill adapter and must not be fabricated for direct XML.

The only outcomes are:

- `retained`
- `excluded_rate_code`
- `duplicate`
- `validation_failed`
- `price_unmatched`

Duplicate records point to the earlier retained candidate through `duplicate_of_source_sequence`. `decision_codes` preserve deterministic reasons such as whitelist exclusion, duplicate selection, exact price matching, the zero-price exception, or a validation error.

Derived keys are:

```text
normalized_rate_code = upper(trim(rate_code))
group_code_key       = upper(trim(res_comment))
company_key          = deterministic company keyword normalization
```

An empty trimmed `RES_COMMENT` produces JSON `null` for `group_code_key`. Never substitute `BLOCK_CODE`, confirmation number, or another value. Non-empty Group Codes start with `booking_source_match_status = not_checked`; empty values use `missing_group_code`.

The database adapter performs the only approved booking-source lookup:

```text
structured record group_code_key
  = booking.booking_source_rows.group_code_key

booking_type_of_room_raw
  = booking.booking_source_rows.hotel_raw
```

This is an exact normalized lookup, not fuzzy matching. `room_category_label` remains the Opera/XML actual room category and must never be overwritten with `hotel_raw`.

## Price and worksheet mapping

- XML/daily `EFFECTIVE_RATE_AMOUNT` becomes structured `effective_rate_amount` and monthly `RATE_AMOUNT`.
- `REAL PRICE` becomes `real_price`.
- `TOTAL PRICE` becomes `total_price` and always equals `real_price * no_of_rooms * nights` for retained rows.
- `nights` may be zero when arrival and departure are the same date; the corresponding `total_price` is numeric zero. Negative nights are invalid.
- `KB（100/晚/间）` becomes `kb_amount`; it is present only for `DY-AI-Easy-KB` and equals `no_of_rooms * 100`.
- The actual monthly worksheet becomes `channel_key`; direct XML still keeps `source_worksheet = null`.
- `pricing_method` is `price_reference_exact` or `zero_price_exception` for retained rows.

## Failure and version-switch boundary

Success requires XML parsing, filtering, deduplication, pricing, deterministic channel assignment, every requested workbook write, independent output validation, and structured completeness validation. Daily v2 validates only the daily workbook and requires no monthly artifact; legacy v1 additionally requires and validates the monthly workbook. A successful payload has no `validation_failed` or `price_unmatched` rows and satisfies:

```text
source_rows = removed_by_rate_code + removed_as_duplicates + output_rows
```

On failure, `activation_eligible` is false, `output_rows` is zero, daily/monthly artifacts are null, and the exception/result artifacts remain available. Valid candidates that never reached a fully validated formal result are marked `validation_failed` with `BATCH_NOT_VALIDATED`; already excluded, duplicate, or price-unmatched conclusions remain explicit.

The future database adapter must:

1. confirm `current_database() = 'booking_test'` before any write;
2. re-hash every file and validate this payload;
3. allocate a new immutable version for every accepted same-day rerun;
4. write file objects, the daily version, all records, channel metrics, and exact Group Code lookup results in one transaction;
5. activate the new daily version and supersede the prior one only at the end of that successful transaction;
6. roll back fully on any error, leaving the previous current version unchanged.

This Skill does not execute database DDL or DML. Its Finance payload targets the current `booking_test` contract after migrations `002`-`004`, while the file processor itself remains runnable without a live database connection.
