{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "recon_rule_set",
  "title": "Recon Rule Set",
  "description": "A tenant's (or business group's) complete reconciliation definition: an ordered list of match stages interpreted by the generic engine. Stored in recon_rule_sets.definition. Replaces the per-type matchers and the reference 'rule_engine' table rows.",
  "type": "object",
  "required": ["name", "stages"],
  "additionalProperties": false,
  "properties": {
    "name": { "type": "string", "pattern": "^[a-z0-9_]+$" },
    "display_name": { "type": "string", "minLength": 1, "maxLength": 200 },
    "description": { "type": "string" },
    "stages": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/definitions/stage" },
      "description": "Executed in order. A stage may consume a prior stage's matched/unmatched output (chaining → N-way recon)."
    },
    "report": {
      "type": "object",
      "additionalProperties": false,
      "description": "Report layout config — the only place presentation lives (D2).",
      "properties": {
        "location_summary": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "enabled": { "type": "boolean", "default": true },
            "group_by_field": {
              "type": "string",
              "default": "location",
              "description": "Canonical field to group by. UI/report labels it with the tenant's location label (Unit/Store/...)."
            }
          }
        },
        "include_tier_breakdown": {
          "type": "boolean",
          "default": true,
          "description": "Summary shows counts/amounts per match tier (exact vs fallback vs tolerant)."
        }
      }
    }
  },
  "definitions": {
    "source_ref": {
      "type": "string",
      "pattern": "^(template:[a-z0-9_]+|stage:[a-z0-9_]+:(matched|unmatched_a|unmatched_b))$",
      "description": "Either a source template ('template:hdfc_card_mpr' — resolved to the session file bound to that template) or a prior stage's output ('stage:<stage>:matched')."
    },
    "side": {
      "type": "object",
      "required": ["source"],
      "additionalProperties": false,
      "properties": {
        "source": { "$ref": "#/definitions/source_ref" },
        "filters": {
          "type": "array",
          "items": { "$ref": "#/definitions/filter" },
          "description": "Row filters applied before matching (replaces reference 'payment_filter' / 'pg_transaction_type' SQL fragments)."
        }
      }
    },
    "filter": {
      "type": "object",
      "required": ["field", "op"],
      "additionalProperties": false,
      "properties": {
        "field": { "type": "string", "description": "Canonical field name" },
        "op": { "enum": ["eq", "ne", "in", "not_in", "contains", "not_contains", "not_null", "is_null"] },
        "value": { "type": ["string", "number"] },
        "values": { "type": "array", "items": { "type": ["string", "number"] } }
      }
    },
    "tier": {
      "type": "object",
      "required": ["name", "keys"],
      "additionalProperties": false,
      "description": "One pass of the match cascade. Rows matched in a tier are consumed (one-to-one, D5) and excluded from later tiers.",
      "properties": {
        "name": { "type": "string", "pattern": "^[a-z0-9_-]+$" },
        "enabled": { "type": "boolean", "default": true },
        "keys": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "array",
            "items": { "type": "string" },
            "minItems": 2,
            "maxItems": 2,
            "description": "[side_a_field, side_b_field] — canonical names; equality after normalization."
          }
        },
        "amount_tolerance": {
          "type": "object",
          "required": ["type", "value", "fields"],
          "additionalProperties": false,
          "properties": {
            "type": { "enum": ["abs", "pct"] },
            "value": { "type": "number", "exclusiveMinimum": 0 },
            "fields": {
              "type": "array",
              "items": { "type": "string" },
              "minItems": 2,
              "maxItems": 2,
              "description": "[side_a_amount_field, side_b_amount_field]"
            }
          }
        },
        "date_window": {
          "type": "object",
          "required": ["days", "fields"],
          "additionalProperties": false,
          "description": "Settlement lag: |date_a - date_b| <= days (T+n).",
          "properties": {
            "days": { "type": "integer", "minimum": 0 },
            "fields": {
              "type": "array",
              "items": { "type": "string" },
              "minItems": 2,
              "maxItems": 2
            }
          }
        }
      }
    },
    "stage": {
      "type": "object",
      "required": ["stage", "side_a", "side_b", "tiers", "output"],
      "additionalProperties": false,
      "properties": {
        "stage": {
          "type": "string",
          "pattern": "^[a-z0-9_]+$",
          "description": "Unique slug within the rule set; referenced by later stages' source_refs."
        },
        "display_name": { "type": "string" },
        "enabled": { "type": "boolean", "default": true },
        "side_a": {
          "$ref": "#/definitions/side",
          "description": "Driving side: every side_a row lands in matched or unmatched output."
        },
        "side_b": { "$ref": "#/definitions/side" },
        "match_mode": {
          "enum": ["one_to_one", "many_to_one"],
          "default": "one_to_one",
          "description": "one_to_one: each side-A row pairs with at most one side-B row. many_to_one (R7): side-A rows are GROUPED by the tier keys and their amount sum must equal one side-B row's amount (batch settlements, e.g. one UPI settlement line covering N transactions); amount fields default to ['amount','amount'] or come from the tier's amount_tolerance.fields; without a tolerance the sum must match to the paisa."
        },
        "tiers": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/definitions/tier" },
          "description": "Ordered cascade, strictest first (e.g. exact-4key → 3key → tolerant). Match records its tier name."
        },
        "output": {
          "type": "object",
          "required": ["matched_sheet", "unmatched_sheet"],
          "additionalProperties": false,
          "properties": {
            "matched_sheet": { "type": "string", "description": "Excel sheet name for matched rows. Null-able in future if a stage is intermediate-only." },
            "unmatched_sheet": { "type": "string", "description": "Sheet for unmatched side_a rows." },
            "unmatched_sheet_b": { "type": "string", "description": "Optional sheet for unmatched side_b rows (e.g. gateway rows with no bank settlement)." },
            "carry_fields": {
              "type": "array",
              "items": { "type": "string" },
              "description": "side_b canonical fields copied onto matched side_a rows (available to later chained stages and the report), e.g. ['transaction_id', 'email']."
            }
          }
        }
      }
    }
  }
}
