# description
The contract for one row of the phase-2 fusion MVP, plus the file-level wrapper. Every column has a defined type, range, and cross-field constraint. The point of this schema is to forbid everything we don't want — so a bad fusion run produces a validation error rather than a silently-corrupted KPI. Mirrors the SQLite schema sketched in docs/FUSION_PLAN.md §4.

VOCABULARY: see docs/CONCEPTS.md. PRIMITIVES: pulled via $ref from _common.schema.json.

AUTHORITATIVE-SOURCE BOUNDARY: per FUSION_PLAN §3a-bis, sales/orders columns come from STORIS; traffic/closes columns come from Trakwell. Fields are named *_raw for per-store STORIS facts; location_* for per-physical-location aggregates that drive the headline KPIs.

# $defs.FusionDailyRow.title
Single (store_id, date) row of fusion_daily

# $defs.FusionDailyRow.properties.physical_location.description
Null only for store_id=='777' (online — no physical door).

# $defs.FusionDailyRow.properties.transactions_raw.description
Per-store STORIS transactions after orderItemId-first commission-split consolidation but BEFORE customerId dedup. Matches Power BI / 'order count'. Performance-filtered (returns + exchanges + adjustments excluded).

# $defs.FusionDailyRow.properties.units.description
Per-store unit count from STORIS netUnits, post-orderItemId consolidation.

# $defs.FusionDailyRow.properties.net_sales.description
Per-store STORIS net sales. Performance-filtered ⇒ non-negative. If you ever need a payroll-style row that includes adjustments, that's a different schema and a different table.

# $defs.FusionDailyRow.properties.gross_profit.description
Per-store STORIS gross profit. CAN be negative (cost > sales) even when net_sales > 0.

# $defs.FusionDailyRow.properties.location_traffic.description
Trakwell unique-visitor count for the physical_location for this date. Same value repeated across co-located rows (003/004 share). Null when store_id=='777' OR Trakwell data missing/incomplete for that location/date. Source: v2 UserSummaryCountNew.total_door (NOT v3 countDetail event count — see EXTERNAL_RESEARCH_INSIGHTS §1.5).

# $defs.FusionDailyRow.properties.location_unique_closes.description
Distinct STORIS customerId across all stores within the physical_location for this date, after orderItemId consolidation. Same value repeated across co-located rows. Null when store_id=='777'.

# $defs.FusionDailyRow.properties.location_net_sales.description
Sum of net_sales across stores within the physical_location. Same value repeated across co-located rows. Null when store_id=='777'.

# $defs.FusionDailyRow.properties.location_conversion.description
location_unique_closes / location_traffic. Null when location_traffic is null or 0. Decimal not percent — 0.42 means 42%.

# $defs.FusionDailyRow.properties.location_sales_per_visitor.description
location_net_sales / location_traffic. Null when location_traffic is null or 0.

# $defs.FusionDailyRow.properties.location_gp_per_visitor.description
location_gross_profit / location_traffic. Can be negative if combined GP is negative. Null when location_traffic is null or 0.

# $defs.FusionDailyRow.properties.avg_ticket.description
Per-store: net_sales / transactions_raw. Null when transactions_raw == 0. Pure STORIS metric — never derived from Trakwell.

# $defs.FusionDailyRow.properties.ghost_attribution_net_sales.description
Per-store sum of net_sales attributed to inactive salesperson codes after their `last_known_active_at` boundary (per docs/salesperson_map.json). Indicates how much of net_sales is post-termination ghost-attribution. Null when no salesperson_map was loaded (couldn't check); 0 means we did check and found nothing. See GSHEETS_IDENTITY_RESOLVER.md v4 for the methodology and limitations (LOWER bound on true ghost volume — see caveat).

# $defs.FusionDailyRow.properties.ghost_attribution_pct.description
ghost_attribution_net_sales / net_sales. Null when net_sales is 0 OR salesperson_map wasn't loaded. Decimal not percent — 0.07 means 7%.

# $defs.FusionDailyRow.properties.storis_run.description
Pointer to salesLog/<dir> the STORIS facts were derived from.

# $defs.FusionDailyRow.properties.trakwell_run.description
Pointer to doorLog/<dir>. Null when no Trakwell data was used (e.g. store 777 row, or the entire Trakwell pull failed).

# $defs.FusionDailyRow.allOf[].title
Store 777 (online) has no physical_location and no traffic

# $defs.FusionDailyRow.allOf[].title
Stores 003 and 004 share the Decatur outlet location

# $defs.FusionDailyRow.allOf[].title
If location_traffic is null, all derived per-visitor KPIs must be null

# $defs.FusionDailyRow.allOf[].title
If transactions_raw is 0, avg_ticket must be null (no division by zero)

# $defs.FusionDailyFile.title
Array wrapper for a per-day fusion CSV/JSON output

# $defs.FusionDailyFile.description
A complete daily run produces one row per (store_id, date) for every store_id in the store map.

EXTRA-SCHEMA INVARIANTS to enforce in the loader (JSON Schema 2020-12 cannot express composite-key uniqueness natively):
  - (store_id, date) is unique across rows.
  - For any given date present in the file, every store_id with has_door_traffic in store_map must appear (or fail loud — never silent missing rows).
  - For co-located stores in the same physical_location, location_traffic / location_unique_closes / location_net_sales / location_conversion / location_sales_per_visitor / location_gp_per_visitor MUST be identical across the co-located rows.
  - location_unique_closes for the physical_location must be ≤ sum(transactions_raw) for the co-located stores (a customer can have many tickets but each ticket has at most one customer).