{
  "openapi": "3.1.0",
  "info": {
    "title": "Propura API",
    "version": "v1",
    "summary": "Public API for Propura — procurement for construction companies.",
    "description": "Programmatic access to Propura's public surface.\n\n- **Catalog items** (`/api/v1/catalog-items`) — manage your product catalog from an external system (ERP, integration). Authenticated with an org API key sent as the `x-api-key` header. Mint one from Settings → API in the app.\n- **Suppliers** (`/api/v1/suppliers`) — manage suppliers from an external system, addressed by the business `code` you own. Creating a supplier that matches an existing code-less (manually-created) one by email/phone/taxId adopts that record instead of duplicating it. No delete — deactivate with `status: INACTIVE`.\n- **Order share links** (`/orders/public/{token}`) — anonymous, capability-based access for suppliers without an account; the URL token is the credential.\n- **Status** (`/api/v1/status`) — public, unauthenticated meta endpoint.\n\nHuman docs: https://propura.app/docs/api",
    "contact": { "name": "Propura", "url": "https://propura.app" }
  },
  "servers": [
    { "url": "https://api.propura.app", "description": "Production" }
  ],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "Catalog", "description": "Product catalog managed via API key." },
    { "name": "Suppliers", "description": "Suppliers managed via API key, addressed by business code." },
    { "name": "Orders", "description": "Anonymous supplier access via share token." },
    { "name": "Meta", "description": "Public metadata." }
  ],
  "paths": {
    "/api/v1/status": {
      "get": {
        "tags": ["Meta"],
        "summary": "Service status and API metadata",
        "description": "Public, unauthenticated. Advertises the API surface and how to authenticate. Exposes no tenant data.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Status" },
                "example": {
                  "name": "Propura API",
                  "version": "v1",
                  "status": "ok",
                  "docs": "https://propura.app/docs/api",
                  "openapi": "https://propura.app/openapi.json",
                  "auth": "x-api-key"
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/catalog-items": {
      "get": {
        "tags": ["Catalog"],
        "summary": "List catalog items",
        "parameters": [
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by name or code (case-insensitive)." },
          { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated list",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CatalogItemPage" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "post": {
        "tags": ["Catalog"],
        "summary": "Create a catalog item",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CatalogItemInput" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogItem" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/catalog-items/{code}": {
      "parameters": [
        { "name": "code", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Org-unique business code of the item." }
      ],
      "put": {
        "tags": ["Catalog"],
        "summary": "Update a catalog item by code",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CatalogItemInput" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogItem" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "delete": {
        "tags": ["Catalog"],
        "summary": "Delete a catalog item by code",
        "responses": {
          "204": { "description": "Deleted" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/suppliers": {
      "get": {
        "tags": ["Suppliers"],
        "summary": "List suppliers",
        "parameters": [
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by name, code or taxId (case-insensitive)." },
          { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated list",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SupplierPage" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "post": {
        "tags": ["Suppliers"],
        "summary": "Create or adopt a supplier",
        "description": "Creates a supplier. If the payload matches an existing supplier that has no `code` (created manually in the app) by `email`, `phone` or `taxId`, the `code` is attached to that record (adoption) instead of creating a duplicate. If it matches more than one code-less supplier the request is ambiguous and returns 409. A duplicate `code`, or an `email` already owned by another supplier, also returns 409.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SupplierCreateInput" } } }
        },
        "responses": {
          "201": {
            "description": "Created or adopted",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Supplier" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/suppliers/{code}": {
      "parameters": [
        { "name": "code", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Org-unique business code of the supplier." }
      ],
      "get": {
        "tags": ["Suppliers"],
        "summary": "Fetch a supplier by code",
        "responses": {
          "200": {
            "description": "The supplier",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Supplier" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "put": {
        "tags": ["Suppliers"],
        "summary": "Update a supplier by code",
        "description": "Updates the supplier with that `code` (the code itself is immutable). Same contact + tags validation as create.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SupplierUpdateInput" } } }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Supplier" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/orders/public/{token}": {
      "parameters": [
        { "name": "token", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Opaque share token. The token itself is the credential." }
      ],
      "get": {
        "tags": ["Orders"],
        "summary": "Fetch a shared order",
        "description": "Anonymous, capability-based access for suppliers without an account.",
        "security": [],
        "responses": {
          "200": { "description": "The order the token points to." },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Org API key. Mint and rotate it in Settings → API."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request (validation failed, bad query params, or unknown unit). `message` is an array of field errors.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "statusCode": 400, "message": ["name should not be empty"], "error": "Bad Request" } } }
      },
      "Unauthorized": {
        "description": "Missing or invalid x-api-key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "statusCode": 401, "message": "Falta el header x-api-key", "error": "Unauthorized" } } }
      },
      "NotFound": {
        "description": "The referenced resource does not exist (in this org).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "statusCode": 404, "message": "No existe un producto con código P001", "error": "Not Found" } } }
      },
      "Conflict": {
        "description": "The resource already exists (e.g. duplicate code).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "statusCode": 409, "message": "Ya existe un producto con código P001", "error": "Conflict" } } }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "statusCode": 500, "message": "Internal server error", "error": "Internal Server Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Standard NestJS error envelope returned by every 4xx/5xx response. `message` is a string for most errors and an array of strings for validation (400) failures.",
        "required": ["statusCode", "message"],
        "properties": {
          "statusCode": { "type": "integer", "description": "HTTP status code, mirrors the response status.", "example": 400 },
          "message": {
            "oneOf": [
              { "type": "string" },
              { "type": "array", "items": { "type": "string" } }
            ],
            "description": "Human-readable message. Array of field errors on validation failures."
          },
          "error": { "type": "string", "description": "Machine-readable status label (e.g. \"Bad Request\", \"Unauthorized\", \"Not Found\", \"Conflict\").", "example": "Bad Request" }
        }
      },
      "MeasurementUnit": {
        "type": "string",
        "enum": ["UNI", "KGR", "LTR", "M3", "MTS"],
        "description": "Unit of measure."
      },
      "CatalogItemInput": {
        "type": "object",
        "required": ["code", "name"],
        "properties": {
          "code": { "type": "string", "description": "Org-unique business code." },
          "name": { "type": "string" },
          "unit": { "$ref": "#/components/schemas/MeasurementUnit" }
        }
      },
      "CatalogItem": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "code": { "type": "string" },
          "name": { "type": "string" },
          "unit": { "oneOf": [{ "$ref": "#/components/schemas/MeasurementUnit" }, { "type": "null" }] },
          "orgId": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "CatalogItemPage": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogItem" } },
          "total": { "type": "integer" },
          "page": { "type": "integer" },
          "limit": { "type": "integer" }
        }
      },
      "TaxCondition": {
        "type": "string",
        "enum": ["REGISTERED", "SMALL_TAXPAYER", "EXEMPT", "NON_TAXPAYER", "END_CONSUMER"],
        "description": "Argentine tax condition (condición fiscal)."
      },
      "AfipVoucherCode": {
        "type": "string",
        "enum": ["001", "002", "003", "004", "005", "006", "007", "008", "009", "010", "011", "012", "013", "015", "016", "017", "018", "019", "020", "021", "022", "023", "024", "025", "026", "027", "028", "030", "032", "033", "034", "035", "036", "037", "038", "039", "040", "041", "043", "045", "046", "048", "049", "050", "051", "052", "053", "054", "055", "056", "057", "058", "059", "060", "061", "063", "064", "066", "070", "081", "082", "083", "090", "099", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119", "120", "150", "151", "157", "161", "166", "167", "180", "182", "183", "185", "186", "188", "190", "195", "196", "197", "201", "202", "203", "206", "207", "208", "211", "212", "213"],
        "description": "Código de comprobante AFIP con el que factura el proveedor, 3 dígitos con ceros a la izquierda (ej. \"006\" = Facturas B). El enum es el catálogo completo aceptado; cualquier otro valor devuelve 400. Ojo con los huecos de la tabla de AFIP (043 salta a 045)."
      },
      "AfipOperationTypeCode": {
        "type": "string",
        "enum": ["0", "A", "C", "E", "N", "T", "X", "Z"],
        "description": "Código de tipo de operación AFIP: 0 gravadas, A no alcanzado, C canje/devol. IVA turistas, E exentas, N no gravado, T reintegro Dto. 1043/2016, X importaciones del exterior, Z importaciones de zona franca."
      },
      "SupplierStatus": {
        "type": "string",
        "enum": ["ACTIVE", "INACTIVE"],
        "description": "Supplier lifecycle status. Deactivate a supplier by setting INACTIVE (there is no delete)."
      },
      "SupplierCreateInput": {
        "type": "object",
        "required": ["code", "name"],
        "description": "At least one of `email` or `phone`, and at least one `tag`, are required (validation returns 400 otherwise).",
        "properties": {
          "code": { "type": "string", "description": "Org-unique business code you own (the ERP identifier)." },
          "name": { "type": "string" },
          "legalName": { "type": "string" },
          "taxId": { "type": "string", "description": "CUIT." },
          "taxCondition": { "$ref": "#/components/schemas/TaxCondition" },
          "afipVoucherCode": { "$ref": "#/components/schemas/AfipVoucherCode" },
          "afipOperationTypeCode": { "$ref": "#/components/schemas/AfipOperationTypeCode" },
          "email": { "type": "string", "format": "email", "description": "Org-unique." },
          "phone": { "type": "string" },
          "financeEmail": { "type": "string", "format": "email" },
          "financePhone": { "type": "string" },
          "fiscalAddress": { "type": "string" },
          "commercialAddress": { "type": "string" },
          "paymentMethod": { "type": "string" },
          "description": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" }, "description": "Categories; at least one. Stored lowercased." },
          "status": { "$ref": "#/components/schemas/SupplierStatus" }
        }
      },
      "SupplierUpdateInput": {
        "type": "object",
        "required": ["name"],
        "description": "Same as SupplierCreateInput without `code` (taken from the URL, immutable). At least one of `email` or `phone`, and at least one `tag`, are required.",
        "properties": {
          "name": { "type": "string" },
          "legalName": { "type": "string" },
          "taxId": { "type": "string", "description": "CUIT." },
          "taxCondition": { "$ref": "#/components/schemas/TaxCondition" },
          "afipVoucherCode": { "$ref": "#/components/schemas/AfipVoucherCode" },
          "afipOperationTypeCode": { "$ref": "#/components/schemas/AfipOperationTypeCode" },
          "email": { "type": "string", "format": "email", "description": "Org-unique." },
          "phone": { "type": "string" },
          "financeEmail": { "type": "string", "format": "email" },
          "financePhone": { "type": "string" },
          "fiscalAddress": { "type": "string" },
          "commercialAddress": { "type": "string" },
          "paymentMethod": { "type": "string" },
          "description": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "status": { "$ref": "#/components/schemas/SupplierStatus" }
        }
      },
      "Supplier": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "code": { "oneOf": [{ "type": "string" }, { "type": "null" }], "description": "Null for suppliers created in the app that no API create has adopted yet." },
          "name": { "type": "string" },
          "legalName": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "taxId": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "taxCondition": { "oneOf": [{ "$ref": "#/components/schemas/TaxCondition" }, { "type": "null" }] },
          "afipVoucherCode": { "oneOf": [{ "$ref": "#/components/schemas/AfipVoucherCode" }, { "type": "null" }] },
          "afipOperationTypeCode": { "oneOf": [{ "$ref": "#/components/schemas/AfipOperationTypeCode" }, { "type": "null" }] },
          "email": { "oneOf": [{ "type": "string", "format": "email" }, { "type": "null" }] },
          "phone": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "financeEmail": { "oneOf": [{ "type": "string", "format": "email" }, { "type": "null" }] },
          "financePhone": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "fiscalAddress": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "commercialAddress": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "paymentMethod": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "description": { "oneOf": [{ "type": "string" }, { "type": "null" }] },
          "tags": { "type": "array", "items": { "type": "string" } },
          "status": { "$ref": "#/components/schemas/SupplierStatus" },
          "orgId": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "SupplierPage": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Supplier" } },
          "total": { "type": "integer" },
          "page": { "type": "integer" },
          "limit": { "type": "integer" }
        }
      },
      "Status": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "version": { "type": "string" },
          "status": { "type": "string" },
          "docs": { "type": "string", "format": "uri" },
          "openapi": { "type": "string", "format": "uri" },
          "auth": { "type": "string" }
        }
      }
    }
  }
}
