{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://chukoudan.com/schemas/quote/v1.json",
  "title": "Quote (chukoudan.com, v1)",
  "description": "One seller’s quote to one buyer: prices and terms only, never the seller’s costs. Each field’s description names its counterpart in OASIS UBL 2.4 Quotation. `digest` is sha256 of the canonical JSON (UTF-8, keys sorted, no spaces) of the document without `digest`; `basis.rate` names the dated exchange rate the prices rest on, which a buyer can check at https://chukoudan.com/v1/datasets/central-parity.",
  "type": "object",
  "properties": {
    "schema": {
      "const": "https://chukoudan.com/schemas/quote/v1.json",
      "description": "This schema’s address, which names its version."
    },
    "number": {
      "type": "string",
      "description": "The seller’s quote number. UBL: cbc:ID."
    },
    "issued": {
      "type": "string",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
      "description": "The day the quote is issued, YYYY-MM-DD. UBL: cbc:IssueDate."
    },
    "valid_until": {
      "type": "string",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
      "description": "The last day the quote holds. UBL: cac:ValidityPeriod/cbc:EndDate."
    },
    "seller": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        "country": {
          "type": "string",
          "pattern": "^[A-Z]{2}$"
        },
        "contact": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "required": ["name", "country"],
      "additionalProperties": false,
      "description": "UBL: cac:SellerSupplierParty. country: ISO 3166-1 alpha-2."
    },
    "buyer": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        "country": {
          "type": "string",
          "pattern": "^[A-Z]{2}$"
        },
        "contact": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "required": ["name", "country"],
      "additionalProperties": false,
      "description": "UBL: cac:OriginatorCustomerParty. country: ISO 3166-1 alpha-2."
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "UBL: cac:Item/cbc:Description."
          },
          "hs_code": {
            "type": "string",
            "description": "The seller’s HS code, digits only. UBL: cac:Item/cac:CommodityClassification/cbc:ItemClassificationCode."
          },
          "origin": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "Country of origin, ISO 3166-1 alpha-2. UBL: cac:Item/cac:OriginCountry."
          },
          "quantity": {
            "type": "string",
            "pattern": "^(0|[1-9]\\d*)(\\.\\d+)?$",
            "description": "A decimal string. UBL: cac:LineItem/cbc:Quantity."
          },
          "unit": {
            "enum": ["PCE", "SET", "CT", "KGM"],
            "type": "string",
            "description": "UN/ECE Recommendation 20 code: the quantity’s unitCode in UBL."
          },
          "packing": {
            "type": "string",
            "description": "How the goods are packed, as the seller says it."
          }
        },
        "required": ["description", "origin", "quantity", "unit"],
        "additionalProperties": false
      },
      "minItems": 1,
      "maxItems": 1,
      "description": "The goods. One line in v1. UBL: cac:QuotationLine/cac:LineItem."
    },
    "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$",
      "description": "ISO 4217 code of every price. UBL: cbc:PricingCurrencyCode."
    },
    "prices": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "term": {
            "enum": ["FOB", "CFR", "CIF"],
            "type": "string",
            "description": "Incoterms 2020 term. UBL: cac:Delivery/cac:DeliveryTerms/cbc:ID."
          },
          "place": {
            "type": "string",
            "description": "The named place: port of loading for FOB, port of destination for CFR and CIF."
          },
          "unit_price": {
            "type": "string",
            "pattern": "^(0|[1-9]\\d*)(\\.\\d+)?$",
            "description": "A decimal string in `currency`. UBL: cac:Price/cbc:PriceAmount."
          },
          "total": {
            "type": "string",
            "pattern": "^(0|[1-9]\\d*)(\\.\\d+)?$",
            "description": "unit_price × quantity, a decimal string in `currency`. UBL: cbc:LineExtensionAmount."
          },
          "commission_included": {
            "type": "boolean",
            "description": "Whether the price includes the buying agent’s commission (含佣)."
          }
        },
        "required": ["term", "place", "unit_price", "total", "commission_included"],
        "additionalProperties": false
      },
      "minItems": 1,
      "description": "One price per term offered. UBL quotes one set of delivery terms per document; this lists several side by side."
    },
    "payment": {
      "type": "string",
      "description": "Payment terms as the seller writes them (T/T, L/C at sight…). UBL: cac:PaymentTerms/cbc:Note."
    },
    "lead_time_days": {
      "type": "integer",
      "description": "Days from order to shipment, a whole number."
    },
    "notes": {
      "type": "string",
      "description": "UBL: cbc:Note."
    },
    "basis": {
      "type": "object",
      "properties": {
        "rate": {
          "type": "object",
          "properties": {
            "base": {
              "type": "string",
              "pattern": "^[A-Z]{3}$"
            },
            "quote": {
              "const": "CNY"
            },
            "value": {
              "type": "string",
              "pattern": "^(0|[1-9]\\d*)(\\.\\d+)?$"
            },
            "per": {
              "enum": ["1", "100"],
              "type": "string"
            },
            "source": {
              "enum": ["central-parity", "given"],
              "type": "string",
              "description": "`central-parity`: the RMB central parity of `day`, from https://chukoudan.com/v1/datasets/central-parity; `given`: the seller’s own rate."
            },
            "day": {
              "type": "string",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
            }
          },
          "required": ["base", "quote", "value", "per", "source"],
          "additionalProperties": false
        },
        "made_with": {
          "const": "https://chukoudan.com"
        }
      },
      "required": ["made_with"],
      "additionalProperties": false,
      "description": "The dated exchange rate the prices rest on, when the seller includes it, so a buyer can check it."
    },
    "digest": {
      "type": "string",
      "pattern": "^sha256:[0-9a-f]{64}$",
      "description": "`sha256:` and the hex SHA-256 of this document’s canonical JSON without `digest`: UTF-8, keys sorted, no spaces, non-ASCII characters written as themselves (Python: json.dumps(doc, sort_keys=True, separators=(\",\", \":\"), ensure_ascii=False))."
    }
  },
  "required": [
    "schema",
    "issued",
    "valid_until",
    "seller",
    "buyer",
    "items",
    "currency",
    "prices",
    "basis"
  ],
  "additionalProperties": false
}
