# Own offer versus Featured Offer: offline comparison model

This small standard-library Python model compares **invented, normalized records**.
It performs no network requests and is not an Amazon response parser. It answers
two separate questions: whether the observations are comparable, and whether the
identified Featured Offer seller is the declared own seller.

Run from this directory with Python 3.11+:

```sh
python3 -B compare.py
python3 -B -m unittest -v test_compare.py
python3 -B compare.py --csv
```

The first and third commands reproduce `example-output.json` and
`example-matrix.csv`. The CSV is an editable worksheet; to change and rerun the
model, edit a copy of `cases.json`, then pass `--cases your-cases.json`. CSV is an
output format, not an accepted input format. No package installation is needed.
Exact executed versions, commands, timestamps and hashes are in
`evidence/metadata.json`; Python 3.11 is a source target, not a claim of testing
that runtime.

## What the worked cases establish

The fourteen cases yield three comparisons and eleven holds under the default
policy. A hold has no calculated subtotal or gap (`null` in JSON, empty in CSV).
Invalid input instead fails the command with a nonzero exit.

| Synthetic case | Own subtotal | Featured subtotal | Own minus featured | Seller result |
| --- | ---: | ---: | ---: | --- |
| $19 item + $4 shipping versus $21 item + known $0 shipping | $23.00 | $21.00 | $2.00 | Different seller |
| $20 + known $0 versus $20 + known $0 | $20.00 | $20.00 | $0.00 | Different seller |
| Same amounts, Featured Offer seller explicitly matches own seller | $20.00 | $20.00 | $0.00 | Same seller |

The remaining cases hold for missing shipping, membership/location/currency
mismatches, a wrong own seller, failed or unavailable observations, substituting
an external reference/FOEP/lowest-price record, and excessive observation-time
skew. Equal prices do not identify a seller, and a lower item price need not mean
a lower subtotal. These are model results, not measurements of Amazon behavior.

## Normalized input contract

`cases.json` is the complete runnable example. Its exact top-level fields are
`schema_version: 1` and `cases`, a list of 1–100 uniquely identified cases.
Each case has exactly `id`, `description`, `expected_own_seller_id`, `own` and
`featured`. The declared own seller is separate from both observations.

An unavailable observation is `null`, `{"status":"absent"}`, or
`{"status":"failed"}`. An observed record requires exactly these fields:

| Field | Normalized meaning |
| --- | --- |
| `status` | `observed`. |
| `source_type` | `own_offer` on the own side, `featured_offer` on the Featured side. `external_reference`, `foep` and `lowest_price` are recognized but cause a hold in either role. |
| `seller_id` | Seller identified by the observation. A different Featured Offer seller is allowed; a wrong own-seller ID is held. |
| `asin` | Product identity already mapped and validated by the input adapter. |
| `marketplace`, `currency`, `condition`, `customer_type` | Must match between records and equal this model's `US`, `USD`, `New`, `Consumer` scope. `US` is a model label, not a raw Amazon marketplace ID. |
| `membership`, `location_context`, `shipping_service` | Explicit selected comparison context; exact string equality is required. |
| `observed_at` | Valid UTC timestamp in `YYYY-MM-DDTHH:MM:SSZ` form. |
| `item_price`, `shipping_price` | Known nonnegative money strings, or `null` for unknown. |

All observed values except `status`/`source_type` may be explicitly `null`, which
causes a hold. Omitted or unknown fields are schema errors. Identity/context text
must have 1–256 characters without surrounding whitespace or ASCII controls below
U+0020. Money accepts up to twelve integer digits and up to two decimal digits:
`"0"`, `"0.00"` and `"19.50"` are valid; negative values, floats, exponent notation,
non-finite values and extra decimal places are rejected. There is no silent
rounding. Duplicate JSON members and duplicate case IDs are rejected; the cases
file is limited to 262,144 UTF-8 bytes. These are lab bounds, not provider limits.

**Single-unit pricing is a fixed input assumption: there is no quantity field.**
The adapter must provide prices for one unit. Matching membership, location and
service strings are trusted context labels, not provider validation. Unknown
context must be `null`, not a fabricated shared label such as `unknown`. The
fixture's location labels describe representative locations, not exact checkout
quotes. This model does not establish the truth of seller, product or source-type
labels either: the adapter must validate real response success, selected datasets,
scope and component provenance before supplying them.

Do not assume `getListingsItem` alone supplies shipping, membership or location
context. Normalization requires suitable separately evidenced inputs for those
fields. A batch envelope's success is not proof that each requested observation
succeeded. An expected-price output, external retailer reference or lowest-price
record is not an observed Featured Offer.

## Arithmetic and timing boundaries

The only price arithmetic is:

```text
(own item + own known shipping) - (featured item + featured known shipping)
```

Python `Decimal` uses a local precision sufficient for all permitted input amounts.
Positive means the own subtotal is higher; negative means it is lower. This is a
**pre-promotion item-plus-known-shipping subtotal difference**. It excludes checkout
tax, coupon eligibility, Amazon Points, complete promotion coverage and fulfillment
quality. It is not a checkout total or a prediction that the lowest subtotal wins.

The default **local policy** accepts an absolute observation-time skew of at most
300 seconds, inclusive. Configure it explicitly, for example:

```sh
python3 -B compare.py --max-skew-seconds 360
```

That command makes the `time_skew` case comparable, producing four comparisons
instead of three. The allowed policy range is 0–86,400 seconds; it is a lab control,
not an Amazon recommendation. The model checks **neither observation age nor
upstream freshness**. Two equally old records can pass, as a test demonstrates.
Observation times do not establish synchronized source revisions or customer
checkout eligibility. Choose and verify a suitable evidence window separately.

The fourteen tests cover the stated arithmetic, exact cents and large bounded
amounts, positive/negative gaps, zero versus missing shipping, seller identity,
each material context/source/status hold, unsupported matching scope, strict input
rejection, timing boundaries and CLI JSON/CSV consistency. No provider account,
raw API mapping, market-wide result or freshness guarantee was tested.
