{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://cost.nusho.ge/schema.json",
  "title": "Nusho Cost import (nusho-cost/1)",
  "description": "Ingredients, preps, dishes, and settings for the Nusho Cost dish-costing app. Every top-level section is optional. Money is in GEL (₾). Units are g, ml, pcs only. Instructions: https://cost.nusho.ge/import-guide.md",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "format": { "const": "nusho-cost/1" },
    "ingredient_categories": { "type": "array", "items": { "$ref": "#/$defs/category" } },
    "dish_categories": { "type": "array", "items": { "$ref": "#/$defs/category" } },
    "settings": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "target_pct": { "type": "number", "exclusiveMinimum": 0, "maximum": 100, "description": "Food-cost target, % of the net menu price. Default 30." },
        "band_pp": { "type": "number", "minimum": 0, "description": "Percentage points above the target that still count as 'near'. Default 3." },
        "vat_on": { "type": "boolean", "description": "Menu prices include VAT. Default false." },
        "vat_rate_pct": { "type": "number", "minimum": 0, "description": "Default 18." },
        "round_step": { "enum": [0.1, 0.5, 1, 0], "description": "Suggested price rounds up to this step in GEL; 0 = no rounding. Default 0.5." }
      }
    },
    "ingredients": { "type": "array", "items": { "$ref": "#/$defs/ingredient" } },
    "preps": { "type": "array", "items": { "$ref": "#/$defs/prep" } },
    "dishes": { "type": "array", "items": { "$ref": "#/$defs/dish" } }
  },
  "$defs": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9_-]*$",
      "description": "Lowercase latin letters, digits, - and _. Reuse the id of an item already in the app to update it. Ingredients and preps share one id space."
    },
    "name": {
      "type": "object",
      "additionalProperties": false,
      "properties": { "ru": { "type": "string" }, "en": { "type": "string" } },
      "anyOf": [{ "required": ["ru"] }, { "required": ["en"] }]
    },
    "category": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id"],
      "properties": { "id": { "$ref": "#/$defs/id" }, "ru": { "type": "string" }, "en": { "type": "string" } },
      "anyOf": [{ "required": ["ru"] }, { "required": ["en"] }]
    },
    "ingredient": {
      "type": "object",
      "description": "Something bought. pack_size + pack_unit + pack_price describe one purchase: 1 kg for 28 GEL is 1000, \"g\", 28. A new ingredient needs id, name, category, pack_size, pack_unit. One already in the app needs only id plus the fields that change; a left-out field keeps its value, null clears it.",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": { "$ref": "#/$defs/id" },
        "name": { "$ref": "#/$defs/name" },
        "category": { "$ref": "#/$defs/id", "description": "An ingredient category id: built in (meat, dairy, produce, dry, bakery, oils), already in the app, or in ingredient_categories." },
        "pack_size": { "type": "number", "exclusiveMinimum": 0 },
        "pack_unit": { "enum": ["g", "ml", "pcs"] },
        "pack_price": { "type": ["number", "null"], "minimum": 0, "description": "GEL as paid. null = price unknown; never guess." },
        "yield_pct": { "type": ["number", "null"], "minimum": 1, "maximum": 100, "description": "Usable share after trimming. null = 100." },
        "piece_weight_g": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "Weight of one piece as bought, before trimming, thawing, or cooking. Needed to mix pieces and grams for this ingredient." },
        "density_g_per_ml": { "type": ["number", "null"], "exclusiveMinimum": 0, "description": "null = 1 g/ml is assumed." },
        "price_history": {
          "type": "array",
          "description": "Optional. Without it, the app adds today's price to the history.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["date", "pack_price"],
            "properties": { "date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, "pack_price": { "type": "number", "minimum": 0 } }
          }
        }
      }
    },
    "line": {
      "type": "object",
      "description": "One row of a recipe: the trimmed, usable amount of an ingredient or prep.",
      "additionalProperties": false,
      "required": ["ref", "qty", "unit"],
      "properties": {
        "ref": { "$ref": "#/$defs/id", "description": "An ingredient or prep id, in this file or already in the app." },
        "qty": { "type": ["number", "null"], "minimum": 0, "description": "null = quantity unknown." },
        "unit": { "enum": ["g", "ml", "pcs"], "description": "pcs is not allowed for preps." },
        "yield_override_pct": { "type": ["number", "null"], "minimum": 1, "maximum": 100, "description": "Only for ingredient lines in g or ml." }
      }
    },
    "prep": {
      "type": "object",
      "description": "Made in-house in batches (sauce, dough, stock) and used in dishes or other preps. A prep must not contain itself. A new prep needs id, name, output, output_unit, lines. One already in the app needs only id plus the fields that change; lines replace the whole recipe.",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": { "$ref": "#/$defs/id" },
        "name": { "$ref": "#/$defs/name" },
        "output": { "type": "number", "exclusiveMinimum": 0, "description": "Finished amount of one batch." },
        "output_unit": { "enum": ["g", "ml"] },
        "lines": { "type": "array", "items": { "$ref": "#/$defs/line" } }
      }
    },
    "dish": {
      "type": "object",
      "description": "A menu item. A new dish needs id, name, category, variants. One already in the app needs only id plus the fields that change; variants match existing ones by name, and a variant left out stays.",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": { "$ref": "#/$defs/id" },
        "name": { "$ref": "#/$defs/name" },
        "category": { "$ref": "#/$defs/id", "description": "A dish category id: built in (breakfast, mains, desserts), already in the app, or in dish_categories." },
        "target_pct": { "type": ["number", "null"], "exclusiveMinimum": 0, "maximum": 100, "description": "Food-cost target for this dish. null = the global target." },
        "menu_variant": { "type": "integer", "minimum": 0, "description": "0-based index of the variant on the menu. Default 0." },
        "variants": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name"],
            "description": "A new variant needs lines; an existing one (matched by name) may give only the fields that change.",
            "properties": {
              "name": { "$ref": "#/$defs/name" },
              "menu_price": { "type": ["number", "null"], "minimum": 0, "description": "GEL, as printed on the menu." },
              "lines": { "type": "array", "items": { "$ref": "#/$defs/line" } }
            }
          }
        }
      }
    }
  }
}
