{
  "openapi": "3.1.0",
  "info": {
    "title": "Split Shared Ledger API",
    "version": "2.0.0",
    "description": "Public read/write by product design. No authentication required for REST. CORS is not authentication. Document writes require revision and are transactional; audit snapshots preserve prior versions."
  },
  "servers": [
    {
      "url": "https://oscar-shared-split.oscarge0319.chatgpt.site"
    },
    {
      "url": "http://terminal.local:4173",
      "description": "Local managed preview"
    }
  ],
  "paths": {
    "/api/books/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getBook",
        "summary": "Read a public shared ledger",
        "responses": {
          "200": {
            "description": "Current book and version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookResponse"
                }
              }
            }
          },
          "404": {
            "description": "Book not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Database unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "replaceBook",
        "summary": "Atomically replace a ledger using optimistic concurrency",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "book",
                  "revision"
                ],
                "properties": {
                  "book": {
                    "$ref": "#/components/schemas/Book"
                  },
                  "revision": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "clientVersion": {
                    "const": 2,
                    "description": "Required when a book uses FX, attachments or finance source metadata"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; revision increments by one",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale revision or incompatible client; refresh and preserve draft",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Database unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/books": {
      "post": {
        "operationId": "createBook",
        "summary": "Create a new public ledger",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "book"
                ],
                "properties": {
                  "book": {
                    "$ref": "#/components/schemas/Book"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "ID already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Check database connectivity",
        "responses": {
          "200": {
            "description": "Connected",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Database unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rent": {
      "get": {
        "operationId": "getRentStatus",
        "summary": "Read d6 rule, due months and balances without mutation",
        "responses": {
          "200": {
            "description": "Rule and status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Database unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "applyDueRent",
        "summary": "Idempotently insert due rent using server time, New York timezone",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "maxProperties": 0
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied or already up to date",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bookId": {
                      "type": "string"
                    },
                    "addedMonths": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "revision": {
                      "type": "integer"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "localDate": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid origin or content type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Unable to save",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "Stateless JSON-RPC: get_ledger_summary and apply_due_rent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "const": "2.0"
                  },
                  "id": {},
                  "method": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC result or error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "Notification acknowledged"
          },
          "400": {
            "description": "Invalid JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/books/{id}/attachments": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "uploadAttachment",
        "summary": "Upload a public file, idempotent by attachment ID and SHA-256",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "file"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored attachment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttachmentResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Missing book",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Attachment ID conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Size or quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported file signature",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Storage unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/books/{id}/attachments/{attachmentId}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "attachmentId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "download",
          "in": "query",
          "schema": {
            "const": "1"
          }
        }
      ],
      "get": {
        "operationId": "getAttachment",
        "summary": "Serve public image or download original file",
        "responses": {
          "200": {
            "description": "Binary file; safe MIME, nosniff, immutable cache"
          },
          "404": {
            "description": "Missing attachment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Storage unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rates": {
      "get": {
        "operationId": "getReferenceRate",
        "summary": "Get daily reference FX rate with one-hour cache and explicit failure fallback",
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reference quote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid currency/date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Manual rate required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Member": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "active": {
            "type": "boolean"
          }
        }
      },
      "Entry": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "date",
          "description",
          "amountCents",
          "payerId"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "enum": [
              "expense",
              "income",
              "transfer"
            ]
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "amountCents": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000000
          },
          "payerId": {
            "type": "string"
          },
          "payeeId": {
            "type": "string"
          },
          "shares": {
            "type": "object",
            "description": "Integer microcents: 1 cent = 1,000,000 units. Expense/income shares must sum to amountCents × 1,000,000.",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            }
          },
          "category": {
            "type": "string"
          },
          "splitMode": {
            "enum": [
              "equal",
              "amount",
              "percent",
              "shares",
              "imported",
              "rent",
              "transfer"
            ]
          },
          "notes": {
            "type": "string",
            "maxLength": 4000
          },
          "deletedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "sourceRow": {
            "type": "integer"
          },
          "fx": {
            "$ref": "#/components/schemas/Fx"
          },
          "attachments": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "$ref": "#/components/schemas/Attachment"
            }
          },
          "source": {
            "$ref": "#/components/schemas/FinanceSource"
          }
        }
      },
      "RentRule": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "startMonth": {
            "type": "string",
            "pattern": "^\\d{4}-(0[1-9]|1[0-2])$"
          },
          "payerId": {
            "type": "string"
          },
          "memberIds": {
            "type": "object",
            "required": [
              "yuan",
              "zhang",
              "xu",
              "ge"
            ],
            "properties": {
              "yuan": {
                "type": "string"
              },
              "zhang": {
                "type": "string"
              },
              "xu": {
                "type": "string"
              },
              "ge": {
                "type": "string"
              }
            }
          }
        }
      },
      "Book": {
        "type": "object",
        "required": [
          "schemaVersion",
          "id",
          "title",
          "currency",
          "members",
          "entries"
        ],
        "properties": {
          "schemaVersion": {
            "const": 1
          },
          "id": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "currency": {
            "const": "USD"
          },
          "members": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Member"
            }
          },
          "entries": {
            "type": "array",
            "maxItems": 10000,
            "items": {
              "$ref": "#/components/schemas/Entry"
            }
          },
          "rentRule": {
            "$ref": "#/components/schemas/RentRule"
          }
        }
      },
      "BookResponse": {
        "type": "object",
        "required": [
          "book",
          "revision",
          "updatedAt"
        ],
        "properties": {
          "book": {
            "$ref": "#/components/schemas/Book"
          },
          "revision": {
            "type": "integer"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Fx": {
        "type": "object",
        "required": [
          "currency",
          "amountMinor",
          "rate",
          "rateDate",
          "source"
        ],
        "properties": {
          "currency": {
            "enum": [
              "CNY",
              "EUR",
              "GBP",
              "CAD",
              "AUD",
              "JPY",
              "KRW",
              "HKD",
              "TWD",
              "SGD",
              "CHF",
              "NZD",
              "THB",
              "INR",
              "MXN",
              "KWD"
            ]
          },
          "amountMinor": {
            "type": "integer",
            "minimum": 1
          },
          "rate": {
            "type": "string",
            "description": "USD per one original unit, up to 12 decimals"
          },
          "rateDate": {
            "type": "string",
            "format": "date"
          },
          "source": {
            "enum": [
              "manual",
              "frankfurter"
            ]
          }
        }
      },
      "Attachment": {
        "type": "object",
        "required": [
          "id",
          "name",
          "mime",
          "size"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^att-[a-f0-9-]{36}$"
          },
          "name": {
            "type": "string",
            "maxLength": 160
          },
          "mime": {
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp",
              "image/gif",
              "image/heic",
              "image/heif",
              "application/pdf"
            ]
          },
          "size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10485760
          }
        }
      },
      "FinanceSource": {
        "type": "object",
        "required": [
          "provider",
          "ref",
          "date",
          "currency",
          "amountMinor",
          "merchant",
          "importedAt"
        ],
        "properties": {
          "provider": {
            "const": "finances"
          },
          "ref": {
            "type": "string",
            "pattern": "^fin-[a-f0-9]{64}$"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "currency": {
            "type": "string"
          },
          "amountMinor": {
            "type": "integer",
            "minimum": 1
          },
          "merchant": {
            "type": "string",
            "maxLength": 160
          },
          "importedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AttachmentResponse": {
        "type": "object",
        "properties": {
          "attachment": {
            "$ref": "#/components/schemas/Attachment"
          }
        }
      },
      "RateResponse": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string"
          },
          "quote": {
            "const": "USD"
          },
          "rate": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "source": {
            "const": "frankfurter"
          },
          "cached": {
            "type": "boolean"
          }
        }
      }
    }
  }
}
