diff --git a/docs/openapi-v1.json b/docs/openapi-v1.json index 050e838..7882251 100644 --- a/docs/openapi-v1.json +++ b/docs/openapi-v1.json @@ -1166,6 +1166,12 @@ "204": { "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "404": { + "description": "Nenhum certificado vivo para este (tenant_ref, branch_ref) -- anti-oracle" + }, "422": { "content": { "application/json": { @@ -1234,6 +1240,12 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "404": { + "description": "Nenhum certificado vivo para este (tenant_ref, branch_ref) -- anti-oracle" + }, "422": { "content": { "application/json": { @@ -1311,15 +1323,17 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "409": { + "description": "`detail.code`=`certificate_upload_conflict` -- upload concorrente venceu a corrida" + }, + "413": { + "description": "Certificado excede o tamanho m\u00e1ximo permitido (262144 bytes)" + }, "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" + "description": "PFX inv\u00e1lido/senha incorreta, CNPJ do certificado diverge do declarado, certificado vencido ou ainda n\u00e3o vigente" } }, "summary": "Upload Certificate Endpoint", @@ -1438,6 +1452,9 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, "422": { "content": { "application/json": { @@ -1497,6 +1514,12 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "404": { + "description": "Documento fiscal n\u00e3o encontrado (ou de outro product/tenant -- anti-oracle)" + }, "422": { "content": { "application/json": { @@ -1550,10 +1573,17 @@ "content": { "application/json": { "schema": {} - } + }, + "application/xml": {} }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "404": { + "description": "Documento fiscal n\u00e3o encontrado (ou de outro product/tenant -- anti-oracle)" + }, "422": { "content": { "application/json": { @@ -1616,6 +1646,16 @@ }, "responses": { "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FiscalDocumentRead" + } + } + }, + "description": "Idempotency-Key repetida -- documento j\u00e1 emitido devolvido (replay, created=False)" + }, + "201": { "content": { "application/json": { "schema": { @@ -1625,6 +1665,12 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "409": { + "description": "Conflito estruturado -- `detail.code` identifica a causa: `fiscal_config_missing` (certificado/s\u00e9rie ausentes ou certificado vencido), `emitente_certificate_cnpj_mismatch` (CNPJ do emitente diverge do certificado da filial), `pagamento_total_diverge` (\u03a3 vPag \u2260 vNF), `fiscal_document_conflict` (colis\u00e3o de chave de acesso)." + }, "422": { "content": { "application/json": { @@ -1720,6 +1766,9 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, "422": { "content": { "application/json": { @@ -1777,6 +1826,12 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "409": { + "description": "`detail.code`=`duplicate_fiscal_series` -- j\u00e1 existe uma s\u00e9rie fiscal (live ou soft-deletada) para este (tenant_ref, branch_ref, document_model, serie)" + }, "422": { "content": { "application/json": { @@ -1846,6 +1901,15 @@ }, "description": "Successful Response" }, + "401": { + "description": "API key ausente ou inv\u00e1lida" + }, + "404": { + "description": "S\u00e9rie fiscal n\u00e3o encontrada (ou de outro product -- anti-oracle)" + }, + "409": { + "description": "`detail.code`=`fiscal_series_number_regression`" + }, "422": { "content": { "application/json": { diff --git a/src/fiscal_svc/certificates/router.py b/src/fiscal_svc/certificates/router.py index d5b4f9e..eb22e4c 100644 --- a/src/fiscal_svc/certificates/router.py +++ b/src/fiscal_svc/certificates/router.py @@ -41,7 +41,22 @@ async def _read_upload_capped(file: UploadFile, max_bytes: int) -> bytes: return b"".join(chunks) -@router.post("", response_model=FiscalCertificateRead, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=FiscalCertificateRead, + status_code=status.HTTP_201_CREATED, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 409: {"description": "`detail.code`=`certificate_upload_conflict` -- upload concorrente venceu a corrida"}, + 413: {"description": f"Certificado excede o tamanho máximo permitido ({_MAX_PFX_UPLOAD_BYTES} bytes)"}, + 422: { + "description": ( + "PFX inválido/senha incorreta, CNPJ do certificado diverge do declarado, " + "certificado vencido ou ainda não vigente" + ) + }, + }, +) async def upload_certificate_endpoint( tenant_ref: str = Query(..., max_length=64), branch_ref: str = Query(..., max_length=64), @@ -77,7 +92,14 @@ async def upload_certificate_endpoint( return FiscalCertificateRead.model_validate(certificate) -@router.get("", response_model=FiscalCertificateRead) +@router.get( + "", + response_model=FiscalCertificateRead, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 404: {"description": "Nenhum certificado vivo para este (tenant_ref, branch_ref) -- anti-oracle"}, + }, +) async def read_certificate_endpoint( tenant_ref: str = Query(..., max_length=64), branch_ref: str = Query(..., max_length=64), @@ -96,7 +118,14 @@ async def read_certificate_endpoint( return FiscalCertificateRead.model_validate(certificate) -@router.delete("", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "", + status_code=status.HTTP_204_NO_CONTENT, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 404: {"description": "Nenhum certificado vivo para este (tenant_ref, branch_ref) -- anti-oracle"}, + }, +) async def deactivate_certificate_endpoint( tenant_ref: str = Query(..., max_length=64), branch_ref: str = Query(..., max_length=64), diff --git a/src/fiscal_svc/emission/router.py b/src/fiscal_svc/emission/router.py index 3617a3c..7f0b1eb 100644 --- a/src/fiscal_svc/emission/router.py +++ b/src/fiscal_svc/emission/router.py @@ -12,8 +12,30 @@ from fiscal_svc.tenancy.models import Product router = APIRouter(prefix="/v1", tags=["emissao"]) +_ERRO_409 = { + "description": ( + "Conflito estruturado -- `detail.code` identifica a causa: " + "`fiscal_config_missing` (certificado/série ausentes ou certificado vencido), " + "`emitente_certificate_cnpj_mismatch` (CNPJ do emitente diverge do certificado da filial), " + "`pagamento_total_diverge` (Σ vPag ≠ vNF), " + "`fiscal_document_conflict` (colisão de chave de acesso)." + ), +} -@router.post("/emissoes", response_model=FiscalDocumentRead) + +@router.post( + "/emissoes", + response_model=FiscalDocumentRead, + status_code=status.HTTP_201_CREATED, + responses={ + 200: { + "description": "Idempotency-Key repetida -- documento já emitido devolvido (replay, created=False)", + "model": FiscalDocumentRead, + }, + 401: {"description": "API key ausente ou inválida"}, + 409: _ERRO_409, + }, +) async def emitir_documento_endpoint( payload: EmissaoRequest, response: Response, @@ -42,7 +64,14 @@ async def emitir_documento_endpoint( return FiscalDocumentRead.model_validate(document) -@router.get("/documentos/{document_id}", response_model=FiscalDocumentRead) +@router.get( + "/documentos/{document_id}", + response_model=FiscalDocumentRead, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 404: {"description": "Documento fiscal não encontrado (ou de outro product/tenant -- anti-oracle)"}, + }, +) async def get_fiscal_document_endpoint( document_id: uuid.UUID, product: Product = Depends(require_product), @@ -56,7 +85,14 @@ async def get_fiscal_document_endpoint( return FiscalDocumentRead.model_validate(document) -@router.get("/documentos/{document_id}/xml") +@router.get( + "/documentos/{document_id}/xml", + responses={ + 200: {"content": {"application/xml": {}}}, + 401: {"description": "API key ausente ou inválida"}, + 404: {"description": "Documento fiscal não encontrado (ou de outro product/tenant -- anti-oracle)"}, + }, +) async def get_fiscal_document_xml_endpoint( document_id: uuid.UUID, product: Product = Depends(require_product), @@ -70,7 +106,11 @@ async def get_fiscal_document_xml_endpoint( return Response(content=document.xml_assinado, media_type="application/xml") -@router.get("/documentos", response_model=list[FiscalDocumentRead]) +@router.get( + "/documentos", + response_model=list[FiscalDocumentRead], + responses={401: {"description": "API key ausente ou inválida"}}, +) async def list_fiscal_documents_endpoint( tenant_ref: str | None = Query(default=None, max_length=64), branch_ref: str | None = Query(default=None, max_length=64), diff --git a/src/fiscal_svc/series/router.py b/src/fiscal_svc/series/router.py index 6482cf3..9a4c6e8 100644 --- a/src/fiscal_svc/series/router.py +++ b/src/fiscal_svc/series/router.py @@ -13,7 +13,20 @@ from fiscal_svc.tenancy.models import Product router = APIRouter(prefix="/v1/series", tags=["series"]) -@router.post("", response_model=FiscalSeriesRead, status_code=status.HTTP_201_CREATED) +@router.post( + "", + response_model=FiscalSeriesRead, + status_code=status.HTTP_201_CREATED, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 409: { + "description": ( + "`detail.code`=`duplicate_fiscal_series` -- já existe uma série fiscal " + "(live ou soft-deletada) para este (tenant_ref, branch_ref, document_model, serie)" + ) + }, + }, +) async def create_fiscal_series_endpoint( payload: FiscalSeriesCreate, product: Product = Depends(require_product), @@ -26,7 +39,11 @@ async def create_fiscal_series_endpoint( return FiscalSeriesRead.model_validate(series) -@router.get("", response_model=list[FiscalSeriesRead]) +@router.get( + "", + response_model=list[FiscalSeriesRead], + responses={401: {"description": "API key ausente ou inválida"}}, +) async def list_fiscal_series_endpoint( tenant_ref: str = Query(..., max_length=64), branch_ref: str = Query(..., max_length=64), @@ -37,7 +54,15 @@ async def list_fiscal_series_endpoint( return [FiscalSeriesRead.model_validate(series) for series in series_list] -@router.patch("/{series_id}", response_model=FiscalSeriesRead) +@router.patch( + "/{series_id}", + response_model=FiscalSeriesRead, + responses={ + 401: {"description": "API key ausente ou inválida"}, + 404: {"description": "Série fiscal não encontrada (ou de outro product -- anti-oracle)"}, + 409: {"description": "`detail.code`=`fiscal_series_number_regression`"}, + }, +) async def update_fiscal_series_endpoint( series_id: uuid.UUID, payload: FiscalSeriesPatch,