{
  "openapi": "3.1.0",
  "info": {
    "title": "Thais Almeida — API de Parceiros",
    "summary": "API de integração para parceiros: catálogo, clientes, orçamentos, cobranças e agenda.",
    "description": "API usada por parceiros e agentes para consultar o catálogo de serviços e pacotes, cadastrar clientes, montar orçamentos, gerar cobranças (PIX, boleto, cartão via Asaas) e reservar horários na agenda de Thais Almeida, maquiadora profissional em Taubaté – SP.\n\nAutenticação por chave de parceiro: `Authorization: Bearer <PARTNER_API_KEY>`. A chave é emitida sob demanda — veja https://www.thaisalmeida.art/developers.\n\nEsta especificação cobre os endpoints com contrato estável e verificado. A API tem outros endpoints de parceiro (eventos, grupos, bloqueio de horários) que ainda não estão especificados aqui; a documentação em prosa está em https://www.thaisalmeida.art/partner-api-docs.md.",
    "version": "1.0.0",
    "contact": {
      "name": "Thais Almeida",
      "url": "https://www.thaisalmeida.art/developers",
      "email": "contato@thaisalmeida.art"
    },
    "license": { "name": "Proprietária", "identifier": "LicenseRef-Proprietary" }
  },
  "externalDocs": {
    "description": "Documentação em prosa da API de Parceiros",
    "url": "https://www.thaisalmeida.art/partner-api-docs.md"
  },
  "servers": [
    {
      "url": "https://kealfidxmdzmskfwhuee.supabase.co/functions/v1",
      "description": "Produção"
    }
  ],
  "security": [{ "partnerApiKey": [] }],
  "tags": [
    { "name": "Catálogo", "description": "Serviços e pacotes oferecidos." },
    { "name": "Clientes", "description": "Busca e cadastro de clientes no CRM." },
    { "name": "Orçamentos", "description": "Criação de orçamentos com página pública." },
    { "name": "Pagamentos", "description": "Cobranças via Asaas (PIX, boleto, cartão)." },
    { "name": "Agenda", "description": "Horários disponíveis e reserva." }
  ],
  "paths": {
    "/partner-list-services": {
      "get": {
        "operationId": "listServices",
        "tags": ["Catálogo"],
        "summary": "Listar serviços",
        "description": "Retorna todos os tipos de serviço ativos, com nome e descrição em pt-BR, inglês e espanhol.",
        "responses": {
          "200": {
            "description": "Lista de serviços.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["services"],
                  "properties": {
                    "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-list-packages": {
      "get": {
        "operationId": "listPackages",
        "tags": ["Catálogo"],
        "summary": "Listar pacotes",
        "description": "Retorna todos os pacotes ativos com preços, itens inclusos e preços de adicionais.",
        "responses": {
          "200": {
            "description": "Lista de pacotes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["packages"],
                  "properties": {
                    "packages": { "type": "array", "items": { "$ref": "#/components/schemas/Package" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-lookup-client": {
      "get": {
        "operationId": "lookupClient",
        "tags": ["Clientes"],
        "summary": "Buscar cliente",
        "description": "Busca um cliente por telefone ou por Instagram Scoped ID (IGSID). Ao menos um dos dois é obrigatório. Cliente inexistente devolve 200 com `found: false` — não 404.",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "required": false,
            "description": "Telefone em formato internacional.",
            "schema": { "type": "string", "examples": ["+5511999999999"] }
          },
          {
            "name": "instagram_scoped_id",
            "in": "query",
            "required": false,
            "description": "IGSID de 15 a 20 dígitos fornecido pela Meta no Instagram DM.",
            "schema": { "type": "string", "pattern": "^[0-9]{15,20}$", "examples": ["6541234567890123"] }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado da busca.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["found"],
                  "properties": {
                    "found": { "type": "boolean" },
                    "client": { "$ref": "#/components/schemas/Client" }
                  }
                },
                "examples": {
                  "encontrado": {
                    "summary": "Cliente encontrado",
                    "value": {
                      "found": true,
                      "client": {
                        "id": "5f1c1b3e-0c2a-4f5b-9d1a-2b3c4d5e6f70",
                        "name": "Maria Silva",
                        "phone": "+5511999999999",
                        "email": "maria@email.com",
                        "instagram": "@maria",
                        "instagram_scoped_id": "6541234567890123",
                        "lead_status": "novo"
                      }
                    }
                  },
                  "naoEncontrado": { "summary": "Cliente não encontrado", "value": { "found": false } }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-create-client": {
      "post": {
        "operationId": "createClient",
        "tags": ["Clientes"],
        "summary": "Criar cliente",
        "description": "Registra um novo cliente no CRM. É obrigatório informar `phone` ou `instagram_scoped_id`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "anyOf": [{ "required": ["phone"] }, { "required": ["instagram_scoped_id"] }],
                "properties": {
                  "name": { "type": "string", "description": "Nome completo." },
                  "phone": { "type": "string", "examples": ["+5511999999999"] },
                  "instagram_scoped_id": { "type": "string", "pattern": "^[0-9]{15,20}$" },
                  "email": { "type": "string", "format": "email" },
                  "instagram": { "type": "string", "examples": ["@mariasilva"] }
                }
              },
              "examples": {
                "porTelefone": {
                  "value": {
                    "name": "Maria Silva",
                    "phone": "+5511999999999",
                    "email": "maria@email.com",
                    "instagram": "@mariasilva"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente criado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "name": { "type": "string" },
                    "phone": { "type": ["string", "null"] },
                    "instagram_scoped_id": { "type": ["string", "null"] }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-create-budget": {
      "post": {
        "operationId": "createBudget",
        "tags": ["Orçamentos"],
        "summary": "Criar orçamento",
        "description": "Cria um orçamento vinculado a um cliente e devolve a URL pública para a cliente aprovar. Se `event_date` for informado, um evento também é criado.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["client_id", "packages"],
                "properties": {
                  "client_id": { "type": "string", "format": "uuid" },
                  "packages": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": ["package_id"],
                      "properties": {
                        "package_id": { "type": "string", "format": "uuid" },
                        "quantity": { "type": "integer", "minimum": 1, "default": 1 },
                        "custom_price": { "type": "number", "description": "Sobrescreve o preço do pacote." }
                      }
                    }
                  },
                  "event_date": { "type": "string", "format": "date", "description": "Informar cria um evento junto." },
                  "event_type_id": { "type": "string", "format": "uuid" },
                  "address": { "type": "string" },
                  "extra_hairstyle_count": { "type": "integer", "default": 0 },
                  "extra_makeup_count": { "type": "integer", "default": 0 },
                  "extra_hairstyle_price": { "type": "number" },
                  "extra_makeup_price": { "type": "number" },
                  "valid_until": { "type": "string", "format": "date" },
                  "notes": { "type": "string", "description": "Observação interna, não aparece para a cliente." },
                  "service_description": { "type": "string" },
                  "language": { "type": "string", "enum": ["pt-BR", "en", "es"], "default": "pt-BR" },
                  "currency": { "type": "string", "enum": ["BRL", "USD", "EUR"], "default": "BRL" }
                }
              },
              "examples": {
                "noivaComMadrinhas": {
                  "summary": "Noiva + 3 madrinhas com preço customizado",
                  "value": {
                    "client_id": "5f1c1b3e-0c2a-4f5b-9d1a-2b3c4d5e6f70",
                    "event_date": "2026-06-15",
                    "packages": [
                      { "package_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "quantity": 1 },
                      { "package_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e", "quantity": 3, "custom_price": 350.0 }
                    ],
                    "extra_makeup_count": 2,
                    "extra_makeup_price": 250.0,
                    "valid_until": "2026-03-30",
                    "service_description": "Maquiagem e penteado para noiva + 3 madrinhas",
                    "language": "pt-BR",
                    "currency": "BRL"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Orçamento criado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "budget_id": { "type": "string", "format": "uuid" },
                    "unique_code": { "type": "string", "description": "Código curto do orçamento.", "examples": ["A1B2C3D4"] },
                    "public_url": { "type": "string", "format": "uri", "description": "Página pública para a cliente aprovar." },
                    "event_id": { "type": ["string", "null"], "format": "uuid" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-create-charge": {
      "post": {
        "operationId": "createCharge",
        "tags": ["Pagamentos"],
        "summary": "Gerar cobrança",
        "description": "Registra um pagamento no CRM e gera a cobrança no gateway Asaas. Pagamentos confirmados voltam por webhook — o parceiro não precisa fazer polling.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["client_id", "amount", "payment_type", "billing_type"],
                "properties": {
                  "client_id": { "type": "string", "format": "uuid" },
                  "amount": { "type": "number", "exclusiveMinimum": 0, "examples": [2500.0] },
                  "payment_type": {
                    "type": "string",
                    "enum": ["sinal", "parcela", "pagamento_final", "pagamento_unico", "adicional"],
                    "description": "`parcela` exige `installment_number` e `total_installments`."
                  },
                  "billing_type": { "type": "string", "enum": ["PIX", "BOLETO", "CREDIT_CARD"] },
                  "description": { "type": "string" },
                  "due_date": { "type": "string", "format": "date", "description": "Padrão: hoje." },
                  "currency": { "type": "string", "default": "BRL", "description": "Cobranças via Asaas são apenas em BRL." },
                  "contract_id": { "type": "string", "format": "uuid" },
                  "installment_number": { "type": "integer", "minimum": 1 },
                  "total_installments": { "type": "integer", "minimum": 1 },
                  "notes": { "type": "string" }
                }
              },
              "examples": {
                "pixAvista": {
                  "summary": "PIX à vista",
                  "value": {
                    "client_id": "5f1c1b3e-0c2a-4f5b-9d1a-2b3c4d5e6f70",
                    "amount": 2500.0,
                    "payment_type": "pagamento_unico",
                    "billing_type": "PIX",
                    "description": "Pacote Noiva Completo",
                    "due_date": "2026-03-15"
                  }
                },
                "parcelaBoleto": {
                  "summary": "Primeira parcela em boleto",
                  "value": {
                    "client_id": "5f1c1b3e-0c2a-4f5b-9d1a-2b3c4d5e6f70",
                    "amount": 833.33,
                    "payment_type": "parcela",
                    "billing_type": "BOLETO",
                    "installment_number": 1,
                    "total_installments": 3,
                    "due_date": "2026-03-01"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cobrança gerada. `pix_qrcode` e `pix_code` só vêm quando `billing_type` é `PIX`; para os demais use `invoice_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payment_id": { "type": "string", "format": "uuid" },
                    "asaas_payment_id": { "type": "string" },
                    "status": { "type": "string", "examples": ["PENDING"] },
                    "invoice_url": { "type": "string", "format": "uri" },
                    "pix_qrcode": { "type": "string", "description": "PNG em data URI." },
                    "pix_code": { "type": "string", "description": "Payload copia e cola do PIX." },
                    "billing_type": { "type": "string", "enum": ["PIX", "BOLETO", "CREDIT_CARD"] },
                    "amount": { "type": "number" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-list-slots": {
      "get": {
        "operationId": "listSlots",
        "tags": ["Agenda"],
        "summary": "Listar horários disponíveis",
        "description": "Devolve os horários livres de uma data, ordenados por hora de início. Um horário já agendado continua aparecendo se todos os serviços daquele agendamento não bloqueiam a agenda.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Data no formato YYYY-MM-DD.",
            "schema": { "type": "string", "format": "date", "examples": ["2026-06-15"] }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Filtra por tipo aceito no horário.",
            "schema": { "type": "string", "enum": ["atendimento", "reuniao", "qualquer"] }
          }
        ],
        "responses": {
          "200": {
            "description": "Horários disponíveis. Array vazio quando não há nenhum.",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Slot" } }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/partner-book-slot": {
      "post": {
        "operationId": "bookSlot",
        "tags": ["Agenda"],
        "summary": "Reservar ou confirmar um horário",
        "description": "Agenda um horário. Com `reserve: true` o horário fica pré-reservado por `ttl_minutes` e é liberado sozinho se não for confirmado. Identifique a cliente por `client_id` ou pelo trio `client_name` + `client_phone` + `client_email`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["slot_id", "booking_type"],
                "properties": {
                  "slot_id": { "type": "string", "format": "uuid" },
                  "booking_type": { "type": "string", "enum": ["atendimento", "reuniao"] },
                  "client_id": { "type": "string", "format": "uuid" },
                  "client_name": { "type": "string", "description": "Nome completo, com sobrenome." },
                  "client_phone": { "type": "string", "description": "Com DDD." },
                  "client_email": { "type": "string", "format": "email" },
                  "service_type_id": { "type": "string", "format": "uuid", "description": "Exigido em `atendimento`, exceto noiva com pacote." },
                  "service_name": { "type": "string" },
                  "meeting_reason": { "type": "string", "description": "Usado quando `booking_type` é `reuniao`." },
                  "notes": { "type": "string" },
                  "reserve": { "type": "boolean", "default": false },
                  "ttl_minutes": { "type": "integer", "default": 15, "description": "Validade da pré-reserva." },
                  "event_type_id": { "type": "string", "format": "uuid" },
                  "event_date": { "type": "string", "format": "date" },
                  "is_bride": { "type": "boolean" },
                  "event_group_id": { "type": "string", "format": "uuid" },
                  "budget_id": { "type": "string", "format": "uuid" },
                  "package_id": { "type": "string", "format": "uuid" },
                  "blocking_qty": { "type": "integer", "default": 1 }
                }
              },
              "examples": {
                "reservaTemporaria": {
                  "summary": "Pré-reserva de 15 minutos",
                  "value": {
                    "slot_id": "3c4d5e6f-7a8b-4c9d-8e1f-2a3b4c5d6e7f",
                    "booking_type": "atendimento",
                    "client_name": "Maria Silva",
                    "client_phone": "+5511999999999",
                    "service_type_id": "4d5e6f7a-8b9c-4d0e-9f1a-2b3c4d5e6f70",
                    "reserve": true,
                    "ttl_minutes": 15
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Horário reservado ou confirmado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "booking_id": { "type": "string", "format": "uuid" },
                    "booking_ids": { "type": "array", "items": { "type": "string", "format": "uuid" } },
                    "reserved_until": { "type": ["string", "null"], "format": "date-time", "description": "Preenchido quando `reserve` é true." }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "409": {
            "description": "O horário não está mais disponível — já foi agendado ou bloqueado. Liste os horários de novo e escolha outro.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": { "ocupado": { "value": { "error": "slot is not available (already booked or blocked)" } } }
              }
            }
          },
          "422": {
            "description": "O horário existe mas não aceita esse `booking_type`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": { "tipoIncompativel": { "value": { "error": "slot does not accept reuniao" } } }
              }
            }
          },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "partnerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Chave de parceiro enviada como `Authorization: Bearer <PARTNER_API_KEY>`. Solicite em https://www.thaisalmeida.art/developers."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Todo erro da API devolve este corpo, com `Content-Type: application/json`. O status HTTP carrega a categoria do erro.",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Mensagem legível descrevendo o que deu errado." }
        },
        "examples": [{ "error": "Unauthorized" }]
      },
      "Service": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "name_en": { "type": ["string", "null"] },
          "name_es": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "description_en": { "type": ["string", "null"] },
          "description_es": { "type": ["string", "null"] },
          "price": { "type": ["number", "null"] },
          "is_bride_only": { "type": ["boolean", "null"], "description": "Serviço exclusivo de noiva." },
          "blocks_agenda_slot": { "type": ["boolean", "null"], "description": "Se true, ocupa o horário inteiro na agenda." }
        }
      },
      "Package": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "name_en": { "type": ["string", "null"] },
          "name_es": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "description_en": { "type": ["string", "null"] },
          "description_es": { "type": ["string", "null"] },
          "price": { "type": "number" },
          "features": { "type": "array", "items": { "type": "string" } },
          "features_en": { "type": "array", "items": { "type": "string" } },
          "features_es": { "type": "array", "items": { "type": "string" } },
          "image_url": { "type": ["string", "null"], "format": "uri" },
          "extra_hairstyle_price": { "type": ["number", "null"] },
          "extra_makeup_price": { "type": ["number", "null"] }
        }
      },
      "Client": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "phone": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"], "format": "email" },
          "instagram": { "type": ["string", "null"] },
          "instagram_scoped_id": { "type": ["string", "null"] },
          "lead_status": { "type": ["string", "null"] }
        }
      },
      "Slot": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "date": { "type": "string", "format": "date" },
          "start_time": { "type": "string", "examples": ["09:00:00"] },
          "end_time": { "type": "string", "examples": ["13:00:00"] },
          "accepted_type": { "type": "string", "enum": ["atendimento", "reuniao", "qualquer"] }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Parâmetros inválidos ou ausentes. A mensagem diz qual campo corrigir.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": { "faltaData": { "value": { "error": "date param is required" } } }
          }
        }
      },
      "Unauthorized": {
        "description": "Chave de parceiro ausente ou inválida. Envie `Authorization: Bearer <PARTNER_API_KEY>`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": { "semChave": { "value": { "error": "Unauthorized" } } }
          }
        }
      },
      "NotFound": {
        "description": "Recurso não encontrado — cliente, pacote ou horário inexistente.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": { "pacote": { "value": { "error": "package not found" } } }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Método HTTP incorreto para este endpoint.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": { "metodo": { "value": { "error": "method not allowed" } } }
          }
        }
      },
      "InternalError": {
        "description": "Erro interno. Tente de novo; se persistir, fale com contato@thaisalmeida.art.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    }
  }
}
