{
  "openapi": "3.1.0",
  "info": {
    "title": "Aurum Auctions API",
    "version": "1.0.0",
    "summary": "Catalog reads plus sign-in and bidding for an online auction of collectibles.",
    "description": "Public read endpoints (categories, search, lots, bid history, profiles, reviews) and the main signed-in flow: sign in, read the session user, place a bid. Money values are strings of integer VND. Every error is JSON: { code, message, requestId, details? }. Demo login: demo_buyer1 / Passw0rd!demo. The full route list is in docs/api.md."
  },
  "servers": [
    { "url": "https://online-auction-uf4s.onrender.com", "description": "Production" },
    { "url": "http://localhost:3000", "description": "Local development" }
  ],
  "tags": [
    { "name": "catalog" },
    { "name": "lots" },
    { "name": "people" },
    { "name": "health" },
    { "name": "auth" },
    { "name": "bidding" }
  ],
  "paths": {
    "/auth/login": {
      "post": {
        "tags": ["auth"],
        "operationId": "login",
        "summary": "Sign in with username or email",
        "requestBody": {
          "required": true,
          "content": { "application/json": {
            "schema": { "type": "object", "required": ["identifier", "password"], "properties": { "identifier": { "type": "string" }, "password": { "type": "string" } } },
            "example": { "identifier": "demo_buyer1", "password": "Passw0rd!demo" }
          } }
        },
        "responses": {
          "200": { "description": "Token and session user", "content": { "application/json": { "schema": { "type": "object", "properties": { "token": { "type": "string" }, "user": { "$ref": "#/components/schemas/SessionUser" } } } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["auth"],
        "operationId": "getSessionUser",
        "summary": "The signed-in user (same shape as login's user)",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Session user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionUser" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/{id}/bid": {
      "post": {
        "tags": ["bidding"],
        "operationId": "placeBid",
        "summary": "Place or raise a private maximum (proxy bidding)",
        "description": "`amount` is your maximum; the visible price only rises as far as needed to lead. `outbid` is a 200: the bid was accepted but another maximum is higher. Needs a verified account that does not own the lot. Refusals carry a stable code, e.g. BID_TOO_LOW with details.minimumNextBid, AUCTION_ENDED.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/LotId" }, { "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": {
            "schema": { "type": "object", "required": ["amount"], "properties": { "amount": { "type": "string", "pattern": "^[0-9]{1,15}$" } } },
            "example": { "amount": "2000000" }
          } }
        },
        "responses": {
          "200": { "description": "Bid accepted", "content": { "application/json": { "schema": { "type": "object", "properties": {
            "outcome": { "type": "string", "enum": ["success", "updated", "outbid"] },
            "isLeading": { "type": "boolean" },
            "yourMaximum": { "type": "string" },
            "currentPrice": { "type": "string" },
            "minimumNextBid": { "type": "string" },
            "version": { "type": "integer" },
            "endTime": { "type": "string", "format": "date-time" },
            "serverTime": { "type": "string", "format": "date-time" }
          } } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/categories": {
      "get": {
        "tags": ["catalog"],
        "operationId": "listCategories",
        "summary": "Category tree",
        "responses": {
          "200": { "description": "Top-level categories with their subcategories", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } } } } }
        }
      }
    },
    "/search": {
      "get": {
        "tags": ["catalog"],
        "operationId": "searchLots",
        "summary": "Search lots (12 per page)",
        "parameters": [
          { "name": "q", "in": "query", "description": "Full-text query", "schema": { "type": "string", "maxLength": 200 } },
          { "name": "category", "in": "query", "description": "Exact category name (\"All\" = no filter); a parent category includes its children", "schema": { "type": "string", "maxLength": 100 } },
          { "name": "minPrice", "in": "query", "schema": { "type": "number", "minimum": 0 } },
          { "name": "maxPrice", "in": "query", "schema": { "type": "number", "minimum": 0 } },
          { "name": "sortBy", "in": "query", "schema": { "type": "string", "enum": ["relevance", "popularity", "price-asc", "price-desc", "ending-soon", "newest", "aToZ", "zToA"] } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["all", "active", "sold", "ended", "cancelled"] } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 10000, "default": 1 } }
        ],
        "responses": {
          "200": { "description": "A page of matching lots", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/top": {
      "get": {
        "tags": ["catalog"],
        "operationId": "listTopLots",
        "summary": "Featured lots for the home page",
        "parameters": [
          { "name": "sortBy", "in": "query", "schema": { "type": "string", "enum": ["newest", "endingSoon", "mostBids", "highestPrice"], "default": "newest" } },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["active", "sold", "ended", "cancelled"], "default": "active" } }
        ],
        "responses": {
          "200": { "description": "Lots", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/{id}": {
      "get": {
        "tags": ["lots"],
        "operationId": "getLot",
        "summary": "Lot snapshot: price, minimum next bid, end time, seller rating and what the caller may do",
        "parameters": [{ "$ref": "#/components/parameters/LotId" }],
        "responses": {
          "200": { "description": "Lot", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/{id}/events": {
      "get": {
        "tags": ["lots"],
        "operationId": "streamLotEvents",
        "summary": "Server-Sent Events stream of live price changes for one lot",
        "parameters": [
          { "$ref": "#/components/parameters/LotId" },
          { "name": "since", "in": "query", "description": "Last version the client has; Last-Event-ID wins over it", "schema": { "type": "integer", "minimum": 0 } }
        ],
        "responses": {
          "200": { "description": "Event stream; on a reset event, refetch GET /auction/{id}", "content": { "text/event-stream": { "schema": { "type": "string" } } } },
          "404": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/bids/{id}": {
      "get": {
        "tags": ["lots"],
        "operationId": "getBidHistory",
        "summary": "Public price history of a lot",
        "parameters": [
          { "$ref": "#/components/parameters/LotId" },
          { "name": "f", "in": "query", "schema": { "type": "string", "enum": ["all", "mine", "recent"], "default": "all" } }
        ],
        "responses": {
          "200": { "description": "Bids, newest first", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/auction/images/{id}": {
      "get": {
        "tags": ["lots"],
        "operationId": "getLotImages",
        "summary": "Photo URLs of a lot",
        "parameters": [{ "$ref": "#/components/parameters/LotId" }],
        "responses": { "200": { "description": "Images", "content": { "application/json": { "schema": {} } } }, "400": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/auction/description/{id}": {
      "get": {
        "tags": ["lots"],
        "operationId": "getLotDescription",
        "summary": "Sanitized HTML description and its edit log",
        "parameters": [{ "$ref": "#/components/parameters/LotId" }],
        "responses": { "200": { "description": "Description", "content": { "application/json": { "schema": {} } } }, "400": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/auction/comments/{id}": {
      "get": {
        "tags": ["lots"],
        "operationId": "getLotComments",
        "summary": "Public questions and answers on a lot",
        "parameters": [{ "$ref": "#/components/parameters/LotId" }],
        "responses": { "200": { "description": "Comments", "content": { "application/json": { "schema": {} } } }, "400": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/users/profile": {
      "get": {
        "tags": ["people"],
        "operationId": "getPublicProfile",
        "summary": "Public profile and rating of a user (rating.percentage is null without reviews)",
        "parameters": [{ "name": "username", "in": "query", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 50 } }],
        "responses": {
          "200": { "description": "Profile", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/reviews": {
      "get": {
        "tags": ["people"],
        "operationId": "listReviews",
        "summary": "Reviews between buyers and sellers",
        "parameters": [
          { "name": "reviewerId", "in": "query", "schema": { "type": "integer" } },
          { "name": "targetId", "in": "query", "schema": { "type": "integer" } },
          { "name": "auctionId", "in": "query", "schema": { "type": "integer" } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 20 } }
        ],
        "responses": { "200": { "description": "A page of reviews", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/reviews/stats": {
      "get": {
        "tags": ["people"],
        "operationId": "getReviewStats",
        "summary": "Positive/negative counts for the same filters as /reviews",
        "parameters": [
          { "name": "reviewerId", "in": "query", "schema": { "type": "integer" } },
          { "name": "targetId", "in": "query", "schema": { "type": "integer" } },
          { "name": "auctionId", "in": "query", "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Counts", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/reviews/{id}": {
      "get": {
        "tags": ["people"],
        "operationId": "getReview",
        "summary": "One review",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } }],
        "responses": { "200": { "description": "Review", "content": { "application/json": { "schema": { "type": "object" } } } }, "404": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/health/live": {
      "get": {
        "tags": ["health"],
        "operationId": "live",
        "summary": "Liveness",
        "responses": { "200": { "description": "The process answers", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "const": "ok" } } } } } } }
      }
    },
    "/health/ready": {
      "get": {
        "tags": ["health"],
        "operationId": "ready",
        "summary": "Readiness: database, migrations and worker status",
        "responses": {
          "200": { "description": "Ready", "content": { "application/json": { "schema": { "type": "object" } } } },
          "503": { "description": "Database unreachable or migrations pending", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "The token from POST /auth/login" }
    },
    "parameters": {
      "LotId": { "name": "id", "in": "path", "required": true, "description": "Lot (auction) id", "schema": { "type": "integer", "minimum": 1 } },
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "description": "Retry-safe key (8-100 of [A-Za-z0-9._:-]); the same key and body replays the stored result", "schema": { "type": "string", "minLength": 8, "maxLength": 100 } }
    },
    "schemas": {
      "SessionUser": {
        "type": "object",
        "required": ["userId", "username", "email", "role"],
        "properties": {
          "userId": { "type": "integer" },
          "username": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "fullName": { "type": ["string", "null"] },
          "role": { "type": "string", "enum": ["unauthorized", "buyer", "seller", "admin"] },
          "status": { "type": "string" },
          "isVerified": { "type": "boolean" },
          "imgUrl": { "type": ["string", "null"] }
        }
      },
      "Category": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "subcategories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } }
        }
      },
      "Error": {
        "type": "object",
        "required": ["code", "message", "requestId"],
        "properties": {
          "code": { "type": "string", "description": "Stable machine-readable code, e.g. NOT_FOUND, VALIDATION_ERROR, BID_TOO_LOW" },
          "message": { "type": "string", "description": "Human-readable explanation" },
          "requestId": { "type": "string", "description": "Also sent as the X-Request-Id header; quote it when reporting a problem" },
          "details": { "description": "Extra context for some codes, e.g. minimumNextBid" }
        }
      }
    },
    "responses": {
      "Error": { "description": "Error envelope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
