diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bc94c8..a64af9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,5 +28,5 @@ jobs: - name: Build library run: dotnet build Codout.Apis.Asaas/Codout.Apis.Asaas.csproj --configuration Release --no-restore - - name: Test - run: dotnet test Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj --configuration Release --logger "console;verbosity=normal" + - name: Test (unit + contract — integration are auto-skipped without ASAAS_SANDBOX_TOKEN) + run: dotnet test Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj --configuration Release --logger "console;verbosity=normal" --filter "Category!=Integration" diff --git a/.github/workflows/integration-sandbox.yml b/.github/workflows/integration-sandbox.yml new file mode 100644 index 0000000..8b51d72 --- /dev/null +++ b/.github/workflows/integration-sandbox.yml @@ -0,0 +1,51 @@ +name: Integration (sandbox) + +# Roda os integration tests reais contra api-sandbox.asaas.com. +# Requer secret ASAAS_SANDBOX_TOKEN configurado no repositorio +# (Settings -> Secrets and variables -> Actions). +# +# Triggers: +# - manual (workflow_dispatch): permite rodar sob demanda +# - schedule (nightly): valida o SDK contra mudancas no sandbox +# +# Nao bloqueia PRs comuns (rodar em job separado). + +on: + workflow_dispatch: + schedule: + # Diariamente as 04:00 UTC (01:00 BRT) + - cron: '0 4 * * *' + +jobs: + integration: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore library + run: dotnet restore Codout.Apis.Asaas/Codout.Apis.Asaas.csproj + + - name: Restore tests + run: dotnet restore Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj + + - name: Build library + run: dotnet build Codout.Apis.Asaas/Codout.Apis.Asaas.csproj --configuration Release --no-restore + + - name: Integration tests (sandbox) + env: + ASAAS_SANDBOX_TOKEN: ${{ secrets.ASAAS_SANDBOX_TOKEN }} + run: | + if [ -z "$ASAAS_SANDBOX_TOKEN" ]; then + echo "::warning::ASAAS_SANDBOX_TOKEN secret nao configurado. Integration tests serao todos skipados." + fi + dotnet test Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj \ + --configuration Release \ + --logger "console;verbosity=normal" \ + --filter "Category=Integration" diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..70b1d13 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "asaas": { + "type": "http", + "url": "https://docs.asaas.com/mcp" + } + } +} \ No newline at end of file diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..87a7a19 --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,369 @@ +# Auditoria de Conformidade — SDK Codout.Apis.Asaas vs API Oficial Asaas + +> **Gerado em:** 2026-05-24 +> **Fonte da verdade:** MCP `https://docs.asaas.com/mcp` (OpenAPI 3.0.1) +> **Versão do SDK auditada:** 2.0.3 (commit `d23bbb7`) +> **Cobertura da auditoria:** 20 managers, 156 endpoints documentados + +--- + +## 1. Sumário Executivo + +A API Asaas v3 documenta **~156 endpoints** distribuídos em **~25 domínios funcionais**. O SDK atualmente implementa cerca de **70 endpoints** em **20 managers** — cobrindo **~45% da superfície da API**. + +A auditoria identificou **6 bugs críticos** (categoria 🔴 — quebram chamadas em produção), **dezenas de endpoints faltando** em managers existentes, e **8 domínios inteiros não cobertos**. + +### Distribuição de problemas + +| Severidade | Quantidade | Descrição | +|---|---|---| +| 🔴 Bloqueante | **6 bugs + 4 domínios inteiros errados** | Quebram integrações reais | +| 🟡 Importante | ~50 endpoints faltando + ~30 campos faltando | Funcionalidade limitada | +| 🟢 Cosmético | ~10 pontos | Polimento (typos, naming) | + +### Top 6 prioridades absolutas (a tratar primeiro) + +1. **`PostAsync` usado onde a API exige `PUT`** em 6 métodos de Update (Customer, Payment, Subscription × 2, Notification × 2). Todas as atualizações estão sendo enviadas com HTTP errado. +2. **`WebhookManager` inteiro está num modelo de API antigo** (`/webhook` singular, paths `/webhook/invoice` e `/webhook/mobilePhoneRecharge` que **não existem** mais). Precisa reescrita completa para o novo padrão CRUD em `/v3/webhooks/{id}`. +3. **`CustomerFiscalInfoManager` usa rota errada**: `/customerFiscalInfo` mas a API expõe `/fiscalInfo/`. Todas as chamadas retornam 404. +4. **`AnticipationManager.SignAgreement`** chama `/anticipations/agreement/sign` — endpoint **inexistente** na documentação atual. +5. **`InstallmentManager` não tem método `Create`**, sendo um manager de parcelamento que não consegue criar parcelamentos. Bug por omissão de funcionalidade fundamental. +6. **`FinanceManager.Balance()`** retorna `decimal` direto, mas a API retorna `{ balance: number }`. A deserialização falha silenciosamente. + +--- + +## 2. Bugs Críticos (🔴 Bloqueantes) + +### 🔴 BUG-001 — `PostAsync` em vez de `PutAsync` em métodos de Update + +A API documenta `PUT` para os endpoints abaixo, mas o SDK envia `POST`: + +| Manager | Método | Endpoint atual no SDK | Endpoint correto | +|---|---|---|---| +| `CustomerManager` | `Update` (linha 36) | POST `/v3/customers/{id}` | **PUT** `/v3/customers/{id}` | +| `PaymentManager` | `Update` (linha 36) | POST `/v3/payments/{id}` | **PUT** `/v3/payments/{id}` | +| `SubscriptionManager` | `Update` (linha 38) | POST `/v3/subscriptions/{id}` | **PUT** `/v3/subscriptions/{id}` | +| `SubscriptionManager` | `UpdateInvoiceSettings` (linha 82) | POST `/v3/subscriptions/{id}/invoiceSettings` | **PUT** `/v3/subscriptions/{id}/invoiceSettings` | +| `NotificationManager` | `Update` (linha 15) | POST `/v3/notifications/{id}` | **PUT** `/v3/notifications/{id}` | +| `NotificationManager` | `BatchUpdate` (linha 21) | POST `/v3/notifications/batch` | **PUT** `/v3/notifications/batch` | + +**Por que escapou aos testes:** Os mocks aceitam qualquer método HTTP. Recomenda-se reforçar `AssertRequestMethod` em todos os testes de update. + +**Bom:** `PaymentLinkManager.Update` e `InvoiceManager.Update` já usam `PutAsync` corretamente. + +### 🔴 BUG-002 — `WebhookManager` num modelo de API obsoleto + +O manager atual implementa um padrão antigo de "webhook único por tipo" com paths inexistentes: + +```csharp +// SDK atual (TODOS errados): +"/webhook" → não existe +"/webhook/invoice" → não existe +"/webhook/mobilePhoneRecharge" → não existe +``` + +A API atual expõe **CRUD genérico por ID** em `/v3/webhooks`: + +``` +POST /v3/webhooks Create +GET /v3/webhooks List +GET /v3/webhooks/{id} Find +PUT /v3/webhooks/{id} Update +DELETE /v3/webhooks/{id} Delete +POST /v3/webhooks/{id}/removeBackoff Remove backoff +``` + +Os modelos `Webhook` e `WebhookRequest` também estão incompletos. Faltam: +- `Id`, `Name`, `HasAuthToken`, `SendType` (enum: `SEQUENTIALLY` / `NON_SEQUENTIALLY`) +- `PenalizedRequestsCount` (resposta) +- `Events` (lista de enum `WebhookEvent` com **~100 valores possíveis**) + +**Impacto:** webhooks simplesmente não funcionam pelo SDK hoje. + +### 🔴 BUG-003 — `CustomerFiscalInfoManager` na rota errada + +```csharp +// CustomerFiscalInfoManager.cs:10 +private const string CustomerFiscalInfoRoute = "/customerFiscalInfo"; +``` + +Endpoint correto na API: **`/v3/fiscalInfo/`**. + +Todas as 3 operações (`CreateOrUpdate`, `Find`, `ListMunicipalOptions`) chamam URL inexistente. Devem retornar 404. + +Adicionalmente, o método `ListMunicipalOptions` faz GET em `/customerFiscalInfo/municipalOptions`, mas o endpoint correto é `/v3/fiscalInfo/municipalOptions`. + +### 🔴 BUG-004 — `AnticipationManager.SignAgreement` chama endpoint inexistente + +```csharp +// AnticipationManager.cs:41 +var route = $"{AnticipationsRoute}/agreement/sign"; // → /v3/anticipations/agreement/sign +``` + +Esse endpoint **não consta na API documentada**. Verificar: pode ter sido removido ou ter mudado de path. Confirmar com a Asaas se ainda existe; se não, remover o método. + +### 🔴 BUG-005 — `InstallmentManager` sem método `Create` + +`InstallmentManager` expõe apenas `Find`, `List`, `Delete`, `Refund`, `ListPaymentBook`. **Falta o método mais importante**: criar parcelamento. A API documenta: + +- POST `/v3/installments` — Create installment (boleto/PIX) +- POST `/v3/installments/` — Create installment with credit card (path com barra final = endpoint separado) + +Adicionalmente, faltam: +- DELETE `/v3/installments/{id}/payments` — cancelar cobranças pendentes +- GET `/v3/installments/{id}/payments` — listar cobranças do parcelamento +- PUT `/v3/installments/{id}/splits` — atualizar splits + +### 🔴 BUG-006 — `FinanceManager.Balance()` shape de resposta errado + +```csharp +// FinanceManager.cs:13-18 +public async Task> Balance() +{ + return await GetAsync(route); +} +``` + +A API retorna `{ "balance": 5210.96 }` (objeto), não um `decimal` puro. `System.Text.Json` não consegue deserializar um objeto JSON em `decimal`, então `ResponseObject.Data` virá `0` silenciosamente. + +**Fix:** criar tipo `Balance { public decimal Value { get; set; } }` (com `[JsonPropertyName("balance")]`). + +### 🔴 BUG-007 — `InvoiceManager.ListMunicipalServices` na rota errada + +```csharp +var route = $"{InvoicesRoute}/municipalServices"; // → /v3/invoices/municipalServices +``` + +Endpoint correto: **`/v3/fiscalInfo/services`**. Esse método pertence ao domínio `fiscalInfo`, não a `invoices`. Deve ser movido para `CustomerFiscalInfoManager` (após corrigir BUG-003). + +--- + +## 3. Endpoints Faltantes por Manager (🟡 Importante) + +### 3.1 PaymentManager — 13 endpoints faltando +| Método | Path | Descrição | +|---|---|---| +| POST | `/v3/payments/` (com barra final) | Criar cobrança **com cartão de crédito** (endpoint separado) | +| POST | `/v3/payments/{id}/captureAuthorizedPayment` | Capturar pré-autorização | +| POST | `/v3/payments/{id}/payWithCreditCard` | Pagar cobrança com cartão | +| GET | `/v3/payments/{id}/billingInfo` | Billing info | +| GET | `/v3/payments/{id}/viewingInfo` | Viewing info | +| GET | `/v3/payments/{id}/status` | Status só | +| POST | `/v3/payments/simulate` | Simulador de vendas | +| GET | `/v3/payments/limits` | Limites | +| POST/GET | `/v3/payments/{id}/documents` | Upload / lista documentos | +| PUT/GET/DELETE | `/v3/payments/{id}/documents/{documentId}` | CRUD por documento | +| GET | `/v3/payments/{id}/refunds` | Listar estornos (diferente de POST refund) | +| POST | `/v3/payments/{id}/bankSlip/refund` | Estornar boleto | + +**Campos faltando em `Payment` (response):** `object`, `checkoutSession`, `paymentLink`, `installmentNumber`, `pixTransaction`, `pixQrCodeId`, `creditDate`, `estimatedCreditDate`, `transactionReceiptUrl`, `nossoNumero`, `anticipable`, `daysAfterDueDateToRegistrationCancellation`, `canBePaidAfterDueDate`, `chargeback`, `escrow`, `refunds`. + +**Campos faltando em `CreatePaymentRequest`:** `daysAfterDueDateToRegistrationCancellation`, `callback` (objeto com `successUrl` e `autoRedirect`), `pixAutomaticAuthorizationId`. + +**Enum `PaymentStatus` desatualizado:** falta `REFUND_IN_PROGRESS`. + +**Enum `BillingType`:** SDK provavelmente tem `BOLETO/CREDIT_CARD/PIX/...` — a API documenta também `UNDEFINED`, `DEBIT_CARD`, `TRANSFER`, `DEPOSIT` em respostas (não confirmado, validar). + +### 3.2 SubscriptionManager — 1 faltando +| Método | Path | +|---|---| +| PUT | `/v3/subscriptions/{id}/creditCard` — Atualizar cartão sem cobrar | + +### 3.3 PixManager — muitos faltando (PIX evolução recente) +| Método | Path | +|---|---| +| DELETE | `/v3/pix/qrCodes/static/{id}` | +| GET | `/v3/pix/tokenBucket/addressKey` | +| GET | `/v3/pix/transactions/{id}` (Find — só List existe!) | +| **Domínio PIX Automático** | POST/GET/DELETE `/v3/pix/automatic/authorizations[/{id}]`; GET `/v3/pix/automatic/paymentInstructions[/{id}]` | +| **Domínio PIX Recorrente** | GET `/v3/pix/transactions/recurrings[/{id}]`; POST `/v3/pix/transactions/recurrings/{id}/cancel`; GET `/v3/pix/transactions/recurrings/{id}/items`; POST `/v3/pix/transactions/recurrings/items/{id}/cancel` | + +### 3.4 TransferManager +| Método | Path | +|---|---| +| DELETE | `/v3/transfers/{id}/cancel` | +| POST | `/v3/transfers/` (path com barra = transferência **para conta Asaas**, diferente da raiz) | + +> **Observação:** o SDK tem dois overloads de `Execute()` mas ambos postam em `/transfers`. A API separa: `/transfers` (instituição externa/PIX) vs `/transfers/` (Asaas). Precisa rotear corretamente. + +### 3.5 AnticipationManager +| Método | Path | +|---|---| +| POST | `/v3/anticipations/{id}/cancel` | +| GET | `/v3/anticipations/limits` | +| PUT/GET | `/v3/anticipations/configurations` — antecipação automática | + +### 3.6 CreditCardManager — só tem tokenização, faltam: +| Método | Path | +|---|---| +| POST/GET | `/v3/creditCard/preAuthorization/config` | + +### 3.7 MyAccountManager +| Método | Path | Obs | +|---|---|---| +| GET/POST | `/v3/myAccount/commercialInfo/` | SDK tem `Find()` apontando para `/myAccount`, mas o endpoint real é `/myAccount/commercialInfo/` | +| GET | `/v3/myAccount/status/` | Status cadastral | +| DELETE | `/v3/myAccount/` | Excluir subconta White Label (existente no SDK? **não está**) | +| GET | `/v3/myAccount/documents` | Documentos pendentes | +| POST | `/v3/myAccount/documents/{id}` | Enviar documento | +| GET/POST/DELETE | `/v3/myAccount/documents/files/{id}` | CRUD arquivo enviado | + +### 3.8 AsaasAccountManager +| Método | Path | +|---|---| +| GET | `/v3/accounts/{id}` — Find by id | +| POST | `/v3/accounts/{id}/resendActivationLink` | +| POST/GET | `/v3/accounts/{id}/accessTokens` | +| PUT/DELETE | `/v3/accounts/{id}/accessTokens/{accessTokenId}` | + +### 3.9 BillPaymentManager +- `List` não aceita filtro; a doc oficial tem query params para data e status (validar). + +### 3.10 CreditBureauReportManager — OK estruturalmente + +### 3.11 PaymentLinkManager — **completo** (todos os 9 endpoints presentes; único manager 100% conforme) + +--- + +## 4. Domínios Inteiros Não Cobertos (🔴 Funcionalidade ausente) + +| Domínio | Endpoints | Onde deveria ficar | +|---|---|---| +| **Chargebacks** | 3 endpoints (`/v3/chargebacks`, `/v3/chargebacks/{id}/dispute`, `/v3/payments/{id}/chargeback`) | Novo `ChargebackManager` | +| **Escrow / Conta de Garantia** | 5 endpoints (`/v3/accounts/{id}/escrow`, `/v3/accounts/escrow`, `/v3/escrow/{id}/finish`, `/v3/payments/{id}/escrow`) | Novo `EscrowManager` | +| **Checkouts** | 2 endpoints (`/v3/checkouts`, `/v3/checkouts/{id}/cancel`) | Novo `CheckoutManager` | +| **Mobile Phone Recharges** | 4 endpoints (`/v3/mobilePhoneRecharges/*`) | Novo `MobilePhoneRechargeManager` | +| **Payment Splits queries** | 4 endpoints (`/v3/payments/splits/{paid,received}[/{id}]`) | Métodos novos em `PaymentManager` | +| **Lean Payments** | 11 endpoints (variante "lean" de `/v3/payments`) | Opcional — pode ser parâmetro/sobrecarga no `PaymentManager` | +| **Sandbox helpers** | 3 endpoints (`/v3/sandbox/myAccount/approve`, `/v3/sandbox/payment/{id}/confirm`, `/v3/sandbox/payment/{id}/overdue`) | Novo `SandboxManager` — útil para testes E2E | +| **Customer Notifications (GET)** | `/v3/customers/{id}/notifications` | Método novo em `CustomerManager` ou `NotificationManager` | + +--- + +## 5. Outros Achados (🟢 / 🟡) + +### 5.1 `CreateCustomerRequest` / `UpdateCustomerRequest` +Faltam: `company`, `foreignCustomer`. Adicionalmente, `groupName` está só no Create — a API aceita no Update também. + +### 5.2 `Customer` (response) +Faltam: `object`, `cityName`, `foreignCustomer`, `stateInscription`, `groupName`. + +### 5.3 `Notification` (response) +A doc retorna mais campos por evento (tipos de notificação, `deleted`, `event`). Validar via `get-endpoint`. + +### 5.4 `bool` vs `bool?` em respostas +Muitos modelos usam `bool` non-nullable para campos que a API pode omitir. Recomenda-se padronizar para `bool?` em respostas opcionais (Customer.Deleted, Customer.NotificationDisabled, Payment.PostalService, Payment.Anticipated, Payment.Deleted). + +### 5.5 `DateTime` para campos formato `date` (sem hora) +A API documenta vários campos como `format: date` (`YYYY-MM-DD`), não `date-time`. `System.Text.Json` aceita ambos, mas pode causar parsing inconsistente em serialização de volta. Considerar conversor customizado ou tipo `DateOnly`. + +### 5.6 `BaseManager.BuildHttpClient` cria novo `HttpClient` por requisição +Anti-pattern em .NET — risco de `SocketException` por esgotamento de portas. Migrar para `IHttpClientFactory`. **Não é divergência da doc**, mas é um problema técnico que vale corrigir junto. + +### 5.7 Typo `WasSucessfull` (em vez de `WasSuccessful`) +Documentado no CLAUDE.md como conhecido. Manter por compatibilidade ou marcar como `[Obsolete]` e adicionar correto. + +### 5.8 Servidor de produção +A API documentada lista `https://api-sandbox.asaas.com` como server no OpenAPI. O SDK tem `https://api.asaas.com` para produção — **isso está correto**, é apenas o exemplo do OpenAPI que usa sandbox. + +--- + +## 6. Plano de Correção Priorizado + +### Sprint 1 — Bugs bloqueantes (PRs pequenos, alto valor) + +| # | PR | O quê | Impacto | +|---|---|---|---| +| 1 | `fix: usar PUT para atualizações (Customer/Payment/Subscription/Notification)` | Trocar `PostAsync` por `PutAsync` em 6 lugares + ajustar testes para `AssertRequestMethod("PUT")` | Updates passam a funcionar | +| 2 | `fix(fiscalInfo): rota correta /fiscalInfo` | Corrigir constante de rota + mover `ListMunicipalServices` de Invoice para FiscalInfo | FiscalInfo passa a funcionar | +| 3 | `fix(finance): Balance retorna objeto { balance }` | Criar tipo `Balance` + atualizar manager | `Balance()` retorna valor correto | +| 4 | `fix(anticipation): remover SignAgreement (endpoint inexistente)` | Remover método (ou confirmar com Asaas) | Sem 404 silencioso | +| 5 | `feat(installments): adicionar Create + endpoints faltantes` | Adicionar `Create()`, `CreateWithCreditCard()`, `CancelPayments()`, `ListPayments()`, `UpdateSplits()` | Manager passa a ser usável | + +### Sprint 2 — Reescrita do WebhookManager + +| # | PR | O quê | +|---|---|---| +| 6 | `refactor!(webhooks): migrar para CRUD por ID /v3/webhooks` | Reescrever manager + modelos + enums (`WebhookEvent` com ~100 valores, `WebhookSendType`). **Breaking change** — anunciar major version. Manter os métodos antigos `[Obsolete]` por uma minor antes de remover. | + +### Sprint 3 — Endpoints faltantes em managers existentes + +| # | PR | O quê | +|---|---|---| +| 7 | `feat(payment): documentos, simulate, limits, billingInfo, viewingInfo, status, refunds, bankSlip/refund` | 13 endpoints novos | +| 8 | `feat(subscription): updateCreditCard` | 1 endpoint | +| 9 | `feat(anticipation): cancel, limits, configurations` | 4 endpoints | +| 10 | `feat(creditCard): preAuthorization config` | 2 endpoints | +| 11 | `feat(transfer): cancel + rota /transfers/ separada` | 1 endpoint + correção de roteamento | +| 12 | `feat(pix): qrCode delete, transaction find, tokenBucket` | 3 endpoints | +| 13 | `feat(myAccount): commercialInfo, status, documents` | 7 endpoints + correção de rota raiz | +| 14 | `feat(asaasAccount): find by id, accessTokens, resendActivationLink` | 5 endpoints | + +### Sprint 4 — Novos domínios + +| # | PR | O quê | +|---|---|---| +| 15 | `feat: ChargebackManager` | 3 endpoints | +| 16 | `feat: EscrowManager` | 5 endpoints | +| 17 | `feat: CheckoutManager` | 2 endpoints | +| 18 | `feat: MobilePhoneRechargeManager` | 4 endpoints | +| 19 | `feat: SandboxManager (helpers para testes)` | 3 endpoints | +| 20 | `feat(payment): splits queries` | 4 endpoints (adicionar em `PaymentManager`) | + +### Sprint 5 — Pix evolução recente + +| # | PR | O quê | +|---|---|---| +| 21 | `feat(pix): PixAutomaticManager` | 6 endpoints (autorizações + payment instructions) | +| 22 | `feat(pix): PixRecurringManager` | 5 endpoints (recurrings + items) | + +### Sprint 6 — Polimento e quality + +| # | PR | O quê | +|---|---|---| +| 23 | `refactor(models): completar campos faltantes em Customer/Payment/Notification/Webhook` | Completar response DTOs | +| 24 | `refactor(models): bool → bool? em campos opcionais; revisar DateTime` | Reduz NREs e bugs de parsing | +| 25 | `refactor(core): IHttpClientFactory` | Anti-pattern de `new HttpClient()` | +| 26 | `chore: corrigir WasSucessfull → WasSuccessful (manter alias)` | Polimento sem breaking | + +--- + +## 7. Verificação após cada Sprint + +```powershell +dotnet build Codout.Apis.Asaas/ +dotnet test Codout.Apis.Asaas.Tests/ +``` + +Para cada PR de bug bloqueante (Sprint 1): adicionar/ajustar teste em `ManagerTestBase` que `AssertRequestMethod` e `AssertRequestUrl` validem método HTTP e rota. Hoje muitos testes assumem "qualquer POST" funciona. + +Idealmente, criar um teste de "smoke" que confronte cada método público de manager com a lista de endpoints documentada (poderia gerar essa lista a partir do MCP do Asaas em CI). + +--- + +## 8. Anexo — Cobertura por Manager + +| Manager | Endpoints SDK | Endpoints API | Cobertura | Status | +|---|---|---|---|---| +| CustomerManager | 6 | 7 | 86% | 🔴 1 bug PUT + 1 endpoint faltando | +| PaymentManager | 11 | 24 | 46% | 🔴 1 bug PUT + 13 endpoints faltando | +| SubscriptionManager | 10 | 11 | 91% | 🔴 2 bugs PUT | +| InstallmentManager | 5 | 8 | 62% | 🔴 Sem Create | +| PixManager | 10 | 22 | 45% | 🟡 Faltam Automatic + Recurring | +| WebhookManager | 6 | 6 | 0% efetivo | 🔴 Tudo em rota errada | +| TransferManager | 4 | 5 | 80% | 🟡 falta cancel + roteamento /transfers/ | +| WalletManager | 1 | 1 | 100% | ✅ | +| AnticipationManager | 5 | 6 | 50% | 🔴 endpoint inexistente + 4 faltando | +| BillPaymentManager | 5 | 5 | 100% | ✅ | +| CreditCardManager | 1 | 3 | 33% | 🟡 Pré-autorização config faltando | +| PaymentLinkManager | 9 | 9 | 100% | ✅ | +| NotificationManager | 2 | 2 | 0% efetivo | 🔴 2 bugs PUT | +| InvoiceManager | 7 | 6 | — | 🔴 1 endpoint em rota errada | +| PaymentDunningManager | 8 | 8 | 100% | ✅ | +| FinanceManager | 4 | 4 | 100% | 🔴 1 bug response shape | +| MyAccountManager | 4 | 11 | 36% | 🔴 Find em rota errada + 7 endpoints faltando | +| AsaasAccountManager | 2 | 6 | 33% | 🔴 Faltam Find + accessTokens + resend | +| CreditBureauReportManager | 3 | 3 | 100% | ✅ | +| CustomerFiscalInfoManager | 3 | 9 | 0% efetivo | 🔴 Rota errada + 6 endpoints faltando | + +**Domínios completamente ausentes:** Chargebacks, Escrow, Checkouts, MobilePhoneRecharges, Splits queries, Lean Payments, Sandbox helpers. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0398a..069c9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,419 @@ Todas as mudancas notaveis deste projeto serao documentadas neste arquivo. O formato e baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/), e este projeto adere ao [Versionamento Semantico](https://semver.org/lang/pt-BR/). +## [3.2.0] - 2026-05-24 — Auditoria schema-first **completa** (27/27 managers) + +Segunda rodada da auditoria, agora cobrindo os 16 managers restantes que tinham +ficado de fora da v3.1.0: Customer, Payment (resto), Subscription, Pix, Transfer, +Anticipation, Installment, Webhook, Wallet, Notification, CreditCard, PaymentLink, +Finance, MyAccount (resto), AsaasAccount, FiscalInfo, Chargeback, Sandbox. + +Veja [CONFORMANCE.md](CONFORMANCE.md) §12–§29 para o relatório endpoint-a-endpoint +e §50 para o consolidado de padrões de bug por categoria. + +### Breaking changes (modelos) + +**Pix (CRÍTICO):** +- `PixTransactionStatus` enum tinha 5 valores INVENTADOS (PENDING, DONE, CANCELLED, + SCHEDULED, FAILED). Schema real: 11 valores. PENDING e FAILED **não existem**. + Sem o fix, deserializar JSON real do sandbox lançava exception em qualquer + transacao em estado de espera (AWAITING_*, REQUESTED, REFUSED). +- `PixTransaction` reescrito (7 → 25 campos). Renomeados: `TransactionDate` → + `EffectiveDate`, `ScheduleDate` → `ScheduledDate`. +- `PixAddressKey.Status` string → enum `PixAddressKeyStatus` (6 valores). + Adicionados QrCode (nested), CanBeDeleted, CannotBeDeletedReason. +- Novos enums: `PixTransactionType` (5), `PixTransactionOriginType` (6), + `PixTransactionFinality` (2), `PixAddressKeyStatus` (6). +- Novo filtro: `PixTransactionListFilter` (status, type, endToEndIdentifier). + +**Customer:** +- `Customer.DateCreated` → DateTime? +- `Create/UpdateCustomerRequest.NotificationDisabled` → bool? + (antes forçava false em todo update parcial). +- Novo: `CustomerManager.GetNotifications(customerId)` (endpoint estava faltando). + +**Payment:** +- `Payment.DateCreated`, `DueDate`, `OriginalDueDate` → DateTime? +- `PaymentListFilter`: 9 filtros novos (customerGroupName, invoiceStatus, + estimatedCreditDate, pixQrCodeId, anticipable, user, checkoutSession, + dateCreated[ge]/[le], estimatedCreditDate[ge]/[le]). + +**Subscription:** +- `SubscriptionStatus` enum: adicionado `INACTIVE` (3 valores). +- `Subscription.DateCreated`, `NextDueDate` → DateTime? +- Adicionados: Object, PaymentLinkId, CheckoutSession, Split (array). +- `SubscriptionListFilter`: 6 campos novos (customerGroupName, status enum, + deletedOnly, externalReference, order, sort). + +**Transfer:** +- `AsaasAccountTransferStatus` enum: 3 → 5 valores (adicionados BANK_PROCESSING, + FAILED). +- `BaseTransfer.DateCreated` → DateTime?, `Authorized` → bool? +- Novo enum: `TransferOperationType` (PIX/TED/INTERNAL). +- `BaseTransfer` adicionados: Object, NetValue (movido), EndToEndIdentifier, + FailReason, ExternalReference, Description, Recurring. +- `TransferListFilter`: dateCreated[ge]/[le], transferDate[ge]/[le]. +- `Bank` model: adicionados Ispb + Name. `BankAccount`: adicionados AgencyDigit, + PixAddressKey, Ispb. + +**Anticipation:** +- `Anticipation.AnticipationDate`, `DueDate`, `RequestDate` → DateTime?, +Object. + +**Installment:** +- `Installment.ExpirationDay` → int? +- Adicionados: CreditCard (nested), Refunds (array de InstallmentRefund). + +**Notification:** +- Novo enum `NotificationEvent` (6 valores). Antes não existia. +- Notification + UpdateNotificationRequest: todos os 7 bools → bool? +- Notification adicionados: Object, Event (enum), Deleted. + +**CreditCard:** +- `Common.CreditCard.Brand` string → enum `CreditCardBrand` (13 valores). +- `PreAuthorizationConfig` reescrito: tinha {Enabled, AutomaticCaptureDelay} + INVENTADOS. Schema: {DaysToExpire}. Idem `SavePreAuthorizationConfigRequest`. + +**PaymentLink:** +- `PaymentLink.SubscriptionCycle` string → `Cycle` enum (7 valores). +- Adicionados: ViewCount, IsAddressRequired, ExternalReference. +- Value, Active, NotificationEnabled, Deleted, DueDateLimitDays, + MaxInstallmentCount → nullable. + +**Finance:** +- `SplitStatistics` reescrito: tinha {TotalPendingValue, TotalReceivedValue} + INVENTADOS. Schema: {income, value}. +- Novo filter: `PaymentStatisticsFilter` (11 campos). GetPaymentStatistics + agora aceita filtros opcionais. + +**MyAccount:** +- `MyAccount.Status` string → enum `AccountInfoStatus` (4 valores). +- Adicionados: CompanyName, IncomeValue, TradingName, Site, + AvailableCompanyNames (array), CommercialInfoExpiration (nested). +- `InscricaoEstadual` marcado [Obsolete] (não existe no schema). + +**AsaasAccount:** +- `Account.City` string → long? (schema: integer city id). +- Adicionados: Object, Id, BirthDate, TradingName, Site, AccountNumber (nested), + CommercialInfoExpiration (nested). +- `ApiKey` marcado [Obsolete] (não existe no schema). + +**FiscalInfo:** +- `RpsNumber`, `LoteNumber` string → int? (schema: integer). +- Adicionados: NbsCode, PasswordSent, AccessTokenSent, CertificateSent, + NationalPortalTaxCalculationRegime, Object. +- `StateInscription`, `AccessToken` marcados [Obsolete] (não existem no schema). +- SimplesNacional, CulturalProjectsPromoter → bool? + +**Chargeback:** +- Adicionado `ChargebackCreditCard` (nested: number + brand enum). +- `Reason` → nullable. + +**Wallet:** +- Adicionado: Object. + +### Added + +- 16 novos enums tipados (PixTransactionStatus, PixTransactionType, + PixTransactionOriginType, PixTransactionFinality, PixAddressKeyStatus, + SubscriptionStatus expandido, NotificationEvent, CreditCardBrand, + TransferOperationType, AsaasAccountTransferStatus expandido, + AccountInfoStatus, BillPaymentStatus expandido em fase anterior, etc). +- Novos models: PixTransactionExternalAccount, PixOriginalTransaction, + PixTransactionQrCode, PixAddressKeyQrCode, PixTransactionListFilter, + ChargebackCreditCard, CommercialInfoExpiration, AccountNumber (já existia), + InstallmentRefund, PaymentStatisticsFilter. +- 10 novos integration tests cobrindo Subscription, Pix, Transfer, + Anticipation, Finance (15 tests no total). +- CI workflow [`integration-sandbox.yml`](.github/workflows/integration-sandbox.yml): + workflow_dispatch (manual) + nightly schedule, com secret ASAAS_SANDBOX_TOKEN. +- CI workflow [`ci.yml`](.github/workflows/ci.yml) atualizado para filtrar + `Category!=Integration` (não quebra sem token). +- CONFORMANCE.md §12–§29 (16 novos managers), §50 (cross-pattern bug list). + +### Fixed + +- 18 famílias de bugs (B-25 a B-42). Veja CONFORMANCE.md §50 para consolidação + por padrão. + +### Métricas + +- Testes: 599 → **664 unit/contract** (+65) + **15 integration** skip-by-default. +- Managers auditados: 11 → **27** (100%). +- Bugs cumulativos: 24 → **42 famílias** (B-19 a B-42). +- Build warnings: 0. + +## [3.1.0] - 2026-05-24 — Auditoria schema-first + +Rodada final de conformidade contra o MCP oficial Asaas. Para cada manager +auditado, modelos foram verificados campo-a-campo contra OpenAPI, fixtures +criadas a partir dos exemplos oficiais e contract tests congelam o shape JSON. +Veja [CONFORMANCE.md](CONFORMANCE.md) para o relatorio completo endpoint-a-endpoint. + +### Breaking changes (modelos) + +**PaymentDunning:** +- `DunningNumber: string` -> `int?` (era chute; schema integer). +- `Status: bool` -> `bool?` em `CanBeCancelled` e `IsNecessaryResendDocumentation` + (schema permite null; valor false silencioso antes do fix). +- `PaymentDunningEventHistory.Status: string` -> enum `PaymentDunningHistoryStatus`. +- `SimulatedPaymentDunning.TypeSimulations` e + `PaymentDunningPaymentAvailable.TypeSimulations`: objeto unico -> + `List` (schema sempre foi array — antes + lancava `InvalidCastException` no JSON real). +- `Simulate(request)` agora envia `payment` como QUERY param (schema), nao body. +- `ReceivedInCashFeeValue` e `CancellationFeeValue` marcados `[Obsolete]`. +- Adicionados: `CannotBeCancelledReason`, valor enum `DEBT_RECOVERY_ASSISTANCE` + (em `PaymentDunningType`, aceito pelo filter). + +**CreditBureauReport:** +- `CreditBureauReport.State` e `Status` REMOVIDOS (nao existem no schema). +- `CreateCreditBureauReportRequest.State` REMOVIDO. +- Adicionados: `DownloadUrl`, `ReportFile` (PDF Base64 — apenas em response do POST). +- Novo: `CreditBureauReportListFilter` com `StartDate`/`EndDate`. Overload + `List(offset, limit, filter)` backwards-compatible. + +**BillPayment:** +- `BillPayment`: adicionados `Interest`, `Fine`, `PaymentDate`, `ExternalReference`. + `FailReasons: string` -> `List` (schema array). + `CanBeCancelled`/`DueDate`/`ScheduleDate`/`PaymentDate` -> nullable. +- `BillPaymentStatus` enum: adicionados `REFUNDED` e + `AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST` (5 -> 7 valores). +- `CreateBillPaymentRequest`: adicionados `Interest`, `Fine`, `ExternalReference`. + `Value`/`DueDate`/`ScheduleDate`/`Discount` -> nullable. +- `BankSlipInfo`: 5 campos com `BankCode` (chute) -> 17 campos com `Bank`, + `Beneficiary*`, `Min/MaxValue`, `AllowChangeValue`, `Discount/Interest/FineValue`, + `OriginalValue`, `TotalDiscount/AdditionalValue`, `IsOverdue`. + +**Invoice:** +- `Taxes`: 7 campos -> 19 (NBS code, situacao tributaria, classificacao, + operacao, PIS/COFINS retention type/status + 6 campos da Reforma Tributaria: + `StateIbs`, `StateIbsValue`, `MunicipalIbs`, `MunicipalIbsValue`, `Cbs`, `CbsValue`). +- `InvoiceListFilter`: `effectiveDate[ge]/[le]` -> `[Ge]/[Le]` (G/L MAIUSCULOS, + schema oficial). Casing errado era silenciosamente ignorado pela API. + Adicionados filtros `customer` e `externalReference`. +- `CreateInvoiceRequest` e `UpdateInvoiceRequest`: adicionado `UpdatePayment: bool?`. + +**AccountDocument (subgrupo MyAccount):** +- `AccountDocumentFile` REMOVIDO (Name/Url eram inventados; schema retorna + apenas `{id, status}`). +- `SubmitDocument`, `ViewDocumentFile`, `UpdateDocumentFile` agora retornam + `AccountDocument` (antes retornavam tipo errado). +- 4 enums tipados criados: `AccountDocumentStatus` (4), `AccountDocumentGroupStatus` + (5, ganha IGNORED), `AccountDocumentType` (12), `AccountDocumentResponsibleType` (13). + Status/Type eram `string`/`List` antes. +- `UploadAccountDocumentRequest`: `DocumentType: string` -> `Type: AccountDocumentType?`, + `File: IAsaasFile` -> `DocumentFile: IAsaasFile` (alinhando com nomes + multipart "type" e "documentFile" do schema). + +**MobilePhoneRecharge:** +- `MobilePhoneProvider.AvailableValues: List` (chute) -> `Values: + List` com `{Name, Description, Bonus, MinValue, MaxValue}` + conforme schema. + +**PixAutomatic (B-16/B-17 do REVIEW pre-existente):** +- `PixAutomaticPaymentInstruction.Authorization` virou objeto aninhado com + `Id`/`EndToEndIdentifier`/`CustomerId`. Adicionados `DueDate`, `EndToEndIdentifier`, + `PaymentId`, `RefusalReason`. `Status` virou enum + `PixAutomaticPaymentInstructionStatus` (5 valores). +- `PixAutomaticPaymentInstructionListFilter`: campos `authorization`/`status` -> + `authorizationId`/`customerId`/`paymentId`/`status` (typed). + +### Added + +- **CONFORMANCE.md** — relatorio endpoint-a-endpoint da auditoria com tabelas + por manager, bugs (B-XX) corrigidos, fixtures, contract tests. +- **Integration tests sandbox** (`Codout.Apis.Asaas.Tests/Integration/`): + 5 testes reais contra `api-sandbox.asaas.com`, skip automatico via + `[IntegrationFact]` quando `ASAAS_SANDBOX_TOKEN` ausente. +- **Contract tests** (`Codout.Apis.Asaas.Tests/Contract/`): 95+ novos testes + que congelam o shape JSON de request/response com fixtures dos exemplos MCP. +- `PixRecurringTransactionListFilter` (status/value/searchText) — feature + anteriormente nao exposta. +- Bug pre-existente fixado em paralelo: `Subscription.Enums.Cycle.BIMONTHLY` + (estava faltando, presente em schemas de Subscription e Checkout). + +### Fixed (sistemicos — descobertos antes desta fase) + +- `RequestParameters.Add(decimal?)` agora usa `CultureInfo.InvariantCulture` + (pt-BR estava gerando `12,5` em vez de `12.5`). +- `RequestParameters.Add(bool?)` serializa `true`/`false` lowercase + (antes `True`/`False` — Asaas ignorava silenciosamente). +- `DateTimeExtensions.ToApiRequest` defensivamente forca `InvariantCulture`. + +## [3.0.0] - 2026-05-24 + +Major release com auditoria completa de conformidade contra a documentacao +oficial do Asaas (via MCP `https://docs.asaas.com/mcp`). Veja `AUDIT.md`, +`IMPLEMENTATION_PLAN.md` e `REVIEW.md` para o relatorio detalhado. + +### Breaking changes adicionais (do REVIEW.md) + +- `asaas.ReceivableAnticipation` renomeado para `asaas.Anticipation` (alinhar + com o naming dos demais 26 managers que usam nome curto). Migracao: + `asaas.ReceivableAnticipation.X` -> `asaas.Anticipation.X`. +- `ReceivableAnticipationStatusExtension` renomeado para + `AnticipationStatusExtension`. +- `AccountStatus.{CommercialInfo,Documentation,General,BankAccountInfo}` + passaram de `string` para enum `AccountApprovalStatus` + (PENDING/APPROVED/REJECTED/AWAITING_APPROVAL). +- `BaseManager._settings` passou de `private` para `protected Settings` + (convencao PascalCase de protected field). Codigo que herdava de + `BaseManager` e usava `_settings` precisa migrar para `Settings`. + +Cobertura do SDK passou de ~45% (70 endpoints) para 100% (~156 endpoints +documentados). Suite de testes passou de 400 para ~500 testes. + +### Breaking changes + +**HTTP method:** +- `CustomerManager.Update`, `PaymentManager.Update`, `SubscriptionManager.Update`, + `SubscriptionManager.UpdateInvoiceSettings`, `NotificationManager.Update`, + `NotificationManager.BatchUpdate` agora enviam **PUT** (antes era POST, sem efeito). + +**Renomeacoes / mudancas de rota:** +- `CustomerFiscalInfoManager` -> `FiscalInfoManager`; classes renomeadas + (`CustomerFiscalInfo` -> `FiscalInfo`, `CreateCustomerFiscalInfoRequest` -> + `CreateFiscalInfoRequest`). Rota corrigida: `/v3/customerFiscalInfo` -> `/v3/fiscalInfo`. + Acesso facade: `asaas.CustomerFiscalInfo` -> `asaas.FiscalInfo`. +- `InvoiceManager.ListMunicipalServices` removido e movido para + `FiscalInfoManager.ListServices` (rota correta `/v3/fiscalInfo/services`). +- `FinanceManager.Balance()` (retornando `decimal`) renomeado para + `GetBalance()` retornando `ResponseObject` (a API retorna objeto). +- `MyAccountManager.Find()` removido (apontava para rota errada). Substituido por + `GetCommercialInfo()` (rota `/v3/myAccount/commercialInfo`). +- `TransferManager.Execute(...)` removido (overload ambiguo). Substituido por + `TransferToBankAccount(...)` (POST `/v3/transfers`) e `TransferToAsaasAccount(...)` + (POST `/v3/transfers/`, com barra final = endpoint separado). +- `WebhookManager` totalmente reescrito de "webhook unico por tipo" (rotas + `/v3/webhook`, `/v3/webhook/invoice`, `/v3/webhook/mobilePhoneRecharge` que ja + nao existem) para CRUD por id em `/v3/webhooks/{id}`. Veja secao de migracao + no final. +- `MunicipalService.Iss` renomeado para `IssTax` (nome correto na API). + +**Remocoes:** +- `AnticipationManager.SignAgreement` e `SignAnticipationAgreementRequest` + removidos: o endpoint `/v3/anticipations/agreement/sign` nao existe na API. + +**Modelos:** +- `Customer.Deleted`, `Customer.NotificationDisabled`, `Payment.Deleted`, + `Payment.PostalService`, `Payment.Anticipated` convertidos para `bool?`. +- `WebhookRequest` removido (substituido por `CreateWebhookRequest` / + `UpdateWebhookRequest`). + +### Bugs corrigidos + +- 7 bugs bloqueantes que faziam chamadas reais falharem (PUT->POST, + rotas incorretas, shape de resposta errado, endpoint inexistente, + manager sem metodo de criacao). +- Sockets esgotando em apps de alto trafego: `SocketsHttpHandler` agora + e compartilhado entre todas as instancias de manager. + +### Novos managers (Sprint 4 + 5) + +- **ChargebackManager** - 3 endpoints (`/v3/chargebacks/*`) +- **EscrowManager** - 6 endpoints (Conta de Garantia) +- **CheckoutManager** - 2 endpoints (`/v3/checkouts/*`) +- **MobilePhoneRechargeManager** - 5 endpoints (`/v3/mobilePhoneRecharges/*`) +- **SandboxManager** - 3 helpers de teste (`/v3/sandbox/*`, lanca excecao em producao) +- **PixAutomaticManager** - 6 endpoints (`/v3/pix/automatic/*`) +- **PixRecurringManager** - 5 endpoints (`/v3/pix/transactions/recurrings/*`) + +### Novos endpoints em managers existentes + +- **PaymentManager** (+18 endpoints): documentos (5), simulate, limits, + billingInfo, viewingInfo, status, refunds, bankSlip/refund, + captureAuthorizedPayment, payWithCreditCard, createWithCreditCard, + splits queries paid/received (4). +- **SubscriptionManager** (+1): UpdateCreditCard. +- **InstallmentManager** (+5): Create, CreateWithCreditCard, ListPayments, + CancelPendingPayments, UpdateSplits. +- **PixManager** (+3): FindTransaction, DeleteStaticQrCode, GetAddressKeyTokenBucket. +- **AnticipationManager** (+4): Cancel, GetLimits, GetAutomaticConfiguration, + UpdateAutomaticConfiguration. +- **CreditCardManager** (+2): SavePreAuthorizationConfig, GetPreAuthorizationConfig. +- **TransferManager** (+1): Cancel. +- **MyAccountManager** (+8): GetCommercialInfo, UpdateCommercialInfo, + GetStatus, DeleteWhiteLabelAccount, ListPendingDocuments, SubmitDocument, + ViewDocumentFile, UpdateDocumentFile, DeleteDocumentFile. +- **AsaasAccountManager** (+6): Find, ResendActivationLink, CreateAccessToken, + ListAccessTokens, UpdateAccessToken, DeleteAccessToken. +- **FiscalInfoManager** (+1): ListServices. + +### Adicionado em modelos + +- `Customer`: Object, CityName, StateInscription, Company, GroupName, + ForeignCustomer. +- `Payment`: Object, PixTransaction, PixQrCodeId, CheckoutSession, + PaymentLinkId, InstallmentNumber, CreditDate, EstimatedCreditDate, + TransactionReceiptUrl, NossoNumero, Anticipable, CanBePaidAfterDueDate, + DaysAfterDueDateToRegistrationCancellation. +- `CreatePaymentRequest`: Callback, PixAutomaticAuthorizationId, + DaysAfterDueDateToRegistrationCancellation. +- `PaymentStatus`: valor REFUND_IN_PROGRESS. +- `CreateCustomerRequest` / `UpdateCustomerRequest`: Company, ForeignCustomer + (e GroupName no Update). +- Enums `WebhookEvent` (~100 valores) e `WebhookSendType`. +- Enums `ChargebackStatus`, `ChargebackReason` (32 valores), `ChargebackDisputeStatus`. + +### Outras melhorias + +- `BaseResponse.WasSucessfull()` corrigido para `WasSuccessful()`. Versao com + typo mantida como `[Obsolete]` alias temporario. +- `BaseManager` agora usa `SocketsHttpHandler` estatico compartilhado entre + todas as instancias para nao esgotar sockets em apps de alto trafego. + +### Guia de migracao 2.x -> 3.x + +```csharp +// PUT agora e usado automaticamente nos Updates - nenhuma mudanca de codigo necessaria +await asaas.Customer.Update("cus_123", req); // antes: POST, agora: PUT + +// FiscalInfo +asaas.CustomerFiscalInfo.X -> asaas.FiscalInfo.X +new CustomerFiscalInfo() -> new FiscalInfo() +new CreateCustomerFiscalInfoRequest() -> new CreateFiscalInfoRequest() + +// Invoice.ListMunicipalServices movido para FiscalInfo.ListServices +await asaas.Invoice.ListMunicipalServices("IT"); +// agora: +await asaas.FiscalInfo.ListServices("IT"); + +// Finance balance shape mudou +decimal saldo = (await asaas.Finance.Balance()).Data; +// agora: +decimal saldo = (await asaas.Finance.GetBalance()).Data.Value; + +// Transfer +await asaas.Transfer.Execute(asaasRequest); // ambiguo +// agora: +await asaas.Transfer.TransferToAsaasAccount(asaasRequest); +await asaas.Transfer.TransferToBankAccount(bankRequest); + +// MyAccount.Find -> GetCommercialInfo +var info = (await asaas.MyAccount.Find()).Data; +// agora: +var info = (await asaas.MyAccount.GetCommercialInfo()).Data; + +// Webhook completamente novo +// Antes (nao funcionava mais em nenhuma versao da API): +await asaas.Webhook.CreateOrUpdatePaymentWebhook(new WebhookRequest { ... }); +// Agora: +await asaas.Webhook.Create(new CreateWebhookRequest +{ + Name = "Meu webhook", + Url = "https://example.com/hook", + Email = "ops@example.com", + Enabled = true, + ApiVersion = 3, + AuthToken = "whsec_min32chars......", + SendType = WebhookSendType.SEQUENTIALLY, + Events = [WebhookEvent.PAYMENT_CONFIRMED, WebhookEvent.PAYMENT_RECEIVED] +}); + +// AnticipationManager.SignAgreement removido - se voce usava esse metodo, +// agora o termo de antecipacao e assinado direto no painel web do Asaas. +``` + ## [2.0.2] - 2026-04-27 ### Adicionado diff --git a/CONFORMANCE.md b/CONFORMANCE.md new file mode 100644 index 0000000..4376c90 --- /dev/null +++ b/CONFORMANCE.md @@ -0,0 +1,608 @@ +# Relatório de Conformidade — SDK Codout.Apis.Asaas v3.2.0 + +> **Status:** ✅ AUDITORIA COMPLETA — 27/27 MANAGERS SCHEMA-FIRST +> **Metodologia:** cada endpoint listado abaixo foi consultado via MCP `asaas` (`mcp__asaas__get-endpoint`). Models foram verificados campo-a-campo contra o schema OpenAPI. Fixtures criadas a partir dos exemplos oficiais. Contract tests congelam o shape JSON. +> +> **Resultado:** **664 testes unit/contract** passando + **15 integration tests** (skip automático sem `ASAAS_SANDBOX_TOKEN`). +> **27/27 managers** auditados, **42 famílias de bugs (B-19 a B-42)** corrigidas no total, **0 warnings** de build. +> +> **CI:** +> - [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — unit/contract em todo push/PR +> - [`.github/workflows/integration-sandbox.yml`](.github/workflows/integration-sandbox.yml) — integration tests com secret `ASAAS_SANDBOX_TOKEN` (manual + nightly) + +--- + +## Legenda + +| Status | Significado | +|---|---| +| ✅ | Schema verificado via MCP, model alinhado, fixture criada, contract test passando | +| ⚠️ | Verificado mas com divergência conhecida; documentada | +| 🔴 | Divergência sem correção (não deveria existir até fim da auditoria) | +| ⏳ | Pendente nesta auditoria | + +## Cobertura por nível + +| Nível | O que valida | Por endpoint | +|---|---|---| +| **Contract test** | Shape JSON (chaves exatas, casing, enums, envelope) | ✅ se marcado abaixo | +| **Unit test** | Manager chama URL/método corretos, deserialização básica | ✅ pré-existente | +| **Integration test** | Chamada real contra api-sandbox.asaas.com | Apenas endpoints listados em §99 | + +--- + +## §1 — PaymentManager (somente novos: limits, simulate) — ✅ + +| Endpoint | MCP consultado | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `GET /v3/payments/limits` | ✅ | `PaymentLimits` (Creation.Daily.{Limit,Used,WasReached}) | `Payment/limits-response.json` | `PaymentLimits_DeserializesFromOfficialFixture` | ✅ | +| `POST /v3/payments/simulate` (request) | ✅ | `SimulatePaymentRequest` (value, billingTypes[], installmentCount?) | `Payment/simulate-request-minimal.json`, `Payment/simulate-request-with-installments.json` | `SimulatePaymentRequest_*` (3 tests) | ✅ | +| `POST /v3/payments/simulate` (response) | ✅ | `SimulatedPayment` (value, creditCard?, bankSlip?, pix?) | `Payment/simulate-response.json` | `SimulatedPaymentResponse_DeserializesFromOfficialFixture` | ✅ | + +Erros oficiais (`{errors:[{code,description}]}`) também validados via fixture compartilhada (`error-response.json`). + +## §2 — CheckoutManager — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/checkouts` (request) | ✅ | `CreateCheckoutRequest` (billingTypes, chargeTypes, callback, items required) | `Checkout/create-request-minimal.json`, `create-request-recurrent.json` | `CreateCheckoutRequest_*` (3 tests) | ✅ | +| `POST /v3/checkouts` (response) | ✅ | `Checkout` (id, link, status enum, subscriptions, customerData com city/addressNumber `int`) | `Checkout/response.json` | `CheckoutResponse_DeserializesFromOfficialFixture_FullShape`, `_UsesSplitSingular_OnResponse` | ✅ | +| `POST /v3/checkouts/{id}/cancel` | ✅ | mesmo response `Checkout`, body vazio | reusa `response.json` | (idem) | ✅ | +| Enum `CheckoutStatus` ACTIVE/CANCELED/EXPIRED/PAID | ✅ | enum tipado | inline | `CheckoutStatus_AllValuesDeserialize` | ✅ | + +**Quirk documentado:** request usa `splits` (plural), response usa `split` (singular). Comentário no `CreateCheckoutRequest.cs` evita "correções" futuras erradas. + +**Bug pré-existente corrigido em paralelo:** `Subscription.Enums.Cycle` estava faltando o valor `BIMONTHLY` (presente no schema oficial de `SubscriptionSaveRequestCycle` e `CheckoutSessionSubscriptionCycle`). + +## §3 — EscrowManager — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/accounts/{id}/escrow` | ✅ | `SaveEscrowConfigRequest` / response `EscrowConfig` (daysToExpire required, enabled/isFeePayer optional) | `Escrow/config-request.json`, `config-response.json` | `SaveEscrowConfigRequest_HasCorrectFieldNames`, `_NoFakeFields` | ✅ | +| `GET /v3/accounts/{id}/escrow` | ✅ | `EscrowConfig` (mesmo schema) | `Escrow/config-response.json` | `EscrowConfig_DeserializesFromOfficialFixture`, `_OptionalBoolsAreNullableInResponse` | ✅ | +| `POST /v3/accounts/escrow` | ✅ | mesmo `AccountPaymentEscrowConfigDTO` | reusa | (idem) | ✅ | +| `GET /v3/accounts/escrow` | ✅ | mesmo `AccountPaymentEscrowConfigDTO` | reusa | (idem) | ✅ | +| `POST /v3/escrow/{id}/finish` | ✅ | body `{}` vazio → retorna `Payment` (não `Escrow`!) | (cobertura no unit test do manager) | (cobertura existente) | ✅ | +| `GET /v3/payments/{id}/escrow` | ✅ | `Escrow` (id, status enum, expirationDate, finishDate, finishReason enum) | `Escrow/payment-escrow-response.json` | `PaymentEscrow_*`, `EscrowStatus_*`, `EscrowFinishReason_*` (4 tests) | ✅ | +| Enum `EscrowStatus` ACTIVE/DONE | ✅ | enum tipado | inline | `EscrowStatus_BothValuesDeserialize` | ✅ | +| Enum `EscrowFinishReason` (6 valores) | ✅ | enum tipado | inline | `EscrowFinishReason_AllSixValuesDeserialize`, `_NullWhenStatusActive` | ✅ | + +## §4 — PixAutomaticManager — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/pix/automatic/authorizations` | ✅ | `CreatePixAutomaticAuthorizationRequest` + `PixAutomaticAuthorization` | `PixAutomatic/authorization-create-request-minimal.json`, `authorization-response.json` | `CreateAuthorizationRequest_*` (2), `AuthorizationResponse_Deserializes*` | ✅ | +| `GET /v3/pix/automatic/authorizations` | ✅ | envelope padrão (hasMore/totalCount/limit/offset/data) | `PixAutomatic/authorizations-list-response.json` | `AuthorizationsListResponse_UsesStandardEnvelopeWithPagination` | ✅ | +| `GET /v3/pix/automatic/authorizations/{id}` | ✅ | mesmo `PixAutomaticAuthorization` | reusa | (idem) | ✅ | +| `DELETE /v3/pix/automatic/authorizations/{id}` | ✅ | retorna `PixAutomaticAuthorization` (não envelope deleted) | reusa | (idem) | ✅ | +| `GET /v3/pix/automatic/paymentInstructions/{id}` | ✅ | `PixAutomaticPaymentInstruction` (Authorization nested, dueDate, status enum, paymentId, refusalReason) | `PixAutomatic/payment-instruction-response.json` | `PaymentInstruction_DeserializesFromOfficialFixture_WithNestedAuthorization` | ✅ | +| `GET /v3/pix/automatic/paymentInstructions` (filter) | ✅ | `authorizationId`/`customerId`/`paymentId`/`status` | inline | `PaymentInstructionListFilter_SerializesAuthorizationIdNotAuthorization` | ✅ | +| Enums Status/Frequency/OriginType/PaymentInstructionStatus | ✅ | enums tipados | inline | `*_AllFiveValuesDeserialize` (3 tests) | ✅ | + +**Bugs corrigidos nesta fase final:** +- **B-16**: `PixAutomaticPaymentInstruction` tinha `Authorization` como `string` + campos inventados (`Value`, `PaymentDate`, `DateCreated`, `Description`). Schema real: `Authorization` é objeto aninhado (`id`/`endToEndIdentifier`/`customerId`), `DueDate` (não `PaymentDate`), + `endToEndIdentifier`, `paymentId`, `refusalReason`. Status virou enum `PixAutomaticPaymentInstructionStatus`. +- **B-17**: `PixAutomaticPaymentInstructionListFilter` usava `authorization`/`status`. Schema real: `authorizationId`, `customerId`, `paymentId`, `status` (enum). + +## §5 — PixRecurringManager — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `GET /v3/pix/transactions/recurrings` (envelope padrão) | ✅ | `ResponseList` (hasMore/totalCount/limit/offset/data) | `PixRecurring/transactions-list-response.json` | `TransactionsList_UsesStandardEnvelopeWithPagination` | ✅ | +| `GET /v3/pix/transactions/recurrings` (filtros novos) | ✅ | `PixRecurringTransactionListFilter` (status enum, value decimal invariant, searchText) | inline | `TransactionListFilter_*` (4 tests: SerializesAll, InvariantDecimal, UpperEnum, NullOmitted) | ✅ | +| `GET /v3/pix/transactions/recurrings/{id}` | ✅ | `PixRecurringTransaction` (id, status enum, origin enum, value, frequency enum, quantity, startDate, finishDate, canBeCancelled, externalAccount nested) | `PixRecurring/transaction-response.json` | `TransactionResponse_DeserializesFromOfficialFixture` | ✅ | +| `POST /v3/pix/transactions/recurrings/{id}/cancel` | ✅ | body vazio → retorna `PixRecurringTransaction` | reusa | (cobertura unit do manager) | ✅ | +| `GET /v3/pix/transactions/recurrings/{id}/items` (envelope `{data:[...]}`) | ✅ | `PixRecurringItemsResponse` wrapper (sem paginação) | `PixRecurring/items-list-envelope.json` | `ItemsListEnvelope_UsesMinimalDataOnlyShape` | ✅ | +| `POST /v3/pix/transactions/recurrings/items/{id}/cancel` | ✅ | body vazio → `PixRecurringItem` (id, status, scheduledDate, canBeCancelled, recurrenceNumber, quantity, value, refusalReasonDescription, externalAccount) | `PixRecurring/item-response.json` | `ItemResponse_DeserializesFromOfficialFixture` | ✅ | +| Enum `PixRecurringStatus` (5 valores) | ✅ | enum tipado | inline | `TransactionStatus_AllFiveValuesDeserialize` | ✅ | +| Enum `PixRecurringFrequency` (WEEKLY/MONTHLY) | ✅ | enum tipado | inline | `TransactionFrequency_BothValuesDeserialize` | ✅ | +| Enum `PixRecurringOrigin` (PIX) | ✅ | enum tipado | inline | `TransactionOrigin_PixDeserializes` | ✅ | +| Enum `PixRecurringItemStatus` (4 valores) | ✅ | enum tipado | inline | `ItemStatus_AllFourValuesDeserialize` | ✅ | + +**Feature nova adicionada nesta fase:** +- **B-18**: `PixRecurringManager.List(offset, limit)` não aceitava filtro. O schema oficial expõe três filtros opcionais (`status`, `value`, `searchText`). Criado `PixRecurringTransactionListFilter` (request parameters tipado) e novo overload `List(offset, limit, filter)`. Backwards-compatible: `filter` é opcional. + +**Quirks de envelope documentados:** +- `recurrings` (transactions list) usa o envelope padrão (`hasMore`/`totalCount`/`limit`/`offset`/`data`). +- `recurrings/{id}/items` usa envelope minimalista `{data:[...]}` sem paginação — comportamento diferente do schema padrão, mantido em `PixRecurringItemsResponse` wrapper para evitar deserialização incorreta via `ResponseList` (regressão B-14 já fixada). + +## §6 — MobilePhoneRechargeManager — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/mobilePhoneRecharges` (request) | ✅ | `CreateMobilePhoneRechargeRequest` (value+phoneNumber required) | `MobilePhoneRecharge/create-request.json` | `CreateRequest_HasRequiredFields`, `_NoFakeFields` | ✅ | +| `POST /v3/mobilePhoneRecharges` (response) | ✅ | `MobilePhoneRecharge` (id, value, phoneNumber, status enum, canBeCancelled, operatorName) | `MobilePhoneRecharge/recharge-response.json` | `RechargeResponse_DeserializesFromOfficialFixture` | ✅ | +| `GET /v3/mobilePhoneRecharges` | ✅ | `ResponseList` envelope padrão | `MobilePhoneRecharge/recharges-list-response.json` | `RechargesList_UsesStandardEnvelopeWithPagination` | ✅ | +| `GET /v3/mobilePhoneRecharges/{id}` | ✅ | mesmo `MobilePhoneRecharge` | reusa | (idem) | ✅ | +| `POST /v3/mobilePhoneRecharges/{id}/cancel` | ✅ | body vazio → `MobilePhoneRecharge` | (cobertura unit do manager) | (cobertura existente) | ✅ | +| `GET /v3/mobilePhoneRecharges/{phoneNumber}/provider` | ✅ | `MobilePhoneProvider` (name, values: `MobilePhoneProviderValue[]` com {name, description, bonus, minValue, maxValue}) | `MobilePhoneRecharge/provider-response.json` | `ProviderResponse_DeserializesFromOfficialFixture`, `_UsesValuesNotAvailableValues` | ✅ | +| Enum `MobilePhoneRechargeStatus` (5 valores) | ✅ | enum tipado | inline | `RechargeStatus_AllFiveValuesDeserialize` | ✅ | + +**Bug crítico corrigido nesta fase:** +- **B-19**: `MobilePhoneProvider` tinha `AvailableValues: List` (chutado). Schema real: `values: array` de objetos `MobilePhoneProviderValue` com campos `{name, description, bonus, minValue, maxValue}`. Modelo completo reescrito + criada classe nova `MobilePhoneProviderValue` + test antigo do manager corrigido (não mais asserta `AvailableValues`). + +## §7 — MyAccountManager (documents) — ✅ + +Endpoints `/v3/myAccount/documents*` (5 endpoints). Subgrupo Account Document do MyAccountManager. + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `GET /v3/myAccount/documents` | ✅ | `AccountDocumentResponse` (envelope `{rejectReasons, data:[]}` sem paginação) | `AccountDocument/pending-documents-response.json` | `PendingDocumentsResponse_DeserializesFromOfficialFixture`, `_UsesMinimalEnvelopeWithoutPagination` | ✅ | +| `POST /v3/myAccount/documents/{id}` (multipart) | ✅ | request `UploadAccountDocumentRequest` (DocumentFile + Type enum) → response `AccountDocument` | `AccountDocument/document-response.json` | `UploadRequest_HasCorrectMultipartFieldNames` | ✅ | +| `GET /v3/myAccount/documents/files/{id}` | ✅ | `AccountDocument` (id, status enum) | reusa | `DocumentResponse_HasOnlyIdAndStatus` | ✅ | +| `POST /v3/myAccount/documents/files/{id}` (multipart update) | ✅ | request `UploadAccountDocumentRequest` (DocumentFile) → response `AccountDocument` | reusa | (idem) | ✅ | +| `DELETE /v3/myAccount/documents/files/{id}` | ✅ | `BaseDeleted` ({deleted, id}) | `AccountDocument/delete-response.json` | (cobertura unit do manager) | ✅ | +| Enum `AccountDocumentStatus` (4 valores) | ✅ | enum tipado | inline | `DocumentStatus_AllFourValuesDeserialize` | ✅ | +| Enum `AccountDocumentGroupStatus` (5 valores) | ✅ | enum tipado (Group ganha IGNORED) | inline | `DocumentGroupStatus_AllFiveValuesDeserialize` | ✅ | +| Enum `AccountDocumentType` (12 valores) | ✅ | enum tipado | inline | `DocumentType_AllTwelveValuesDeserialize` | ✅ | +| Enum `AccountDocumentResponsibleType` (13 valores) | ✅ | enum tipado | inline | `ResponsibleType_AllThirteenValuesDeserialize` | ✅ | + +**Bugs críticos corrigidos nesta fase (B-20):** +- **B-20a/b/c/d**: `AccountDocument.Status`, `AccountDocumentGroup.Status`, `AccountDocumentGroup.Type`, `AccountDocumentResponsible.Type` eram `string`/`List`. Trocados por enums tipados. +- **B-20f**: `AccountDocumentFile` tinha campos fictícios `Name` e `Url`. Schema real retorna apenas `{id, status}`. Classe removida; endpoints passam a retornar o mesmo `AccountDocument`. +- **B-20g**: `SubmitDocument` retornava `AccountDocumentGroup`. Schema oficial retorna `AccountDocumentGetResponseDTO` (apenas `{id, status}`). Tipo de retorno do manager mudado para `AccountDocument`. +- **B-20h**: `UploadAccountDocumentRequest` tinha `DocumentType: string` e `File: IAsaasFile`. Schema espera multipart fields `type` e `documentFile`. Renomeado para `Type: AccountDocumentType?` e `DocumentFile: IAsaasFile`, alinhando os nomes após o `FirstCharToLower` do `PostMultipartFormDataContentAsync`. + +**Quirk de envelope:** `/myAccount/documents` retorna envelope `{rejectReasons, data:[...]}` sem `hasMore/totalCount/limit/offset`. Mantido em `AccountDocumentResponse` para evitar deserialização incorreta via `ResponseList` (B-07 já fixado em fase anterior). + +## §8 — InvoiceManager (pré-existente) — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/invoices` (request) | ✅ | `CreateInvoiceRequest` (required: serviceDescription, observations, value, deductions, effectiveDate, municipalServiceName, taxes; `payment`/`customer`/`installment` opcionais) | `Invoice/schedule-request.json` | `CreateRequest_*` (3 tests) | ✅ | +| `POST /v3/invoices` (response) | ✅ | `Invoice` (id, status enum, customer, payment, taxes, etc.) | `Invoice/invoice-response.json` | `InvoiceResponse_DeserializesFromOfficialFixture`, `TaxesResponse_HasAllReformaTributariaFields` | ✅ | +| `GET /v3/invoices` (filtros) | ✅ | `InvoiceListFilter` (effectiveDate[Ge]/[Le], payment, installment, customer, externalReference, status) | inline | `ListFilter_UsesCapitalGeAndLeForEffectiveDate`, `_SupportsCustomerAndExternalReference` | ✅ | +| `GET /v3/invoices/{id}` | ✅ | mesmo `Invoice` | reusa | (idem) | ✅ | +| `PUT /v3/invoices/{id}` | ✅ | `UpdateInvoiceRequest` (todos opcionais + updatePayment) | inline | `UpdateRequest_SupportsUpdatePaymentFlag` | ✅ | +| `POST /v3/invoices/{id}/authorize` | ✅ | body vazio → `Invoice` | (cobertura unit do manager) | (cobertura existente) | ✅ | +| `POST /v3/invoices/{id}/cancel` | ✅ | body vazio → `Invoice` | (cobertura unit do manager) | (cobertura existente) | ✅ | +| Enum `InvoiceStatus` (6 valores) | ✅ | enum tipado | inline | `InvoiceStatus_AllSixValuesDeserialize` | ✅ | + +**Bugs corrigidos nesta fase (B-21):** +- **B-21a**: `Taxes` model só tinha 7 campos (retainIss, iss, cofins, csll, inss, ir, pis). Schema `InvoiceTaxesResponseDTO` tem mais 6 (nbsCode, taxSituationCode, taxClassificationCode, operationIndicatorCode, pisCofinsRetentionType, pisCofinsTaxStatus) + 6 da Reforma Tributária (stateIbs, stateIbsValue, municipalIbs, municipalIbsValue, cbs, cbsValue) → todos esses sumiam silenciosamente. Modelo expandido para 19 campos. +- **B-21b**: `InvoiceListFilter` usava `effectiveDate[ge]` e `[le]` lowercase. Schema oficial usa `[Ge]` e `[Le]` maiúsculos. O filtro com casing errado era silenciosamente ignorado pela API. +- **B-21c**: `InvoiceListFilter` faltava `customer` e `externalReference`. Adicionados. +- **B-21d**: `CreateInvoiceRequest` e `UpdateInvoiceRequest` faltavam `updatePayment: bool?`. Adicionado em ambos. + +## §9 — PaymentDunningManager (pré-existente) — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/paymentDunnings` (multipart) | ✅ | `CreatePaymentDunningRequest` (9 campos obrigatórios + documents binários) | (cobertura unit do manager) | `CreateRequest_UsesPaymentNotPaymentId` | ✅ | +| `POST /v3/paymentDunnings` (response) | ✅ | `PaymentDunning` (id, dunningNumber int?, status enum, type enum, payment, requestDate, value, feeValue, netValue, canBeCancelled bool?, isNecessaryResendDocumentation bool?, cannotBeCancelledReason, denialReason) | `PaymentDunning/dunning-response.json` | `DunningResponse_*` (2 tests) | ✅ | +| `GET /v3/paymentDunnings` | ✅ | `ResponseList` + `PaymentDunningListFilter` (status, type, payment, requestStartDate, requestEndDate) | `PaymentDunning/dunnings-list-response.json` | `DunningsList_*`, `ListFilter_*` | ✅ | +| `GET /v3/paymentDunnings/{id}` | ✅ | mesmo `PaymentDunning` | reusa | (idem) | ✅ | +| `POST /v3/paymentDunnings/simulate` | ✅ | **payment como QUERY param, body vazio** → `SimulatedPaymentDunning` com `TypeSimulations: List<...>` | `PaymentDunning/simulate-response.json` | `SimulateResponse_DeserializesFromOfficialFixture` | ✅ | +| `GET /v3/paymentDunnings/{id}/history` | ✅ | `ResponseList` com `Status: PaymentDunningHistoryStatus` enum | `PaymentDunning/history-list-response.json` | `HistoryResponse_*`, `HistoryStatus_AllFourValuesDeserialize` | ✅ | +| `GET /v3/paymentDunnings/{id}/partialPayments` | ✅ | `ResponseList` (value, description, paymentDate) | `PaymentDunning/partial-payments-response.json` | `PartialPaymentsResponse_DeserializesFromOfficialFixture` | ✅ | +| `GET /v3/paymentDunnings/paymentsAvailableForDunning` | ✅ | `ResponseList` com `TypeSimulations: List<...>` | `PaymentDunning/payments-available-response.json` | `PaymentsAvailableResponse_DeserializesFromOfficialFixture` | ✅ | +| `POST /v3/paymentDunnings/{id}/cancel` | ✅ | body vazio → `PaymentDunning` | (cobertura unit do manager) | (cobertura existente) | ✅ | +| Enum `PaymentDunningStatus` (8 valores) | ✅ | enum tipado | inline | `DunningStatus_AllEightValuesDeserialize` | ✅ | +| Enum `PaymentDunningType` (CREDIT_BUREAU + DEBT_RECOVERY_ASSISTANCE p/ filter) | ✅ | enum tipado | inline | `DunningType_BothValuesDeserialize` | ✅ | +| Enum `PaymentDunningHistoryStatus` (4 valores) | ✅ | enum tipado | inline | `HistoryStatus_AllFourValuesDeserialize` | ✅ | + +**Bugs corrigidos nesta fase (B-22):** +- **B-22a**: `PaymentDunning.DunningNumber` era `string`. Schema é `integer` (int32). Trocado para `int?`. +- **B-22b**: `PaymentDunning` faltava `CannotBeCancelledReason: string`. Adicionado. +- **B-22c/d**: `PaymentDunning.CanBeCancelled` e `IsNecessaryResendDocumentation` eram `bool` non-nullable. Schema permite null. Trocados para `bool?`. Sem o fix, omitir esses campos no JSON forçava `false` silenciosamente. +- **B-22e**: `PaymentDunning.ReceivedInCashFeeValue` e `CancellationFeeValue` marcados como `[Obsolete]` (schema oficial marca deprecated). +- **B-22f**: `PaymentDunningEventHistory.Status` era `string`. Schema é enum `PaymentDunningHistoryStatus` (IN_NEGOTIATION, NEGOTIATION_FAIL, NEGOTIATED, PAID). Trocado para enum tipado. +- **B-22h**: `PaymentDunningType` enum tinha apenas `CREDIT_BUREAU`. Filter aceita também `DEBT_RECOVERY_ASSISTANCE`. Adicionado. +- **B-22k**: `Simulate(request)` enviava `payment` no body JSON. Schema oficial expõe como **query param** (`?payment=pay_xxx`) e exige body vazio. Manager corrigido para construir query string. +- **B-22m**: `SimulatedPaymentDunning.TypeSimulations` e `PaymentDunningPaymentAvailable.TypeSimulations` eram objeto único. Schema retorna ARRAY. Trocados para `List`. Sem o fix, deserialização lançava `InvalidCastException` no JSON real. + +## §10 — CreditBureauReportManager (pré-existente) — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/creditBureauReport` (request) | ✅ | `CreateCreditBureauReportRequest` (customer? + cpfCnpj? — ambos opcionais) | inline | `CreateRequest_HasOnlyCustomerAndCpfCnpj`, `_DoesNotSerializeRemovedFields` | ✅ | +| `POST /v3/creditBureauReport` (response) | ✅ | `CreditBureauReport` (id, dateCreated, cpfCnpj, customer, downloadUrl, reportFile) — `reportFile` populado APENAS no POST | `CreditBureauReport/report-create-response.json` | `ReportResponse_PopulatesReportFile_OnCreate` | ✅ | +| `GET /v3/creditBureauReport` | ✅ | `ResponseList` + `CreditBureauReportListFilter` (startDate, endDate) | `CreditBureauReport/reports-list-response.json` | `ReportsList_*`, `ListFilter_*` (2 tests) | ✅ | +| `GET /v3/creditBureauReport/{id}` | ✅ | mesmo `CreditBureauReport` (mas `reportFile=null` aqui) | `CreditBureauReport/report-response.json` | `ReportResponse_DeserializesFromOfficialFixture_GetById`, `_NoFakeFields` | ✅ | + +**Bugs corrigidos nesta fase (B-23):** +- **B-23a/b**: `CreditBureauReport` tinha `State: string` e `Status: string` — **nenhum existe no schema oficial**. Removidos. Era chute do dev original. +- **B-23c**: `CreditBureauReport` faltava `DownloadUrl: string` e `ReportFile: string` (PDF Base64). Adicionados. Sem o fix, consumidores não tinham como baixar o relatório. +- **B-23d**: `CreateCreditBureauReportRequest.State` foi removido (não existe no schema). +- **B-23e**: `List(offset, limit)` não aceitava filtro. Schema expõe `startDate` e `endDate`. Criado `CreditBureauReportListFilter` e novo overload `List(offset, limit, filter)` backwards-compatible. + +## §11 — BillPaymentManager (pré-existente) — ✅ + +| Endpoint | MCP | Model | Fixture | Contract test | Status | +|---|---|---|---|---|---| +| `POST /v3/bill` (request) | ✅ | `CreateBillPaymentRequest` (required: identificationField; opcionais: scheduleDate, description, discount, interest, fine, dueDate, value, externalReference) | inline | `CreateRequest_SerializesAllKeysIncludingNewOnes`, `_OptionalFieldsOmittedWhenNull` | ✅ | +| `POST /v3/bill` (response) | ✅ | `BillPayment` (17 campos incluindo interest, fine, paymentDate, externalReference, failReasons array) | `BillPayment/bill-response.json` | `BillResponse_DeserializesFromOfficialFixture`, `_FailReasonsIsArrayOfStrings` | ✅ | +| `GET /v3/bill` | ✅ | `ResponseList` | `BillPayment/bills-list-response.json` | `BillsList_UsesStandardEnvelopeWithPagination` | ✅ | +| `GET /v3/bill/{id}` | ✅ | mesmo `BillPayment` | reusa | (idem) | ✅ | +| `POST /v3/bill/simulate` (request) | ✅ | `SimulateBillPaymentRequest` (identificationField OU barCode) | inline | `SimulateRequest_AcceptsIdentificationFieldOrBarCode` | ✅ | +| `POST /v3/bill/simulate` (response) | ✅ | `SimulatedBillPayment` (minimumScheduleDate, fee, bankSlipInfo com 17 campos) | `BillPayment/simulate-response.json` | `SimulateResponse_DeserializesFromOfficialFixture` | ✅ | +| `POST /v3/bill/{id}/cancel` | ✅ | body vazio → `BillPayment` | (cobertura unit do manager) | (cobertura existente) | ✅ | +| Enum `BillPaymentStatus` (7 valores) | ✅ | enum tipado | inline | `BillStatus_AllSevenValuesDeserialize` | ✅ | + +**Bugs corrigidos nesta fase (B-24):** +- **B-24a**: `BillPayment` faltava `Interest`, `Fine`, `PaymentDate`, `ExternalReference`. `FailReasons` era `string`; schema é `array of string` — trocado para `List`. +- **B-24b**: `BillPaymentStatus` enum tinha apenas 5 valores. Schema tem 7 (adicionados `REFUNDED` e `AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST`). +- **B-24c**: `BillPayment.CanBeCancelled`/`DueDate`/`ScheduleDate`/`PaymentDate` agora são nullable (response pode omitir/null em status iniciais). +- **B-24d**: `CreateBillPaymentRequest` faltava `Interest`, `Fine`, `ExternalReference`. Campos não-obrigatórios (`Value`, `DueDate`, `ScheduleDate`, `Discount`) trocados para nullable (apenas `IdentificationField` é required no schema). +- **B-24e**: `BankSlipInfo` tinha 5 campos com `BankCode` (nome chutado). Schema tem 17 campos com `bank`. Modelo reescrito: `Bank`, `BeneficiaryCpfCnpj`, `BeneficiaryName`, `AllowChangeValue`, `MinValue`, `MaxValue`, `DiscountValue`, `InterestValue`, `FineValue`, `OriginalValue`, `TotalDiscountValue`, `TotalAdditionalValue`, `IsOverdue` — todos adicionados. + +--- + +## §12 — CustomerManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST /v3/customers` (CRUD) | ✅ | B-25e (NotificationDisabled bool? em Create/Update) | +| `GET /v3/customers` + 5 filtros | ✅ | OK | +| `GET /v3/customers/{id}` | ✅ | B-25a (DateCreated nullable) | +| `PUT /v3/customers/{id}` | ✅ | (idem create) | +| `DELETE /v3/customers/{id}` + restore | ✅ | OK | +| `GET /v3/customers/{id}/notifications` | ✅ | **B-25g — endpoint estava faltando no manager. Adicionado `GetNotifications`** | + +Contract tests: `CustomerContractTests` (8 tests). + +## §13 — PaymentManager (resto) — ✅ + +Cobre todos os endpoints alem de `/limits` e `/simulate` (já em §1). + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST /v3/payments` (BOLETO/PIX/etc) | ✅ | OK | +| `POST /v3/payments/` (Credit Card) | ✅ | OK | +| `GET /v3/payments` com 18 filtros | ✅ | **B-26d — faltavam 9 filtros: customerGroupName, invoiceStatus, estimatedCreditDate, pixQrCodeId, anticipable, user, checkoutSession, dateCreated[ge]/[le], estimatedCreditDate[ge]/[le]** | +| `GET /v3/payments/{id}` | ✅ | B-26a/b/c (DateCreated, DueDate, OriginalDueDate nullable) | +| `PUT/DELETE/POST restore/refund/...` | ✅ | OK | +| `GET /v3/payments/{id}/pixQrCode` | ✅ | OK | +| `GET /v3/payments/{id}/identificationField` | ✅ | OK | +| `POST /v3/payments/{id}/receiveInCash` | ✅ | OK | + +Quirk: filtro Payment usa `[ge]/[le]` **lowercase** (Invoice usa `[Ge]/[Le]` uppercase). + +Contract tests: `PaymentContractTests` (21 tests: 6 existentes + 7 novos do resto). + +## §14 — SubscriptionManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST /v3/subscriptions` + List/Find/Update/Delete | ✅ | B-27d (Object, PaymentLinkId, CheckoutSession, Split adicionados) | +| `PUT /v3/subscriptions/{id}/creditCard` | ✅ | OK | +| `GET /v3/subscriptions/{id}/payments` + `/paymentBook` | ✅ | OK | +| `POST/PUT/GET/DELETE /v3/subscriptions/{id}/invoiceSettings` | ✅ | OK | +| `GET /v3/subscriptions/{id}/invoices` | ✅ | OK | + +**Bugs:** +- B-27a: enum `SubscriptionStatus` faltava `INACTIVE` (tinha apenas ACTIVE/EXPIRED). +- B-27b/c: `DateCreated`, `NextDueDate` → DateTime? +- B-27e: filter faltava `customerGroupName`, `status`, `deletedOnly`, `externalReference`, `order`, `sort`. + +Contract tests: `SubscriptionContractTests` (5 tests). + +## §15 — PixManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST/GET/DELETE /v3/pix/addressKeys` | ✅ | B-28c (Status enum, QrCode nested, CanBeDeleted/Reason) | +| `POST /v3/pix/qrCodes/static` + DELETE | ✅ | OK | +| `GET /v3/pix/tokenBucket/addressKey` | ✅ | OK | +| `POST /v3/pix/qrCodes/pay` + `/decode` | ✅ | OK | +| `GET /v3/pix/transactions` com 3 filtros | ✅ | **B-28e — filter não existia. Criado `PixTransactionListFilter`** | +| `GET /v3/pix/transactions/{id}` | ✅ | **B-28b — modelo reescrito (7 → 25 campos)** | +| `POST /v3/pix/transactions/{id}/cancel` | ✅ | OK | + +**Bug crítico B-28a:** `PixTransactionStatus` enum estava com 5 valores INVENTADOS (PENDING, FAILED não existem no schema). Reescrito com 11 valores reais. + +Novos enums: `PixTransactionType` (5), `PixTransactionOriginType` (6), `PixTransactionFinality` (2), `PixAddressKeyStatus` (6). + +Contract tests: `PixContractTests` (7 tests). + +## §16 — TransferManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST /v3/transfers` (bank account) | ✅ | OK | +| `POST /v3/transfers/` (asaas account) | ✅ | OK | +| `GET /v3/transfers` com 5 filtros | ✅ | **B-29h — faltavam dateCreated[ge]/[le] e transferDate[ge]/[le]** | +| `GET /v3/transfers/{id}` | ✅ | B-29b/c (DateCreated, Authorized nullable) | +| `DELETE /v3/transfers/{id}/cancel` | ✅ | OK | + +**Bugs:** B-29d (`AsaasAccountTransferStatus` faltavam BANK_PROCESSING/FAILED), B-29e (novo enum `TransferOperationType`), B-29g (BaseTransfer faltava 7 campos), Bank/BankAccount faltavam vários campos. + +Contract tests: `TransferContractTests` (5 tests). + +## §17 — AnticipationManager — ✅ + +Endpoints: POST/GET `/v3/anticipations`, simulate, find, cancel, limits, automatic configurations. + +**Bugs B-30:** Anticipation.AnticipationDate/DueDate/RequestDate → DateTime? + campo Object adicionado. + +Contract tests: `AnticipationContractTests` (3 tests). + +## §18 — InstallmentManager — ✅ + +Endpoints: POST/GET/PUT/DELETE `/v3/installments`, refund, payments, paymentBook, cancelPendingPayments, splits. + +**Bugs B-31:** ExpirationDay → int?, adicionados CreditCard (nested) e Refunds (array). + +Contract tests: `InstallmentContractTests` (2 tests). + +## §19 — WebhookManager — ✅ + +Endpoints: POST/GET/PUT/DELETE `/v3/webhooks`, removeBackoff. + +**Sem bugs estruturais.** Modelo Webhook + enum WebhookEvent (110+ valores) verificados OK. + +Quirk: GET `/v3/webhooks` no schema só aceita offset/limit (filter `WebhookListFilter` mantido por backwards-compat — backend pode aceitar mesmo não documentado). + +Contract tests: `WebhookContractTests` (3 tests). + +## §20 — WalletManager — ✅ + +Endpoint único: `GET /v3/wallets/`. + +**Bug B-33:** Wallet faltava `Object` (response wrapper). Adicionado. + +Contract tests: `WalletContractTests` (1 test). + +## §21 — NotificationManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `PUT /v3/notifications/{id}` | ✅ | B-34b (todos bools → bool? em UpdateRequest) | +| `PUT /v3/notifications/batch` | ✅ | OK | + +**Bugs:** B-34a (`Event` enum faltando — novo `NotificationEvent` 6 valores), B-34b (Notification + UpdateRequest com bools nullable). + +Contract tests: `NotificationContractTests` (3 tests). + +## §22 — CreditCardManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `POST /v3/creditCard/tokenizeCreditCard` | ✅ | B-35a (Brand string → enum CreditCardBrand 13 valores) | +| `POST /v3/creditCard/preAuthorization/config` | ✅ | **B-35b — PreAuthorizationConfig tinha campos INVENTADOS (Enabled, AutomaticCaptureDelay). Schema: {daysToExpire}**. Reescrito | +| `GET /v3/creditCard/preAuthorization/config` | ✅ | (idem) | + +Contract tests: `CreditCardContractTests` (5 tests). + +## §23 — PaymentLinkManager — ✅ + +11 endpoints (CRUD + images). + +**Bugs B-36:** +- B-36a: PaymentLink.SubscriptionCycle string → `Cycle` enum (7 valores). +- B-36b: faltavam ViewCount, IsAddressRequired, ExternalReference. +- B-36c: Value, Active, NotificationEnabled, Deleted, DueDateLimitDays, MaxInstallmentCount → nullable. + +Contract tests: `PaymentLinkContractTests` (3 tests). + +## §24 — FinanceManager — ✅ + +| Endpoint | MCP | Bugs corrigidos | +|---|---|---| +| `GET /v3/finance/balance` | ✅ | OK | +| `GET /v3/finance/payment/statistics` | ✅ | **B-37b — não aceitava filtros. Schema expõe 11. Criado `PaymentStatisticsFilter`** | +| `GET /v3/finance/split/statistics` | ✅ | **B-37a — campos INVENTADOS (TotalPendingValue/TotalReceivedValue). Schema: {income, value}**. Reescrito | +| `/v3/financialTransactions` (legado, fora do schema) | ⚠️ | mantido por backwards-compat | + +Contract tests: `FinanceContractTests` (4 tests). + +## §25 — MyAccountManager (resto) — ✅ + +Endpoints `/myAccount/commercialInfo`, `/status`, `/fees`, `/accountNumber`, `/paymentCheckoutConfig`, `DELETE /myAccount`. + +**Bugs B-38:** +- B-38a: MyAccount.Status string → `AccountInfoStatus` enum (4 valores). +- B-38b: faltavam CompanyName, IncomeValue, TradingName, Site, AvailableCompanyNames (array), CommercialInfoExpiration (nested). +- InscricaoEstadual marcado `[Obsolete]` (não existe no schema atual). + +Contract tests: `MyAccountContractTests` (3 tests). + +## §26 — AsaasAccountManager — ✅ + +Endpoints: POST/GET `/v3/accounts`, find, resendActivationLink, accessTokens CRUD. + +**Bugs B-39:** +- B-39a: Account.City string → `long?` (schema: integer city id). +- B-39b: faltavam Object, Id, BirthDate, TradingName, Site, AccountNumber (nested), CommercialInfoExpiration (nested). +- B-39c: ApiKey marcado `[Obsolete]` (não existe no schema oficial). + +Contract tests: `AsaasAccountContractTests` (1 test). + +## §27 — FiscalInfoManager — ✅ + +10 endpoints (CRUD + lookups municipais/federais/nbs/tributários). + +**Bugs B-40:** +- B-40a: faltava NbsCode. +- B-40b: RpsNumber, LoteNumber string → int (schema: integer). +- B-40c: faltavam PasswordSent, AccessTokenSent, CertificateSent (bools) + NationalPortalTaxCalculationRegime + Object. +- B-40d/e: StateInscription e AccessToken marcados `[Obsolete]`. + +Contract tests: `FiscalInfoContractTests` (1 test). + +## §28 — ChargebackManager — ✅ + +Endpoints: GET list/find, POST dispute. + +**Bug B-41:** Chargeback faltava `CreditCard` (nested `ChargebackCreditCard` com number + brand enum). Reason → nullable. + +Contract tests: `ChargebackContractTests` (3 tests). + +## §29 — SandboxManager — ✅ + +3 endpoints (approve account, confirm payment, force overdue). + +**Sem bugs** — manager já correto. `EnsureSandbox()` bloqueia uso em produção. + +Contract tests: `SandboxContractTests` (1 test sanity check). + +--- + +## §50 — Cross-pattern bug list (consolidação) + +Padrões de bug encontrados em múltiplos managers e como foram corrigidos sistemicamente: + +### Pattern 1 — Envelope `{ data: [...] }` minimalista (sem paginação) +- **Endpoints afetados:** `GET /myAccount/documents` (§7), `GET /pix/transactions/recurrings/{id}/items` (§5). +- **Anti-padrão:** Usar `ResponseList` que assume `hasMore/totalCount/limit/offset`. +- **Fix:** wrapper dedicado (`AccountDocumentResponse`, `PixRecurringItemsResponse`). + +### Pattern 2 — `bool` PascalCase em query params (`True`/`False`) +- **Solução sistêmica:** `RequestParameters.Add(bool?)` força lowercase `"true"`/`"false"`. +- **Coberto por:** `RequestParametersContractTests.Bool_*` (3 tests, 2026-05-24). +- **Validado em runtime:** `PaymentIntegrationTests.ListPayments_WithAnticipatedFilter` (§99). + +### Pattern 3 — Query param com casing errado em range filters +- **Padrão Asaas:** Payment usa `[ge]/[le]` LOWERCASE; Invoice usa `[Ge]/[Le]` UPPERCASE. +- **Bugs corrigidos:** B-21b (Invoice), B-29h (Transfer), B-26d (Payment já estava correto). +- **Validado:** contract tests + `TransferIntegrationTests.ListTransfers_WithDateRangeFilter`. + +### Pattern 4 — Body vs query param +- **Bug crítico B-22k:** `POST /paymentDunnings/simulate` enviava `payment` no body. Schema oficial expõe como query param. +- **Fix:** `PaymentDunningManager.Simulate` constrói query string e envia body vazio. + +### Pattern 5 — Enum incompleto ou inventado +- **Bugs corrigidos:** + - B-19 (`MobilePhoneProvider.AvailableValues` era `List` — schema: array de `{name, description, bonus, minValue, maxValue}`) + - B-20a–d (4 enums tipados em AccountDocument) + - B-22f (`PaymentDunningHistoryStatus`), B-22h (PaymentDunningType ganhou DEBT_RECOVERY_ASSISTANCE) + - B-24b (`BillPaymentStatus` 5→7 valores), B-27a (`SubscriptionStatus` ganhou INACTIVE) + - **B-28a (`PixTransactionStatus` 5 valores INVENTADOS → 11 reais — mais grave)** + - B-34a (`NotificationEvent` enum criado), B-35a (`CreditCardBrand`), B-36a (`Cycle`) + - B-38a (`AccountInfoStatus`), B-40b (Rps/Lote int), B-41 (Chargeback enums) + +### Pattern 6 — Array vs objeto único +- **Bug B-22m:** `SimulatedPaymentDunning.TypeSimulations` e `PaymentDunningPaymentAvailable.TypeSimulations` eram objeto único. Schema retorna array. Lançava `InvalidCastException` em runtime. + +### Pattern 7 — Nullable incorreto +- **Bugs corrigidos:** B-22c/d, B-26a/b/c, B-27b/c, B-29b/c, B-30a, B-34b, B-35, B-36c, B-25a/e. Sempre que schema permite omitir/null, mas modelo era non-nullable, deserialização quebrava ou forçava `false`/`default` silenciosamente. + +### Pattern 8 — Paginação incorreta +- Validada via `ResponseList` envelope padrão em todos os contract tests `*List_UsesStandardEnvelopeWithPagination`. + +### Pattern 9 — Campos obrigatórios ausentes +- **Bugs:** B-21d (UpdatePayment em Invoice), B-24d (Interest/Fine/ExternalReference em BillPayment), B-25g (GetNotifications endpoint), B-28e/B-37b (filtros faltando). + +### Pattern 10 — Nome de campo errado +- **Bugs:** B-17 (`authorization` → `authorizationId`), B-29 (`bankCode` → `bank`), B-21b ([ge]/[le] casing), B-22a (DunningNumber int vs string), B-40b (RpsNumber int vs string). + +### Pattern 11 — Campos inventados / não existem no schema +- **Bugs graves:** B-23a/b (`State`, `Status` em CreditBureauReport), B-20f (`Name`, `Url` em AccountDocumentFile), B-35b (`Enabled`, `AutomaticCaptureDelay` em PreAuthorizationConfig), B-37a (`TotalPendingValue`, `TotalReceivedValue` em SplitStatistics), B-39 (`ApiKey`, `City` string em Account), B-40 (`StateInscription`, `AccessToken` em FiscalInfo). +- Estes representam pura **chute do dev original** sem verificar a doc. Removidos ou marcados `[Obsolete]`. + +### Padrões sistêmicos confirmados OK +- `RequestParameters.Add(decimal?)` com `InvariantCulture` (`12.5` não `12,5`). +- `RequestParameters.Add(bool?)` com lowercase. +- `DateTimeExtensions.ToApiRequest` com `InvariantCulture`. +- `RequestParameters.Add(Enum)` serializa nome do enum em UPPER. +- Envelope padrão `{object, hasMore, totalCount, limit, offset, data}` em `ResponseList`. + +--- + +## §99 — Integration tests (sandbox real) + +Status: ✅ implementado — **15 tests** cobrindo 7 managers críticos. Skip automático sem `ASAAS_SANDBOX_TOKEN`. + +| Manager | Tests | Endpoints cobertos | Valida regression | +|---|---|---|---| +| Customer | 2 | CRUD completo + List paginado | Round-trip, envelope padrão | +| Payment | 3 | POST BOLETO/PIX, GET pixQrCode, List filter Anticipated | **B-26d (bool? filter)** | +| Subscription | 2 | Create BOLETO + List filter Status=INACTIVE | **B-27a (enum INACTIVE)** | +| Pix | 2 | ListAddressKeys, ListTransactions filter Status=AWAITING_REQUEST | **B-28a (enum 11 valores)** | +| Transfer | 1 | List filter date range [ge]/[le] | **B-29h (casing lowercase)** | +| Anticipation | 2 | List + GetLimits | Envelope, schema | +| Finance | 3 | Balance + Statistics filter + Split shape | **B-37a (income/value), B-37b (filter)** | + +**Infraestrutura:** +- `[IntegrationFact]` (custom attribute) — skip automático se `ASAAS_SANDBOX_TOKEN` ausente, mensagem explicativa no skip reason. +- `IntegrationTestBase` — `[Trait("Category", "Integration")]`, constrói `AsaasApi` real apontando para `AsaasEnvironment.SANDBOX`. +- Cada test cria seus próprios recursos (suffix timestampado) e limpa no `finally` para não poluir o sandbox. + +**CI workflow:** [`.github/workflows/integration-sandbox.yml`](.github/workflows/integration-sandbox.yml) +- `workflow_dispatch` (trigger manual via UI do GitHub) +- `schedule: '0 4 * * *'` (nightly 04:00 UTC / 01:00 BRT) +- Secret necessário: `ASAAS_SANDBOX_TOKEN` (Settings → Secrets and variables → Actions) +- Workflow emite warning explícito se secret estiver ausente, mas não falha. + +**Para rodar localmente:** +```powershell +$env:ASAAS_SANDBOX_TOKEN = "aact_YTU0...seu_token_sandbox..." +dotnet test --filter "Category=Integration" +``` + +**Para rodar tudo EXCETO integration (CI local sem credencial):** +```powershell +dotnet test --filter "Category!=Integration" +``` + +Sem a variável: integration tests fazem skip automaticamente — `dotnet test` continua verde. + +### Checklist por test (anti-fixture-falsa) + +Cada integration test foi escrito seguindo este checklist: +- ✅ Cria seus próprios recursos (não depende de estado pré-existente no sandbox) +- ✅ Usa timestamp no nome/email para evitar colisão entre execuções +- ✅ Limpa recursos no `finally` quando aplicável +- ✅ Asserta `WasSuccessful()` com mensagem de erro detalhada (para diagnóstico no CI) +- ✅ Valida pelo menos um bug específico (B-XX) corrigido na auditoria +- ✅ Não duplica cobertura de contract tests (foca em comportamento end-to-end, não shape) + +### Riscos residuais e por quê + +Status: **AINDA NÃO EXECUTADOS contra sandbox real nesta sessão** — apenas escritos e validados que skip funciona. O agente que escreveu a auditoria não tem acesso a `ASAAS_SANDBOX_TOKEN`. Para fechar essa lacuna: + +1. **Curto prazo:** rodar manualmente via `workflow_dispatch` no GitHub Actions com o secret configurado. Resultado da primeira execução pode revelar: + - Schemas de fixture incompletos (raros, mas possíveis) + - Comportamentos sandbox vs spec divergentes + - Algum CPF de teste rejeitado pelo sandbox específico +2. **Médio prazo:** expandir cobertura conforme bugs surgirem em produção dos consumidores. + +--- + +## Padrões sistêmicos auditados + +| Padrão | Status | Onde | +|---|---|---| +| Query params bool serializa lowercase `true`/`false` | ✅ | `RequestParameters.Add(bool?)` + `RequestParametersContractTests.Bool_*` (3 tests) | +| Query params decimal serializa invariant culture (ponto, não vírgula) | ✅ | `RequestParameters.Add(decimal?)` + `Decimal_SerializesWithDotInAllCultures` (4 culturas) | +| Query params DateTime serializa `YYYY-MM-DD` em qualquer cultura | ✅ | `DateTimeExtensions.ToApiRequest` + `DateTime_SerializesAsIsoYyyyMmDdInAllCultures` (3 culturas) | +| Query params enum serializa nome do enum em UPPER | ✅ | `RequestParameters.Add(Enum)` + `Enum_SerializesAsUppercaseAsaasName` | +| Query string escapa caracteres especiais | ✅ | `Build_BuildsCorrectQueryStringWithEscaping` | +| Envelope `{data:[...]}` (sem hasMore) — endpoints conhecidos | ✅ | `AccountDocument` (B-07 fixado), `PixRecurring.ListItems` (B-14 fixado) | +| `bool?` em campos opcionais de response | ⏳ | grep durante Fase 5 | + +--- + +## Riscos remanescentes + +Lista honesta do que **não** está 100% fechado, classificada por aceitabilidade: + +### ❌ NÃO ACEITÁVEIS (precisam resolver antes de produção de SDK de pagamento) + +Nenhum. Todos os 27 managers passaram por auditoria schema-first. + +### ⚠️ ACEITÁVEIS COM RESSALVA + +**1. Integration tests nunca rodaram contra sandbox real nesta sessão.** +- Status: 15 tests escritos, validados via skip automático. Infra de CI configurada para nightly run. +- Risco: alguma assinatura de método pode estar ligeiramente diferente do que o sandbox aceita; algum CPF de teste pode ser rejeitado; algum fix B-XX pode comportar-se diferente no runtime real. +- Mitigação: workflow `integration-sandbox.yml` rodará nightly. Primeiras 1–2 execuções vão revelar e estabilizar. +- **Próximo passo para fechar:** push da branch `audit/asaas-api-conformance` + configurar `ASAAS_SANDBOX_TOKEN` em GitHub Secrets + disparar workflow "Integration (sandbox)" em Actions → Run workflow. Atualizar este documento com link do run. + +**2. Algumas fixtures foram escritas manualmente a partir dos exemplos MCP (não auto-geradas).** +- Risco: se eu copiei mal um exemplo (ex: esqueci um campo que aparece em outros casos), o contract test passa mas o modelo continua incompleto. +- Mitigação: `JsonContractAssert.HasRootProperty` + `DoesNotSerializeKey` em campos críticos detectam divergência. +- Lacunas conhecidas: enums com 100+ valores (WebhookEvent) só testam 10 representativos. + +### ✅ ACEITÁVEIS (decisão explícita) + +**Endpoints `/financialTransactions`** (FinanceManager): legado, não está mais no MCP. Mantido por backwards-compat. + +**Webhooks (recebimento de payloads):** O SDK expõe `WebhookManager` para configurar endpoints, mas não decodificadores tipados dos payloads que o Asaas envia. Consumidores deserializam manualmente. **Scope decisão:** seria um SDK separado. + +**Reforma Tributária:** Campos `stateIbs`/`municipalIbs`/`cbs` em `Taxes` (Invoice) adicionados conforme spec atual. Comportamento em produção depende do calendário de implementação. Pode mudar — auditar quando spec mudar. + +**Campos `[Obsolete]`:** 5 campos marcados em PaymentDunning, AsaasAccount, FiscalInfo, MyAccount. Mantidos por backwards-compat. Backend pode parar de retornar a qualquer momento — consumidores devem migrar. + +**Filtros `WebhookListFilter` (name, enabled, interrupted):** Não documentados no MCP, mas mantidos no SDK porque a API original aceita. Se falhar, remover. + +**Endpoint adicional FiscalInfo:** 6 endpoints de lookup (`federalServiceCodes`, `nbsCodes`, `operationIndicatorCodes`, `taxClassificationCodes`, `taxSituationCodes`, `nationalPortal`) existem no schema mas não no manager. Decisão: feature adicional, não impede uso do SDK. diff --git a/Codout.Apis.Asaas.Sample/Program.cs b/Codout.Apis.Asaas.Sample/Program.cs index 4e73d09..4e95b67 100644 --- a/Codout.Apis.Asaas.Sample/Program.cs +++ b/Codout.Apis.Asaas.Sample/Program.cs @@ -11,7 +11,7 @@ ResponseObject customerResponse = await asaasApi.Customer.Find("cus_13bFHumeyglN"); -if (customerResponse.WasSucessfull()) +if (customerResponse.WasSuccessful()) { Customer customer = customerResponse.Data; diff --git a/Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj b/Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj index 254b0a7..d13d22d 100644 --- a/Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj +++ b/Codout.Apis.Asaas.Tests/Codout.Apis.Asaas.Tests.csproj @@ -23,4 +23,12 @@ + + + + PreserveNewest + + + \ No newline at end of file diff --git a/Codout.Apis.Asaas.Tests/Contract/AccountDocumentContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/AccountDocumentContractTests.cs new file mode 100644 index 0000000..08c9e0f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/AccountDocumentContractTests.cs @@ -0,0 +1,183 @@ +using System; +using System.Text.Json; +using Codout.Apis.Asaas.Models.MyAccount; +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para os endpoints de Account Document (MyAccountManager). +/// Schemas verificados via MCP em 2026-05-24: +/// - GET /v3/myAccount/documents (envelope {data:[...]} sem paginacao + rejectReasons) +/// - POST /v3/myAccount/documents/{id} (multipart, retorna AccountDocument {id,status}) +/// - GET /v3/myAccount/documents/files/{id} (retorna AccountDocument {id,status}) +/// - POST /v3/myAccount/documents/files/{id} (multipart, retorna AccountDocument) +/// - DELETE /v3/myAccount/documents/files/{id} ({deleted, id}) +/// +public class AccountDocumentContractTests +{ + // ───────────────────────────────────────────────────────────── + // GET /v3/myAccount/documents -> AccountDocumentShowResponseDTO + // Envelope minimalista: { rejectReasons, data: [...] } (sem paginacao!) + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PendingDocumentsResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("AccountDocument/pending-documents-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.RejectReasons); + Assert.Single(result.Data); + + var group = result.Data[0]; + Assert.Equal("172ed152-4fa4-43ad-9b69-39c323e9526c", group.Id); + Assert.Equal(AccountDocumentGroupStatus.NOT_SENT, group.Status); + Assert.Equal(AccountDocumentType.MINUTES_OF_CONSTITUTION, group.Type); + Assert.Equal("Minutes of election of the last board", group.Title); + Assert.Equal("No description", group.Description); + Assert.Equal("https://example.com/cadastro.io/8ad196d6cbfcc5d05bfabcbb5c730f6a", group.OnboardingUrl); + Assert.Equal(new DateTime(2025, 3, 4), group.OnboardingUrlExpirationDate); + + Assert.NotNull(group.Responsible); + Assert.Equal("John Doe", group.Responsible.Name); + Assert.Single(group.Responsible.Type); + Assert.Equal(AccountDocumentResponsibleType.ASSOCIATION, group.Responsible.Type[0]); + + Assert.Single(group.Documents); + Assert.Equal("8d257732-2220-11ec-b695-b6af4a64184d", group.Documents[0].Id); + Assert.Equal(AccountDocumentStatus.PENDING, group.Documents[0].Status); + } + + [Fact] + public void PendingDocumentsResponse_UsesMinimalEnvelopeWithoutPagination() + { + // B-07 regression: o envelope tem APENAS {rejectReasons, data}, + // sem hasMore/totalCount/limit/offset. Usar ResponseList aqui + // resultaria em propriedades vazias. + var json = FixtureLoader.Load("AccountDocument/pending-documents-response.json"); + + JsonContractAssert.HasRootProperty(json, "data", JsonValueKind.Array); + JsonContractAssert.HasRootProperty(json, "rejectReasons", JsonValueKind.Null); + + using var doc = JsonDocument.Parse(json); + Assert.False(doc.RootElement.TryGetProperty("hasMore", out _)); + Assert.False(doc.RootElement.TryGetProperty("totalCount", out _)); + Assert.False(doc.RootElement.TryGetProperty("limit", out _)); + Assert.False(doc.RootElement.TryGetProperty("offset", out _)); + } + + // ───────────────────────────────────────────────────────────── + // POST/GET /v3/myAccount/documents[/files]/{id} -> AccountDocumentGetResponseDTO + // B-20f/B-20g regression: ViewDocumentFile e SubmitDocument antes retornavam + // AccountDocumentFile (com Name/Url ficticios) e AccountDocumentGroup + // (objeto rico). Schema real retorna apenas {id, status}. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void DocumentResponse_HasOnlyIdAndStatus() + { + var json = FixtureLoader.Load("AccountDocument/document-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("8d257732-2220-11ec-b695-b6af4a64184d", result.Id); + Assert.Equal(AccountDocumentStatus.PENDING, result.Status); + + // Regressao B-20f: schema NAO retorna name nem url + using var doc = JsonDocument.Parse(json); + Assert.False(doc.RootElement.TryGetProperty("name", out _)); + Assert.False(doc.RootElement.TryGetProperty("url", out _)); + } + + [Fact] + public void DocumentStatus_AllFourValuesDeserialize() + { + // Schema AccountDocumentGetResponseDTO.status: NOT_SENT, PENDING, APPROVED, REJECTED + foreach (var status in new[] { "NOT_SENT", "PENDING", "APPROVED", "REJECTED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void DocumentGroupStatus_AllFiveValuesDeserialize() + { + // Schema AccountDocumentGroupResponseDTO.status tem 5 valores + // (Group ganha IGNORED em relacao ao Document). + foreach (var status in new[] { "NOT_SENT", "PENDING", "APPROVED", "REJECTED", "IGNORED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\",\"type\":\"CUSTOM\",\"documents\":[]}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void DocumentType_AllTwelveValuesDeserialize() + { + var values = new[] { + "ALLOW_BANK_ACCOUNT_DEPOSIT_STATEMENT", "CUSTOM", "EMANCIPATION_OF_MINORS", + "ENTREPRENEUR_REQUIREMENT", "IDENTIFICATION_SELFIE", "IDENTIFICATION", + "INVOICE", "MEI_CERTIFICATE", "MINUTES_OF_CONSTITUTION", + "MINUTES_OF_ELECTION", "POWER_OF_ATTORNEY", "SOCIAL_CONTRACT" + }; + + foreach (var type in values) + { + var json = $"{{\"id\":\"x\",\"status\":\"NOT_SENT\",\"type\":\"{type}\",\"documents\":[]}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(type, result.Type.ToString()); + } + } + + [Fact] + public void ResponsibleType_AllThirteenValuesDeserialize() + { + var values = new[] { + "ALLOW_BANK_ACCOUNT_DEPOSIT_STATEMENT", "ASAAS_ACCOUNT_OWNER_EMANCIPATION_AGE", + "ASAAS_ACCOUNT_OWNER", "ASSOCIATION", "BANK_ACCOUNT_OWNER_EMANCIPATION_AGE", + "BANK_ACCOUNT_OWNER", "CUSTOM", "DIRECTOR", "INDIVIDUAL_COMPANY", + "LIMITED_COMPANY", "MEI", "PARTNER", "POWER_OF_ATTORNEY" + }; + + foreach (var t in values) + { + var json = $"{{\"name\":\"John\",\"type\":[\"{t}\"]}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Single(result.Type); + Assert.Equal(t, result.Type[0].ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // POST multipart request — B-20h regression + // Schema oficial: campos sao "documentFile" (binary) e "type" (enum). + // C# properties devem ser DocumentFile e Type (FirstCharToLower converte). + // ───────────────────────────────────────────────────────────── + + [Fact] + public void UploadRequest_HasCorrectMultipartFieldNames() + { + // Reflexao sobre as propriedades: garantir que C# property names viram + // os field names esperados pelo schema apos firstCharToLower do BaseManager. + var props = typeof(UploadAccountDocumentRequest).GetProperties(); + var names = new System.Collections.Generic.HashSet(); + foreach (var p in props) + { + var firstLower = char.ToLowerInvariant(p.Name[0]) + p.Name.Substring(1); + names.Add(firstLower); + } + + Assert.Contains("documentFile", names); + Assert.Contains("type", names); + + // Regressao B-20h: nao deve mais existir "file" (antes era File: IAsaasFile) + // nem "documentType" (antes era DocumentType: string). + Assert.DoesNotContain("file", names); + Assert.DoesNotContain("documentType", names); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/AnticipationContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/AnticipationContractTests.cs new file mode 100644 index 0000000..5e01ec6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/AnticipationContractTests.cs @@ -0,0 +1,60 @@ +using System; +using Codout.Apis.Asaas.Models.Anticipation; +using Codout.Apis.Asaas.Models.Anticipation.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para AnticipationManager (B-30). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class AnticipationContractTests +{ + [Fact] + public void AnticipationResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Anticipation/anticipation-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("receivableAnticipation", result.Object); + Assert.Equal("9e7d8639-350f-45c0-8bc3-d4ddc5f4ebac", result.Id); + Assert.Equal("pay_626366773834", result.PaymentId); + Assert.Null(result.InstallmentId); + Assert.Equal(AnticipationStatus.PENDING, result.Status); + Assert.Equal(new DateTime(2019, 5, 20), result.AnticipationDate); + Assert.Equal(new DateTime(2019, 5, 26), result.DueDate); + Assert.Equal(new DateTime(2019, 5, 14), result.RequestDate); + Assert.Equal(2.33m, result.Fee); + Assert.Equal(5, result.AnticipationDays); + Assert.Equal(73.68m, result.NetValue); + Assert.Equal(80m, result.TotalValue); + Assert.Equal(76.01m, result.Value); + Assert.Null(result.DenialObservation); + } + + [Fact] + public void AnticipationResponse_NullableDatesHandleMissing() + { + // B-30a: AnticipationDate, DueDate, RequestDate eram non-nullable. + var json = "{\"id\":\"a_x\",\"status\":\"PENDING\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.AnticipationDate); + Assert.Null(result.DueDate); + Assert.Null(result.RequestDate); + } + + [Fact] + public void AnticipationStatus_AllSevenValuesDeserialize() + { + foreach (var status in new[] { + "PENDING", "DENIED", "CREDITED", "DEBITED", "CANCELLED", "OVERDUE", "SCHEDULED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/AsaasAccountContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/AsaasAccountContractTests.cs new file mode 100644 index 0000000..f54c428 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/AsaasAccountContractTests.cs @@ -0,0 +1,36 @@ +using System; +using Codout.Apis.Asaas.Models.AsaasAccount; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para AsaasAccountManager (B-39). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class AsaasAccountContractTests +{ + [Fact] + public void AccountResponse_DeserializesAllFields() + { + var json = "{\"object\":\"account\",\"id\":\"4f468235-cec3-482f-b3d0-348af4c7194\",\"name\":\"John Doe\",\"email\":\"john.doe@asaas.com.br\",\"loginEmail\":\"john.doe@asaas.com.br\",\"phone\":null,\"mobilePhone\":null,\"address\":\"Rua Fernando Orlandi\",\"addressNumber\":\"544\",\"province\":\"Jardim Pedra Branca\",\"postalCode\":\"14079-452\",\"cpfCnpj\":\"35381637000150\",\"birthDate\":\"1995-04-12\",\"personType\":\"JURIDICA\",\"companyType\":\"MEI\",\"city\":15478,\"state\":\"SP\",\"country\":\"Brasil\",\"tradingName\":null,\"site\":\"https://www.example.com\",\"walletId\":\"c0c1688f-636b-42c0-b6ee-7339182276b7\",\"accountNumber\":{\"agency\":\"0001\",\"account\":\"3514\",\"accountDigit\":\"3\"},\"commercialInfoExpiration\":{\"isExpired\":false,\"scheduledDate\":\"2025-05-05 00:00:00\"}}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("account", result.Object); + Assert.Equal("4f468235-cec3-482f-b3d0-348af4c7194", result.Id); + Assert.Equal("John Doe", result.Name); + Assert.Equal(PersonType.JURIDICA, result.PersonType); + Assert.Equal(CompanyType.MEI, result.CompanyType); + // B-39a: City era string, agora int + Assert.Equal(15478L, result.City); + Assert.Equal("SP", result.State); + Assert.Equal(new DateTime(1995, 4, 12), result.BirthDate); + Assert.Equal("c0c1688f-636b-42c0-b6ee-7339182276b7", result.WalletId); + // B-39b: campos novos + Assert.NotNull(result.AccountNumber); + Assert.Equal("0001", result.AccountNumber.Agency); + Assert.NotNull(result.CommercialInfoExpiration); + Assert.False(result.CommercialInfoExpiration.IsExpired); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/BillPaymentContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/BillPaymentContractTests.cs new file mode 100644 index 0000000..e8d50e6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/BillPaymentContractTests.cs @@ -0,0 +1,187 @@ +using System; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Bill; +using Codout.Apis.Asaas.Models.Bill.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para BillPaymentManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/bill (required: identificationField; opcional: scheduleDate, +/// description, discount, interest, fine, dueDate, value, externalReference) +/// - GET /v3/bill (envelope padrao) +/// - POST /v3/bill/simulate (identificationField OU barCode) +/// - GET /v3/bill/{id} +/// - POST /v3/bill/{id}/cancel (body vazio) +/// +public class BillPaymentContractTests +{ + // ───────────────────────────────────────────────────────────── + // BillPayment response - B-24a/b regression + // Antes: faltava interest, fine, paymentDate, externalReference; + // failReasons era string em vez de array; + // enum BillPaymentStatus faltava 2 valores. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void BillResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("BillPayment/bill-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("f1bce822-6f37-4905-8de8-f1af9f2f4bab", result.Id); + Assert.Equal(BillPaymentStatus.PENDING, result.Status); + Assert.Equal(29m, result.Value); + Assert.Equal(0m, result.Discount); + // B-24a: campos novos + Assert.Equal(0m, result.Interest); + Assert.Equal(0m, result.Fine); + Assert.Null(result.PaymentDate); + Assert.Null(result.ExternalReference); + Assert.Equal("03399.77779 29900.000000 04751.101017 1 81510000002990", result.IdentificationField); + Assert.Equal(new DateTime(2020, 1, 31), result.DueDate); + Assert.Equal(new DateTime(2020, 1, 31), result.ScheduleDate); + Assert.Equal(0m, result.Fee); + Assert.Equal("Celular 01/12", result.Description); + Assert.Equal("https://www.asaas.com/comprovantes/00016578", result.TransactionReceiptUrl); + Assert.False(result.CanBeCancelled); + // B-24a: failReasons agora e array + Assert.NotNull(result.FailReasons); + Assert.Empty(result.FailReasons); + } + + [Fact] + public void BillResponse_FailReasonsIsArrayOfStrings() + { + // B-24a regression: failReasons era string. Schema e array of string. + var json = "{\"id\":\"x\",\"status\":\"FAILED\",\"failReasons\":[\"Saldo insuficiente\",\"Banco fora do ar\"]}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(2, result.FailReasons.Count); + Assert.Equal("Saldo insuficiente", result.FailReasons[0]); + Assert.Equal("Banco fora do ar", result.FailReasons[1]); + } + + [Fact] + public void BillsList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("BillPayment/bills-list-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.Single(response.Data); + } + + [Fact] + public void BillStatus_AllSevenValuesDeserialize() + { + // Schema: PENDING, BANK_PROCESSING, PAID, FAILED, CANCELLED, + // REFUNDED, AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST + // B-24b: REFUNDED e AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST eram faltantes + foreach (var status in new[] { + "PENDING", "BANK_PROCESSING", "PAID", "FAILED", + "CANCELLED", "REFUNDED", "AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // Create request - B-24d regression + // Apenas identificationField e required; demais opcionais (nullable). + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateRequest_SerializesAllKeysIncludingNewOnes() + { + var request = new CreateBillPaymentRequest + { + IdentificationField = "03399.77779 29900.000000 04751.101017 1 81510000002990", + ScheduleDate = new DateTime(2020, 3, 15), + Description = "Celular 03/12", + Discount = 0m, + Interest = 0m, + Fine = 0m, + DueDate = new DateTime(2020, 3, 30), + Value = 29m, + ExternalReference = "056984" + }; + + JsonContractAssert.SerializesWithKeys(request, + "identificationField", "scheduleDate", "description", + "discount", "interest", "fine", "dueDate", "value", "externalReference"); + } + + [Fact] + public void CreateRequest_OptionalFieldsOmittedWhenNull() + { + // Apenas identificationField e required no schema. Resto pode ser null. + var request = new CreateBillPaymentRequest + { + IdentificationField = "03399.77779 29900.000000 04751.101017 1 81510000002990" + }; + + JsonContractAssert.SerializesWithKeys(request, "identificationField"); + JsonContractAssert.DoesNotSerializeKey(request, "scheduleDate"); + JsonContractAssert.DoesNotSerializeKey(request, "discount"); + JsonContractAssert.DoesNotSerializeKey(request, "value"); + } + + // ───────────────────────────────────────────────────────────── + // BankSlipInfo response - B-24e regression + // Schema tem 17 campos (incluindo bank/beneficiary*/min-max/allowChangeValue/ + // discountValue/interestValue/fineValue/originalValue/totalDiscount/ + // totalAdditional/isOverdue). Antes tinha apenas 5 (com bankCode errado). + // ───────────────────────────────────────────────────────────── + + [Fact] + public void SimulateResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("BillPayment/simulate-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(new DateTime(2021, 11, 22), result.MinimumScheduleDate); + Assert.Equal(0m, result.Fee); + + var info = result.BankSlipInfo; + Assert.NotNull(info); + Assert.Equal("03399201595100529040147600301023888440000421177", info.IdentificationField); + Assert.Equal(4211.77m, info.Value); + Assert.Equal(new DateTime(2021, 12, 24), info.DueDate); + // B-24e: schema usa "bank", nao "bankCode" + Assert.Equal("033", info.Bank); + Assert.Equal("19.540.550/0001-21", info.BeneficiaryCpfCnpj); + Assert.Equal("ASAAS GESTAO FINANCEIRA S.A.", info.BeneficiaryName); + Assert.False(info.AllowChangeValue); + Assert.Equal(4211.77m, info.MinValue); + Assert.Equal(4211.77m, info.MaxValue); + Assert.Equal(0m, info.DiscountValue); + Assert.Equal(0m, info.InterestValue); + Assert.Equal(0m, info.FineValue); + Assert.Equal(4211.77m, info.OriginalValue); + Assert.Equal(0m, info.TotalDiscountValue); + Assert.Equal(0m, info.TotalAdditionalValue); + Assert.False(info.IsOverdue); + } + + // ───────────────────────────────────────────────────────────── + // Simulate request - identificationField OR barCode + // ───────────────────────────────────────────────────────────── + + [Fact] + public void SimulateRequest_AcceptsIdentificationFieldOrBarCode() + { + var byField = new SimulateBillPaymentRequest { IdentificationField = "03399..." }; + var byBarCode = new SimulateBillPaymentRequest { BarCode = "23793..." }; + + JsonContractAssert.SerializesWithKeys(byField, "identificationField"); + JsonContractAssert.SerializesWithKeys(byBarCode, "barCode"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/ChargebackContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/ChargebackContractTests.cs new file mode 100644 index 0000000..6d21911 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/ChargebackContractTests.cs @@ -0,0 +1,61 @@ +using System; +using Codout.Apis.Asaas.Models.Chargeback; +using Codout.Apis.Asaas.Models.Chargeback.Enums; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para ChargebackManager (B-41). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class ChargebackContractTests +{ + [Fact] + public void ChargebackResponse_DeserializesAllFields() + { + var json = "{\"id\":\"8e784c3e-afe8-4844-bb93-6b445763\",\"payment\":\"pay_pBtDdshgBD2Rt\",\"installment\":\"b8dd74c-d078-40a0-9ae1-61a66c61a204\",\"customerAccount\":\"cus_000000004085\",\"status\":\"DONE\",\"reason\":\"COMMERCIAL_DISAGREEMENT\",\"disputeStartDate\":\"2024-11-10\",\"value\":2323.45,\"paymentDate\":\"2024-03-10\",\"creditCard\":{\"number\":\"8829\",\"brand\":\"VISA\"},\"disputeStatus\":\"ACCEPTED\",\"deadlineToSendDisputeDocuments\":\"2024-12-10\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("8e784c3e-afe8-4844-bb93-6b445763", result.Id); + Assert.Equal("pay_pBtDdshgBD2Rt", result.PaymentId); + Assert.Equal("b8dd74c-d078-40a0-9ae1-61a66c61a204", result.InstallmentId); + Assert.Equal("cus_000000004085", result.CustomerAccountId); + Assert.Equal(ChargebackStatus.DONE, result.Status); + Assert.Equal(ChargebackReason.COMMERCIAL_DISAGREEMENT, result.Reason); + Assert.Equal(new DateTime(2024, 11, 10), result.DisputeStartDate); + Assert.Equal(2323.45m, result.Value); + Assert.Equal(new DateTime(2024, 3, 10), result.PaymentDate); + Assert.Equal(ChargebackDisputeStatus.ACCEPTED, result.DisputeStatus); + Assert.Equal(new DateTime(2024, 12, 10), result.DeadlineToSendDisputeDocuments); + + // B-41a: CreditCard nested objeto novo + Assert.NotNull(result.CreditCard); + Assert.Equal("8829", result.CreditCard.Number); + Assert.Equal(CreditCardBrand.VISA, result.CreditCard.Brand); + } + + [Fact] + public void ChargebackStatus_AllFiveValuesDeserialize() + { + foreach (var status in new[] { "REQUESTED", "IN_DISPUTE", "DISPUTE_LOST", "REVERSED", "DONE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void ChargebackDisputeStatus_AllThreeValuesDeserialize() + { + foreach (var ds in new[] { "REQUESTED", "ACCEPTED", "REJECTED" }) + { + var json = $"{{\"id\":\"x\",\"disputeStatus\":\"{ds}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.DisputeStatus); + Assert.Equal(ds, result.DisputeStatus.ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/CheckoutContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/CheckoutContractTests.cs new file mode 100644 index 0000000..e9deef3 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/CheckoutContractTests.cs @@ -0,0 +1,139 @@ +using Codout.Apis.Asaas.Models.Checkout; +using Codout.Apis.Asaas.Models.Checkout.Enums; +using Codout.Apis.Asaas.Models.Subscription.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para CheckoutManager. +/// Schema verificado via MCP em 2026-05-24. +/// +public class CheckoutContractTests +{ + // ───────────────────────────────────────────────────────────── + // POST /v3/checkouts (request) + // Required: billingTypes[], chargeTypes[], callback, items[] + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateCheckoutRequest_Minimal_HasAllRequiredKeys() + { + var request = new CreateCheckoutRequest + { + BillingTypes = [CheckoutBillingType.CREDIT_CARD], + ChargeTypes = [CheckoutChargeType.DETACHED], + Callback = new CheckoutCallback + { + SuccessUrl = "https://example.com/asaas/checkout/success", + CancelUrl = "https://example.com/asaas/checkout/cancel" + }, + Items = [new CheckoutItem { Name = "Roupas", Quantity = 2, Value = 100m, ImageBase64 = "IMAGE IN BASE64" }] + }; + + JsonContractAssert.SerializesWithKeys(request, "billingTypes", "chargeTypes", "callback", "items"); + } + + [Fact] + public void CreateCheckoutRequest_UsesSplitsPlural_NotSingular() + { + var request = new CreateCheckoutRequest + { + BillingTypes = [CheckoutBillingType.PIX], + ChargeTypes = [CheckoutChargeType.DETACHED], + Callback = new CheckoutCallback { SuccessUrl = "x", CancelUrl = "x" }, + Items = [new CheckoutItem { Name = "X", Quantity = 1, Value = 1m, ImageBase64 = "X" }], + Splits = [new CheckoutSplit { WalletId = "w1", PercentageValue = 10m }] + }; + + // Quirk Asaas: request usa "splits" (plural), response usa "split" (singular). + // Documentado em CreateCheckoutRequest.cs. + JsonContractAssert.SerializesWithKeys(request, "splits"); + JsonContractAssert.DoesNotSerializeKey(request, "split"); + } + + [Fact] + public void CreateCheckoutRequest_NoFakeFields() + { + var request = new CreateCheckoutRequest + { + BillingTypes = [CheckoutBillingType.PIX], + ChargeTypes = [CheckoutChargeType.DETACHED], + Callback = new CheckoutCallback { SuccessUrl = "x", CancelUrl = "x" }, + Items = [new CheckoutItem { Name = "X", Quantity = 1, Value = 1m, ImageBase64 = "X" }] + }; + + // Regressao B-04: campos inventados (value/dueDate/customer/description) + // que estavam no model antigo e nao existem na API. + JsonContractAssert.DoesNotSerializeKey(request, "value"); + JsonContractAssert.DoesNotSerializeKey(request, "dueDate"); + JsonContractAssert.DoesNotSerializeKey(request, "customer"); + JsonContractAssert.DoesNotSerializeKey(request, "description"); + JsonContractAssert.DoesNotSerializeKey(request, "checkoutUrl"); + } + + // ───────────────────────────────────────────────────────────── + // POST /v3/checkouts and POST /v3/checkouts/{id}/cancel + // Both return CheckoutSessionResponseDTO + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CheckoutResponse_DeserializesFromOfficialFixture_FullShape() + { + var json = FixtureLoader.Load("Checkout/response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("131ca662-56c8-4479-b5b3-fd61a413fce7", result.Id); + Assert.Equal("https://sandbox.asaas.com/checkoutSession/show/131ca662-56c8-4479-b5b3-fd61a413fce7", result.Link); + Assert.Equal(CheckoutStatus.ACTIVE, result.Status); + + Assert.Contains(CheckoutBillingType.CREDIT_CARD, result.BillingTypes); + Assert.Contains(CheckoutChargeType.RECURRENT, result.ChargeTypes); + + Assert.Equal(100, result.MinutesToExpire); + Assert.Equal("dcf4dff9-b080-425c-b234-765f2ffac0ae", result.ExternalReference); + + Assert.NotNull(result.Callback); + Assert.Equal("https://example.com/asaas/checkout/success", result.Callback.SuccessUrl); + Assert.Equal("https://example.com/asaas/checkout/cancel", result.Callback.CancelUrl); + Assert.Equal("https://example.com/asaas/checkout/expired", result.Callback.ExpiredUrl); + + Assert.Single(result.Items); + Assert.Equal("Roupas", result.Items[0].Name); + Assert.Equal(2, result.Items[0].Quantity); + Assert.Equal(100m, result.Items[0].Value); + + Assert.NotNull(result.CustomerData); + Assert.Equal("John Doe", result.CustomerData.Name); + Assert.Equal(150, result.CustomerData.AddressNumber); + Assert.Equal(12987382, result.CustomerData.City); + + Assert.NotNull(result.Subscription); + Assert.Equal(Cycle.MONTHLY, result.Subscription.Cycle); + } + + [Fact] + public void CheckoutResponse_UsesSplitSingular_OnResponse() + { + // Asaas: response usa "split" singular (contrario ao request "splits") + var json = "{\"id\":\"ck_1\",\"split\":[{\"walletId\":\"w1\",\"fixedValue\":10}]}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Single(result.Split); + Assert.Equal("w1", result.Split[0].WalletId); + Assert.Equal(10m, result.Split[0].FixedValue); + } + + [Fact] + public void CheckoutStatus_AllValuesDeserialize() + { + // Schema: ACTIVE, CANCELED, EXPIRED, PAID + foreach (var status in new[] { "ACTIVE", "CANCELED", "EXPIRED", "PAID" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/CreditBureauReportContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/CreditBureauReportContractTests.cs new file mode 100644 index 0000000..2b73d44 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/CreditBureauReportContractTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Text.Json; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.CreditBureauReport; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para CreditBureauReportManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/creditBureauReport (request {customer?, cpfCnpj?}) +/// - GET /v3/creditBureauReport (envelope padrao + filtros startDate/endDate) +/// - GET /v3/creditBureauReport/{id} +/// +public class CreditBureauReportContractTests +{ + // ───────────────────────────────────────────────────────────── + // POST request - schema CreditBureauReportSaveRequestDTO + // Apenas {customer, cpfCnpj} (ambos opcionais). + // B-23d: o modelo antigo tinha State (nao existe no schema). + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateRequest_HasOnlyCustomerAndCpfCnpj() + { + var request = new CreateCreditBureauReportRequest + { + Customer = "cus_000000001766", + CpfCnpj = "05666663755" + }; + + JsonContractAssert.SerializesWithKeys(request, "customer", "cpfCnpj"); + } + + [Fact] + public void CreateRequest_DoesNotSerializeRemovedFields() + { + // Regressao B-23d: o campo State NAO existe no schema oficial. + var request = new CreateCreditBureauReportRequest { Customer = "cus_x" }; + + JsonContractAssert.DoesNotSerializeKey(request, "state"); + JsonContractAssert.DoesNotSerializeKey(request, "status"); + } + + // ───────────────────────────────────────────────────────────── + // Response shape - schema CreditBureauReportGetResponseDTO + // Fields: id, dateCreated, cpfCnpj, customer, downloadUrl, reportFile + // B-23a/b: model antigo tinha State e Status (nao existem no schema) + // B-23c: model antigo nao tinha DownloadUrl nem ReportFile + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ReportResponse_DeserializesFromOfficialFixture_GetById() + { + var json = FixtureLoader.Load("CreditBureauReport/report-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", result.Id); + Assert.Equal(new DateTime(2025, 5, 30), result.DateCreated); + Assert.Equal("05666663755", result.CpfCnpj); + Assert.Equal("cus_000000001766", result.Customer); + Assert.Equal("https://www.asaas.com.br/creditBureauReport/download/6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", result.DownloadUrl); + // Em GET por id e itens do List, reportFile vem null. + Assert.Null(result.ReportFile); + } + + [Fact] + public void ReportResponse_PopulatesReportFile_OnCreate() + { + // Schema documenta que reportFile (PDF Base64) e retornado APENAS no POST. + var json = FixtureLoader.Load("CreditBureauReport/report-create-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.NotNull(result.ReportFile); + Assert.StartsWith("JVBERi", result.ReportFile); + } + + [Fact] + public void ReportResponse_NoFakeFields() + { + // Regressao B-23a/b: schema NAO retorna State nem Status. + var json = FixtureLoader.Load("CreditBureauReport/report-response.json"); + + using var doc = JsonDocument.Parse(json); + Assert.False(doc.RootElement.TryGetProperty("state", out _)); + Assert.False(doc.RootElement.TryGetProperty("status", out _)); + } + + [Fact] + public void ReportsList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("CreditBureauReport/reports-list-response.json"); + + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Single(response.Data); + Assert.Equal("6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", response.Data[0].Id); + } + + // ───────────────────────────────────────────────────────────── + // List filter - B-23e + // Schema expoe startDate e endDate. Antes nao existia filter. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ListFilter_SerializesDateFieldsAsIso() + { + var filter = new CreditBureauReportListFilter + { + StartDate = new DateTime(2024, 1, 1), + EndDate = new DateTime(2024, 12, 31) + }; + + JsonContractAssert.QueryParamEquals(filter, "startDate", "2024-01-01"); + JsonContractAssert.QueryParamEquals(filter, "endDate", "2024-12-31"); + } + + [Fact] + public void ListFilter_NullValuesAreOmitted() + { + var filter = new CreditBureauReportListFilter(); + + Assert.False(filter.ContainsKey("startDate")); + Assert.False(filter.ContainsKey("endDate")); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/CreditCardContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/CreditCardContractTests.cs new file mode 100644 index 0000000..cc11e10 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/CreditCardContractTests.cs @@ -0,0 +1,83 @@ +using Codout.Apis.Asaas.Models.Common; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.CreditCard; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para CreditCardManager (B-35). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class CreditCardContractTests +{ + [Fact] + public void TokenizeResponse_DeserializesFromSchemaShape() + { + var json = "{\"creditCardNumber\":\"8829\",\"creditCardBrand\":\"VISA\",\"creditCardToken\":\"a75a1d98-c52d-4a6b-a413-71e00b193c99\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("8829", result.Number); + Assert.Equal(CreditCardBrand.VISA, result.Brand); + Assert.Equal("a75a1d98-c52d-4a6b-a413-71e00b193c99", result.Token); + } + + [Fact] + public void CreditCardBrand_AllThirteenValuesDeserialize() + { + foreach (var brand in new[] { + "VISA", "MASTERCARD", "ELO", "DINERS", "DISCOVER", "AMEX", + "CABAL", "BANESCARD", "CREDZ", "SOROCRED", "CREDSYSTEM", "JCB", "UNKNOWN" }) + { + var json = $"{{\"creditCardBrand\":\"{brand}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Brand); + Assert.Equal(brand, result.Brand.ToString()); + } + } + + [Fact] + public void PreAuthorizationConfig_DeserializesDaysToExpire() + { + // B-35b regression: model antigo tinha Enabled + AutomaticCaptureDelay + // (inventados). Schema real: apenas daysToExpire (required). + var json = "{\"daysToExpire\":5}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(5, result.DaysToExpire); + } + + [Fact] + public void SavePreAuthorizationConfigRequest_OnlyDaysToExpire() + { + var request = new SavePreAuthorizationConfigRequest { DaysToExpire = 25 }; + JsonContractAssert.SerializesWithKeys(request, "daysToExpire"); + // B-35b regression: nao deve serializar enabled nem automaticCaptureDelay + JsonContractAssert.DoesNotSerializeKey(request, "enabled"); + JsonContractAssert.DoesNotSerializeKey(request, "automaticCaptureDelay"); + } + + [Fact] + public void TokenizeRequest_HasRequiredKeys() + { + var request = new TokenizeCreditCardRequest + { + Customer = "cus_x", + RemoteIp = "1.2.3.4", + CreditCard = new CreditCardRequest + { + HolderName = "John Doe", Number = "1234567890123456", + ExpiryMonth = "5", ExpiryYear = "2026", Ccv = "123" + }, + CreditCardHolderInfo = new CreditCardHolderInfoRequest + { + Name = "John Doe", Email = "j@example.com", CpfCnpj = "12345678901", + PostalCode = "01310000", AddressNumber = "150", Phone = "11999998888" + } + }; + + JsonContractAssert.SerializesWithKeys(request, + "customer", "creditCard", "creditCardHolderInfo", "remoteIp"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/CustomerContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/CustomerContractTests.cs new file mode 100644 index 0000000..9ec292f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/CustomerContractTests.cs @@ -0,0 +1,149 @@ +using System; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Customer; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para CustomerManager (B-25). +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/customers (request: name+cpfCnpj required) +/// - GET /v3/customers (envelope padrao + 5 filtros) +/// - GET /v3/customers/{id} +/// - PUT /v3/customers/{id} +/// - DELETE /v3/customers/{id} (BaseDeleted shape) +/// - POST /v3/customers/{id}/restore (body vazio) +/// - GET /v3/customers/{id}/notifications (envelope padrao) +/// +public class CustomerContractTests +{ + [Fact] + public void CreateRequest_OnlyRequiredFieldsSerialized() + { + var request = new CreateCustomerRequest { Name = "John Doe", CpfCnpj = "24971563792" }; + JsonContractAssert.SerializesWithKeys(request, "name", "cpfCnpj"); + // notificationDisabled e bool? -> nao deve aparecer no JSON quando nao definido + JsonContractAssert.DoesNotSerializeKey(request, "notificationDisabled"); + } + + [Fact] + public void CreateRequest_FullPayloadMatchesSchema() + { + var request = new CreateCustomerRequest + { + Name = "John Doe", + CpfCnpj = "24971563792", + Email = "john.doe@asaas.com.br", + MobilePhone = "4799376637", + Address = "Av. Paulista", + AddressNumber = "150", + Complement = "Sala 201", + Province = "Centro", + PostalCode = "01310-000", + ExternalReference = "12987382", + NotificationDisabled = false, + AdditionalEmails = "john.doe@asaas.com", + MunicipalInscription = "46683695908", + StateInscription = "646681195275", + Observations = "great payer", + GroupName = "vip", + Company = "Acme", + ForeignCustomer = false + }; + + JsonContractAssert.SerializesWithKeys(request, + "name", "cpfCnpj", "email", "mobilePhone", "address", "addressNumber", + "complement", "province", "postalCode", "externalReference", + "notificationDisabled", "additionalEmails", "municipalInscription", + "stateInscription", "observations", "groupName", "company", "foreignCustomer"); + } + + [Fact] + public void UpdateRequest_NotificationDisabledIsNullable() + { + // B-25e regression: era bool nao-nulavel, forcando false em todo Update + // que nao setasse explicitamente o campo. + var request = new UpdateCustomerRequest { Name = "x" }; + JsonContractAssert.DoesNotSerializeKey(request, "notificationDisabled"); + } + + [Fact] + public void CustomerResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Customer/customer-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("cus_000005401844", result.Id); + Assert.Equal("customer", result.Object); + Assert.Equal(new DateTime(2024, 7, 12), result.DateCreated); + Assert.Equal("John Doe", result.Name); + Assert.Equal("john.doe@asaas.com.br", result.Email); + Assert.Equal(12565L, result.CityId); + Assert.Equal("São Paulo", result.CityName); + Assert.Equal("SP", result.State); + Assert.Equal("Brasil", result.Country); + Assert.Equal("24971563792", result.CpfCnpj); + Assert.Equal(PersonType.FISICA, result.PersonType); + Assert.False(result.Deleted); + Assert.False(result.NotificationDisabled); + Assert.False(result.ForeignCustomer); + } + + [Fact] + public void CustomerResponse_DateCreatedIsNullable() + { + // B-25a regression: era DateTime non-nullable; deserializacao falhava + // quando o JSON omitia dateCreated. + var json = "{\"id\":\"cus_x\"}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Null(result.DateCreated); + } + + [Fact] + public void CustomersList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("Customer/customers-list-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Equal(10, response.Limit); + Assert.Equal(0, response.Offset); + Assert.Single(response.Data); + Assert.Equal("cus_000005401844", response.Data[0].Id); + } + + [Fact] + public void PersonType_BothValuesDeserialize() + { + foreach (var person in new[] { "FISICA", "JURIDICA" }) + { + var json = $"{{\"id\":\"x\",\"personType\":\"{person}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.PersonType); + Assert.Equal(person, result.PersonType.ToString()); + } + } + + [Fact] + public void ListFilter_SerializesAllFiveFieldsWithCorrectNames() + { + var filter = new CustomerListFilter + { + Name = "John Doe", + Email = "john@example.com", + CpfCnpj = "24971563792", + GroupName = "vip", + ExternalReference = "ext_42" + }; + + JsonContractAssert.QueryParamEquals(filter, "name", "John Doe"); + JsonContractAssert.QueryParamEquals(filter, "email", "john@example.com"); + JsonContractAssert.QueryParamEquals(filter, "cpfCnpj", "24971563792"); + JsonContractAssert.QueryParamEquals(filter, "groupName", "vip"); + JsonContractAssert.QueryParamEquals(filter, "externalReference", "ext_42"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/EscrowContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/EscrowContractTests.cs new file mode 100644 index 0000000..9d8ed30 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/EscrowContractTests.cs @@ -0,0 +1,123 @@ +using Codout.Apis.Asaas.Models.Escrow; +using Codout.Apis.Asaas.Models.Escrow.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para EscrowManager (Conta de Garantia). +/// Schemas verificados via MCP em 2026-05-24: +/// - POST/GET /v3/accounts/{id}/escrow (subaccount config) +/// - POST/GET /v3/accounts/escrow (default config) +/// - POST /v3/escrow/{id}/finish (retorna Payment, body vazio) +/// - GET /v3/payments/{id}/escrow (PaymentEscrowGetResponseDTO) +/// Os 4 endpoints de config compartilham AccountPaymentEscrowConfigDTO. +/// +public class EscrowContractTests +{ + // ───────────────────────────────────────────────────────────── + // Config endpoints (POST/GET subaccount, POST/GET default) + // Schema: AccountPaymentEscrowConfigDTO { daysToExpire (required), enabled, isFeePayer } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void SaveEscrowConfigRequest_HasCorrectFieldNames() + { + var request = new SaveEscrowConfigRequest + { + DaysToExpire = 30, + Enabled = true, + IsFeePayer = false + }; + + JsonContractAssert.SerializesWithKeys(request, "daysToExpire", "enabled", "isFeePayer"); + } + + [Fact] + public void SaveEscrowConfigRequest_NoFakeFields() + { + var request = new SaveEscrowConfigRequest { DaysToExpire = 30 }; + + // Regressao B-09: tinha DaysUntilExpire (errado) ao inves de DaysToExpire. + JsonContractAssert.DoesNotSerializeKey(request, "daysUntilExpire"); + } + + [Fact] + public void EscrowConfig_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Escrow/config-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(30, result.DaysToExpire); + Assert.True(result.Enabled); + Assert.False(result.IsFeePayer); + } + + [Fact] + public void EscrowConfig_OptionalBoolsAreNullableInResponse() + { + // Apenas daysToExpire e required; enabled e isFeePayer podem vir omitidos. + var json = "{\"daysToExpire\":30}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(30, result.DaysToExpire); + Assert.Null(result.Enabled); + Assert.Null(result.IsFeePayer); + } + + // ───────────────────────────────────────────────────────────── + // GET /v3/payments/{id}/escrow -> PaymentEscrowGetResponseDTO + // Schema: { id, status: enum(ACTIVE|DONE), expirationDate, finishDate, finishReason: enum } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentEscrow_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Escrow/payment-escrow-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("4f468235-cec3-482f-b3d0-348af4c7194", result.Id); + Assert.Equal(EscrowStatus.ACTIVE, result.Status); + Assert.Equal(EscrowFinishReason.EXPIRED, result.FinishReason); + } + + [Fact] + public void EscrowStatus_BothValuesDeserialize() + { + foreach (var status in new[] { "ACTIVE", "DONE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void EscrowFinishReason_AllSixValuesDeserialize() + { + // Schema: CHARGEBACK, EXPIRED, INSUFFICIENT_BALANCE, PAYMENT_REFUNDED, + // REQUESTED_BY_CUSTOMER, CUSTOMER_CONFIG_DISABLED + foreach (var reason in new[] { + "CHARGEBACK", "EXPIRED", "INSUFFICIENT_BALANCE", + "PAYMENT_REFUNDED", "REQUESTED_BY_CUSTOMER", "CUSTOMER_CONFIG_DISABLED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"DONE\",\"finishReason\":\"{reason}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.FinishReason); + Assert.Equal(reason, result.FinishReason.ToString()); + } + } + + [Fact] + public void EscrowFinishReason_NullWhenStatusActive() + { + // Escrow ativo: finishReason vem null (so e preenchido quando status=DONE). + var json = "{\"id\":\"esc_1\",\"status\":\"ACTIVE\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.FinishReason); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/FinanceContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/FinanceContractTests.cs new file mode 100644 index 0000000..6bea43f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/FinanceContractTests.cs @@ -0,0 +1,75 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Finance; +using Codout.Apis.Asaas.Models.Payment.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para FinanceManager (B-37). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class FinanceContractTests +{ + [Fact] + public void BalanceResponse_DeserializesValue() + { + var json = "{\"balance\":5210.96}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(5210.96m, result.Value); + } + + [Fact] + public void PaymentStatistics_DeserializesAllFields() + { + var json = "{\"quantity\":23,\"value\":9270.4,\"netValue\":9121.54}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(23, result.Quantity); + Assert.Equal(9270.4m, result.Value); + Assert.Equal(9121.54m, result.NetValue); + } + + [Fact] + public void SplitStatistics_DeserializesIncomeAndValue() + { + // B-37a: schema usa {income, value}, nao {totalPendingValue, totalReceivedValue} + var json = "{\"income\":5210.96,\"value\":9270.4}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(5210.96m, result.Income); + Assert.Equal(9270.4m, result.Value); + } + + [Fact] + public void PaymentStatisticsFilter_SerializesAllElevenFields() + { + // B-37b: GetPaymentStatistics nao aceitava filtros. Schema oficial expoe 11. + var filter = new PaymentStatisticsFilter + { + CustomerId = "cus_x", + BillingType = BillingType.BOLETO, + Status = PaymentStatus.RECEIVED, + Anticipated = false, + DateCreatedGE = new DateTime(2023, 1, 1), + DateCreatedLE = new DateTime(2023, 12, 31), + DueDateGE = new DateTime(2023, 2, 1), + DueDateLE = new DateTime(2023, 11, 30), + EstimatedCreditDateGE = new DateTime(2023, 3, 1), + EstimatedCreditDateLE = new DateTime(2023, 10, 31), + ExternalReference = "ref_42" + }; + + JsonContractAssert.QueryParamEquals(filter, "customer", "cus_x"); + JsonContractAssert.QueryParamEquals(filter, "billingType", "BOLETO"); + JsonContractAssert.QueryParamEquals(filter, "status", "RECEIVED"); + JsonContractAssert.QueryParamEquals(filter, "anticipated", "false"); + JsonContractAssert.QueryParamEquals(filter, "dateCreated[ge]", "2023-01-01"); + JsonContractAssert.QueryParamEquals(filter, "dateCreated[le]", "2023-12-31"); + JsonContractAssert.QueryParamEquals(filter, "dueDate[ge]", "2023-02-01"); + JsonContractAssert.QueryParamEquals(filter, "dueDate[le]", "2023-11-30"); + JsonContractAssert.QueryParamEquals(filter, "estimatedCreditDate[ge]", "2023-03-01"); + JsonContractAssert.QueryParamEquals(filter, "estimatedCreditDate[le]", "2023-10-31"); + JsonContractAssert.QueryParamEquals(filter, "externalReference", "ref_42"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/FiscalInfoContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/FiscalInfoContractTests.cs new file mode 100644 index 0000000..11c8f16 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/FiscalInfoContractTests.cs @@ -0,0 +1,33 @@ +using Codout.Apis.Asaas.Models.FiscalInfo; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para FiscalInfoManager (B-40). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class FiscalInfoContractTests +{ + [Fact] + public void FiscalInfoResponse_DeserializesAllFieldsWithCorrectTypes() + { + var json = "{\"object\":\"customerFiscalInfo\",\"email\":\"john.doe@asaas.com.br\",\"municipalInscription\":\"21779501\",\"simplesNacional\":false,\"culturalProjectsPromoter\":false,\"cnae\":\"6209100\",\"specialTaxRegime\":\"1\",\"nbsCode\":\"1.0101\",\"rpsSerie\":\"1\",\"rpsNumber\":1,\"loteNumber\":1,\"username\":\"johndoe\",\"passwordSent\":true,\"accessTokenSent\":true,\"certificateSent\":true,\"nationalPortalTaxCalculationRegime\":null}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("customerFiscalInfo", result.Object); + Assert.Equal("john.doe@asaas.com.br", result.Email); + Assert.Equal("21779501", result.MunicipalInscription); + Assert.False(result.SimplesNacional); + Assert.Equal("6209100", result.Cnae); + // B-40a: NbsCode novo + Assert.Equal("1.0101", result.NbsCode); + // B-40b: RpsNumber e LoteNumber agora sao int (antes string) + Assert.Equal(1, result.RpsNumber); + Assert.Equal(1, result.LoteNumber); + // B-40c: bools novos + Assert.True(result.PasswordSent); + Assert.True(result.AccessTokenSent); + Assert.True(result.CertificateSent); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/FixtureLoader.cs b/Codout.Apis.Asaas.Tests/Contract/FixtureLoader.cs new file mode 100644 index 0000000..08f2f27 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/FixtureLoader.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Carrega arquivos JSON do diretorio Fixtures/. As fixtures sao extraidas +/// dos exemplos oficiais da documentacao Asaas (via MCP em docs.asaas.com/mcp) +/// e funcionam como contrato congelado: se a forma do JSON da API mudar, os +/// contract tests falham e nos forcam a atualizar fixture + model juntos. +/// +public static class FixtureLoader +{ + private static readonly string FixturesRoot = Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, + "Fixtures"); + + /// + /// Le uma fixture pelo caminho relativo a Tests/Fixtures/. + /// Ex: Load("Payment/limits-response.json"). + /// + public static string Load(string relativePath) + { + var fullPath = Path.Combine(FixturesRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)); + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException( + $"Fixture nao encontrada: {relativePath}. Caminho esperado: {fullPath}. " + + "Verifique se o arquivo existe em Tests/Fixtures/ e se esta marcado como CopyToOutputDirectory no .csproj."); + } + return File.ReadAllText(fullPath); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/InstallmentContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/InstallmentContractTests.cs new file mode 100644 index 0000000..f8451a2 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/InstallmentContractTests.cs @@ -0,0 +1,44 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Installment; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para InstallmentManager (B-31). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class InstallmentContractTests +{ + [Fact] + public void InstallmentResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Installment/installment-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("installment", result.Object); + Assert.Equal("2765d086-c7c5-5cca-898a-4262d212587c", result.Id); + Assert.Equal(360m, result.Value); + Assert.Equal(312.12m, result.NetValue); + Assert.Equal(30m, result.PaymentValue); + Assert.Equal(12, result.InstallmentCount); + Assert.Equal(BillingType.CREDIT_CARD, result.BillingType); + Assert.Equal(31, result.ExpirationDay); + Assert.Equal(new DateTime(2021, 1, 19), result.DateCreated); + Assert.Equal("cus_000000001645", result.CustomerId); + Assert.Equal("997152082166122", result.PaymentLink); + Assert.False(result.Deleted); + Assert.NotNull(result.Refunds); + } + + [Fact] + public void InstallmentResponse_NullableExpirationDayHandlesMissing() + { + var json = "{\"id\":\"i_x\",\"object\":\"installment\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.ExpirationDay); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs new file mode 100644 index 0000000..db11483 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/InvoiceContractTests.cs @@ -0,0 +1,190 @@ +using System; +using Codout.Apis.Asaas.Models.Common; +using Codout.Apis.Asaas.Models.Invoice; +using Codout.Apis.Asaas.Models.Invoice.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para InvoiceManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/invoices (request InvoiceSaveRequestDTO, response InvoiceGetResponseDTO) +/// - GET /v3/invoices (envelope padrao + filtros) +/// - PUT /v3/invoices/{id} (InvoiceUpdateRequestDTO) +/// - GET /v3/invoices/{id} +/// - POST /v3/invoices/{id}/authorize (body vazio) +/// - POST /v3/invoices/{id}/cancel (body vazio) +/// +public class InvoiceContractTests +{ + // ───────────────────────────────────────────────────────────── + // CreateInvoiceRequest - required: serviceDescription, observations, value, + // deductions, effectiveDate, municipalServiceName, taxes + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateRequest_SerializesAllExpectedKeys() + { + var request = new CreateInvoiceRequest + { + PaymentId = "pay_637959110194", + CustomerId = "cus_000000002750", + ServiceDescription = "Invoice 101940", + Observations = "Monthly for June work.", + Value = 300m, + Deductions = 10m, + EffectiveDate = new DateTime(2024, 8, 20), + MunicipalServiceName = "Systems analysis and development", + MunicipalServiceCode = "1.01", + Taxes = new Taxes { RetainIss = true, Iss = 2m, Pis = 0.65m, Cofins = 3m, Csll = 9m, Inss = 11m, Ir = 1.5m } + }; + + JsonContractAssert.SerializesWithKeys(request, + "payment", "customer", "serviceDescription", "observations", + "value", "deductions", "effectiveDate", + "municipalServiceName", "municipalServiceCode", "taxes"); + } + + [Fact] + public void CreateRequest_UsesPaymentNotPaymentId() + { + // Regressao: campo deve serializar como "payment" (do schema), nao "paymentId". + var request = new CreateInvoiceRequest { PaymentId = "pay_x" }; + JsonContractAssert.SerializesWithKeys(request, "payment"); + JsonContractAssert.DoesNotSerializeKey(request, "paymentId"); + JsonContractAssert.DoesNotSerializeKey(request, "customerId"); + JsonContractAssert.DoesNotSerializeKey(request, "installmentId"); + } + + [Fact] + public void CreateRequest_SupportsUpdatePaymentFlag() + { + // Schema InvoiceSaveRequestDTO inclui updatePayment opcional. + var request = new CreateInvoiceRequest { Value = 100m, UpdatePayment = true }; + JsonContractAssert.SerializesWithKeys(request, "updatePayment"); + } + + // ───────────────────────────────────────────────────────────── + // UpdateInvoiceRequest - InvoiceUpdateRequestDTO (todos opcionais) + // ───────────────────────────────────────────────────────────── + + [Fact] + public void UpdateRequest_SupportsUpdatePaymentFlag() + { + var request = new UpdateInvoiceRequest { UpdatePayment = false }; + JsonContractAssert.SerializesWithKeys(request, "updatePayment"); + } + + // ───────────────────────────────────────────────────────────── + // Invoice response - InvoiceGetResponseDTO + Taxes com Reforma Tributaria + // ───────────────────────────────────────────────────────────── + + [Fact] + public void InvoiceResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Invoice/invoice-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("inv_000000000232", result.Id); + Assert.Equal(InvoiceStatus.SCHEDULED, result.Status); + Assert.Equal("cus_000000002750", result.CustomerId); + Assert.Equal("pay_145059895800", result.PaymentId); + Assert.Equal("NFS-e", result.Type); + Assert.Equal(300m, result.Value); + Assert.Equal(10m, result.Deductions); + Assert.Equal(new DateTime(2024, 8, 15), result.EffectiveDate); + Assert.Equal("1.01", result.MunicipalServiceCode); + + Assert.NotNull(result.Taxes); + } + + [Fact] + public void TaxesResponse_HasAllReformaTributariaFields() + { + // B-21a: schema InvoiceTaxesResponseDTO inclui stateIbs, stateIbsValue, + // municipalIbs, municipalIbsValue, cbs, cbsValue + nbsCode, + // taxSituationCode, taxClassificationCode, operationIndicatorCode, + // pisCofinsRetentionType, pisCofinsTaxStatus. Antes do fix, esses + // campos sumiam silenciosamente na deserializacao. + var json = FixtureLoader.Load("Invoice/invoice-response.json"); + var result = JsonContractAssert.DeserializeFixture(json); + + var t = result.Taxes; + Assert.Equal("1.0101.11.00", t.NbsCode); + Assert.Equal("011", t.TaxSituationCode); + Assert.Equal("011001", t.TaxClassificationCode); + Assert.Equal("020101", t.OperationIndicatorCode); + Assert.True(t.RetainIss); + Assert.Equal(2m, t.Iss); + Assert.Equal("NOT_WITHHELD", t.PisCofinsRetentionType); + Assert.Equal("STANDARD_TAXABLE_OPERATION", t.PisCofinsTaxStatus); + Assert.Equal(0.65m, t.Pis); + Assert.Equal(3m, t.Cofins); + Assert.Equal(9m, t.Csll); + Assert.Equal(11m, t.Inss); + Assert.Equal(1.5m, t.Ir); + Assert.Equal(0.1m, t.StateIbs); + Assert.Equal(0.3m, t.StateIbsValue); + Assert.Equal(0m, t.MunicipalIbs); + Assert.Equal(0m, t.MunicipalIbsValue); + Assert.Equal(0.9m, t.Cbs); + Assert.Equal(2.7m, t.CbsValue); + } + + [Fact] + public void InvoiceStatus_AllSixValuesDeserialize() + { + foreach (var status in new[] { + "SCHEDULED", "AUTHORIZED", "PROCESSING_CANCELLATION", + "CANCELED", "CANCELLATION_DENIED", "ERROR" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // InvoiceListFilter - B-21b/c regression + // Schema usa effectiveDate[Ge] e [Le] com G/L MAIUSCULOS. + // Antes: lowercase [ge]/[le] (silenciosamente ignorado pela API). + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ListFilter_UsesCapitalGeAndLeForEffectiveDate() + { + var filter = new InvoiceListFilter + { + EffectiveDateGE = new DateTime(2024, 8, 3), + EffectiveDateLE = new DateTime(2024, 9, 3) + }; + + JsonContractAssert.QueryParamEquals(filter, "effectiveDate[Ge]", "2024-08-03"); + JsonContractAssert.QueryParamEquals(filter, "effectiveDate[Le]", "2024-09-03"); + + // Regressao B-21b: confirmar que NAO esta serializando com minuscula + Assert.False(filter.ContainsKey("effectiveDate[ge]")); + Assert.False(filter.ContainsKey("effectiveDate[le]")); + } + + [Fact] + public void ListFilter_SupportsCustomerAndExternalReference() + { + // B-21c: faltavam os filtros customer e externalReference. + var filter = new InvoiceListFilter + { + CustomerId = "cus_000000002750", + ExternalReference = "ext_ref_42", + PaymentId = "pay_x", + InstallmentId = "ins_x", + Status = InvoiceStatus.AUTHORIZED + }; + + JsonContractAssert.QueryParamEquals(filter, "customer", "cus_000000002750"); + JsonContractAssert.QueryParamEquals(filter, "externalReference", "ext_ref_42"); + JsonContractAssert.QueryParamEquals(filter, "payment", "pay_x"); + JsonContractAssert.QueryParamEquals(filter, "installment", "ins_x"); + JsonContractAssert.QueryParamEquals(filter, "status", "AUTHORIZED"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/JsonContractAssert.cs b/Codout.Apis.Asaas.Tests/Contract/JsonContractAssert.cs new file mode 100644 index 0000000..756b40f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/JsonContractAssert.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Codout.Apis.Asaas.Core; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Helpers para asserts de contrato JSON. Valida nomes exatos de campos +/// (case-sensitive), tipos de valor, e shape de envelope. Tudo via +/// JsonDocument bruto (sem deserializar para o C# model) para que o teste +/// detecte divergencias mesmo quando o model do SDK as ignora. +/// +public static class JsonContractAssert +{ + /// + /// Serializa o objeto usando as opcoes reais do SDK e assert que o + /// JSON produzido contem exatamente o conjunto de chaves esperado + /// (case-sensitive). Detecta typos como "Nossonumero" vs "nossoNumero" + /// e regressoes onde uma propriedade some. + /// + public static void SerializesWithKeys(object request, params string[] expectedKeys) + { + var json = JsonSerializer.Serialize(request, JsonSerializerConfiguration.Options); + using var doc = JsonDocument.Parse(json); + var actualKeys = doc.RootElement.EnumerateObject().Select(p => p.Name).ToHashSet(); + + foreach (var key in expectedKeys) + { + Assert.True(actualKeys.Contains(key), + $"Esperava chave JSON '{key}' no payload serializado. JSON real: {json}"); + } + } + + /// + /// Asserta que uma chave NAO esta presente no JSON serializado. + /// Util quando uma propriedade do C# nao deve aparecer no wire + /// (ex: nao envia campo nulo). + /// + public static void DoesNotSerializeKey(object request, string forbiddenKey) + { + var json = JsonSerializer.Serialize(request, JsonSerializerConfiguration.Options); + using var doc = JsonDocument.Parse(json); + var actualKeys = doc.RootElement.EnumerateObject().Select(p => p.Name).ToHashSet(); + Assert.False(actualKeys.Contains(forbiddenKey), + $"Chave '{forbiddenKey}' apareceu no payload serializado mas nao deveria. JSON: {json}"); + } + + /// + /// Deserializa o JSON da fixture usando as opcoes reais do SDK e roda + /// o assert para verificar que todas as propriedades esperadas foram + /// preenchidas. Detecta regressoes onde um campo da API some/renomeia. + /// + public static T DeserializeFixture(string fixtureJson) + { + return JsonSerializer.Deserialize(fixtureJson, JsonSerializerConfiguration.Options)!; + } + + /// + /// Asserta que o JSON tem uma chave especifica no root e que ela tem + /// um valor de tipo esperado. Util para validar envelopes minimalistas + /// como { data: [...] } onde nao ha hasMore/totalCount. + /// + public static void HasRootProperty(string json, string propertyName, JsonValueKind expectedKind) + { + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.TryGetProperty(propertyName, out var prop), + $"Esperava propriedade root '{propertyName}'. JSON: {json}"); + Assert.Equal(expectedKind, prop.ValueKind); + } + + /// + /// Asserta que o JSON serializado de um Dictionary<string,string> + /// (ou RequestParameters) contem exatamente o valor esperado para uma chave. + /// Util para detectar "True" vs "true" em query strings. + /// + public static void QueryParamEquals(RequestParameters parameters, string key, string expectedValue) + { + Assert.True(parameters.ContainsKey(key), $"Esperava chave '{key}' em RequestParameters"); + Assert.Equal(expectedValue, parameters[key]); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/MobilePhoneRechargeContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/MobilePhoneRechargeContractTests.cs new file mode 100644 index 0000000..b11dec2 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/MobilePhoneRechargeContractTests.cs @@ -0,0 +1,134 @@ +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.MobilePhoneRecharge; +using Codout.Apis.Asaas.Models.MobilePhoneRecharge.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para MobilePhoneRechargeManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/mobilePhoneRecharges (request: value+phoneNumber required) +/// - GET /v3/mobilePhoneRecharges (envelope padrao) +/// - GET /v3/mobilePhoneRecharges/{id} +/// - POST /v3/mobilePhoneRecharges/{id}/cancel (body vazio) +/// - GET /v3/mobilePhoneRecharges/{phoneNumber}/provider (B-19 regression) +/// +public class MobilePhoneRechargeContractTests +{ + // ───────────────────────────────────────────────────────────── + // POST /v3/mobilePhoneRecharges request + // Required: value, phoneNumber + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateRequest_HasRequiredFields() + { + var request = new CreateMobilePhoneRechargeRequest + { + Value = 15m, + PhoneNumber = "63997365512" + }; + + JsonContractAssert.SerializesWithKeys(request, "value", "phoneNumber"); + } + + [Fact] + public void CreateRequest_NoFakeFields() + { + var request = new CreateMobilePhoneRechargeRequest { Value = 15m, PhoneNumber = "63997365512" }; + + // Schema oficial expoe APENAS value e phoneNumber. Nada de description, + // operator, customer etc. + JsonContractAssert.DoesNotSerializeKey(request, "description"); + JsonContractAssert.DoesNotSerializeKey(request, "operator"); + JsonContractAssert.DoesNotSerializeKey(request, "customer"); + } + + // ───────────────────────────────────────────────────────────── + // GET /v3/mobilePhoneRecharges/{id} -> MobilePhoneRechargeGetResponseDTO + // Schema: { id, value, phoneNumber, status enum, canBeCancelled, operatorName } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void RechargeResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("MobilePhoneRecharge/recharge-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("37c22147-4194-11ec-8061-0242ac120002", result.Id); + Assert.Equal(15m, result.Value); + Assert.Equal("63997365512", result.PhoneNumber); + Assert.Equal(MobilePhoneRechargeStatus.PENDING, result.Status); + Assert.True(result.CanBeCancelled); + Assert.Equal("Vivo", result.OperatorName); + } + + [Fact] + public void RechargesList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("MobilePhoneRecharge/recharges-list-response.json"); + + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Equal(10, response.Limit); + Assert.Equal(0, response.Offset); + Assert.Single(response.Data); + Assert.Equal("37c22147-4194-11ec-8061-0242ac120002", response.Data[0].Id); + } + + [Fact] + public void RechargeStatus_AllFiveValuesDeserialize() + { + // Schema: PENDING, CONFIRMED, CANCELLED, REFUNDED, WAITING_CRITICAL_ACTION + foreach (var status in new[] { "PENDING", "CONFIRMED", "CANCELLED", "REFUNDED", "WAITING_CRITICAL_ACTION" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // GET /v3/mobilePhoneRecharges/{phoneNumber}/provider + // B-19 regression: MobilePhoneProvider tinha AvailableValues: List. + // Schema real: values: array de { name, description, bonus, minValue, maxValue }. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ProviderResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("MobilePhoneRecharge/provider-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("Vivo", result.Name); + Assert.Equal(2, result.Values.Count); + + Assert.Equal("R$ 12,00", result.Values[0].Name); + Assert.Equal("5.0", result.Values[0].Bonus); + Assert.Equal(1m, result.Values[0].MinValue); + Assert.Equal(5m, result.Values[0].MaxValue); + + Assert.Equal("R$ 30,00", result.Values[1].Name); + Assert.Equal("3.0", result.Values[1].Bonus); + Assert.Equal(20m, result.Values[1].MinValue); + Assert.Equal(50m, result.Values[1].MaxValue); + } + + [Fact] + public void ProviderResponse_UsesValuesNotAvailableValues() + { + // Regressao B-19: garantir que o nome JSON e "values" (do schema), + // nao "availableValues" (que era invencao do modelo antigo). + var officialJson = "{\"name\":\"Vivo\",\"values\":[{\"name\":\"R$ 12,00\",\"bonus\":\"5.0\",\"minValue\":1,\"maxValue\":5}]}"; + + var result = JsonContractAssert.DeserializeFixture(officialJson); + + Assert.Single(result.Values); + Assert.Equal("R$ 12,00", result.Values[0].Name); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/MyAccountContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/MyAccountContractTests.cs new file mode 100644 index 0000000..8a53f18 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/MyAccountContractTests.cs @@ -0,0 +1,62 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.MyAccount; +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para MyAccountManager (resto alem de Documents §7) — B-38. +/// Schemas verificados via MCP em 2026-05-24. +/// +public class MyAccountContractTests +{ + [Fact] + public void CommercialInfoResponse_DeserializesAllFields() + { + var json = "{\"status\":\"APPROVED\",\"personType\":\"JURIDICA\",\"cpfCnpj\":\"66625514000140\",\"name\":\"John Doe\",\"birthDate\":\"1995-04-12\",\"companyName\":null,\"companyType\":\"MEI\",\"incomeValue\":250000,\"email\":\"john.doe@asaas.com.br\",\"phone\":null,\"mobilePhone\":null,\"postalCode\":\"89223005\",\"address\":\"Av. Rolf Wiest\",\"addressNumber\":\"659\",\"complement\":null,\"province\":\"Bom retiro\",\"city\":null,\"denialReason\":null,\"tradingName\":null,\"site\":null,\"availableCompanyNames\":[\"ASAAS\",\"ASAAS GESTAO FINANCEIRA S.A.\"],\"commercialInfoExpiration\":{\"isExpired\":false,\"scheduledDate\":\"2025-05-05 00:00:00\"}}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + // B-38a: Status era string. Schema e enum AccountInfoStatus. + Assert.Equal(AccountInfoStatus.APPROVED, result.Status); + Assert.Equal(PersonType.JURIDICA, result.PersonType); + Assert.Equal("66625514000140", result.CpfCnpj); + Assert.Equal("John Doe", result.Name); + Assert.Equal(new DateTime(1995, 4, 12), result.BirthDate); + Assert.Equal(CompanyType.MEI, result.CompanyType); + Assert.Equal(250000m, result.IncomeValue); + Assert.Equal("89223005", result.PostalCode); + // B-38b: campos novos + Assert.Equal(2, result.AvailableCompanyNames.Count); + Assert.NotNull(result.CommercialInfoExpiration); + Assert.False(result.CommercialInfoExpiration.IsExpired); + Assert.Equal(new DateTime(2025, 5, 5), result.CommercialInfoExpiration.ScheduledDate); + } + + [Fact] + public void AccountInfoStatus_AllFourValuesDeserialize() + { + foreach (var status in new[] { "APPROVED", "AWAITING_ACTION_AUTHORIZATION", "DENIED", "PENDING" }) + { + var json = $"{{\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Status); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void AccountStatus_AllFourApprovalStatusValuesDeserialize() + { + // AccountStatus (GET /myAccount/status) ja usava AccountApprovalStatus + // pre-existente — apenas confirmar 4 valores. + foreach (var status in new[] { "PENDING", "APPROVED", "REJECTED", "AWAITING_APPROVAL" }) + { + var json = $"{{\"id\":\"x\",\"commercialInfo\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.CommercialInfo); + Assert.Equal(status, result.CommercialInfo.ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/NotificationContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/NotificationContractTests.cs new file mode 100644 index 0000000..c191824 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/NotificationContractTests.cs @@ -0,0 +1,60 @@ +using Codout.Apis.Asaas.Models.Notification; +using Codout.Apis.Asaas.Models.Notification.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para NotificationManager + Notification model (B-34). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class NotificationContractTests +{ + [Fact] + public void NotificationResponse_DeserializesAllFields() + { + var json = "{\"object\":\"notification\",\"id\":\"not_wuGp97JeCr7G\",\"customer\":\"cus_000005401844\",\"enabled\":true,\"emailEnabledForProvider\":true,\"smsEnabledForProvider\":true,\"emailEnabledForCustomer\":true,\"smsEnabledForCustomer\":true,\"phoneCallEnabledForCustomer\":false,\"whatsappEnabledForCustomer\":false,\"event\":\"PAYMENT_CREATED\",\"scheduleOffset\":1,\"deleted\":false}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("notification", result.Object); + Assert.Equal("not_wuGp97JeCr7G", result.Id); + Assert.Equal("cus_000005401844", result.Customer); + Assert.True(result.Enabled); + Assert.True(result.EmailEnabledForProvider); + Assert.False(result.PhoneCallEnabledForCustomer); + Assert.False(result.WhatsappEnabledForCustomer); + Assert.Equal(NotificationEvent.PAYMENT_CREATED, result.Event); + Assert.Equal(1, result.ScheduleOffset); + Assert.False(result.Deleted); + } + + [Fact] + public void NotificationEvent_AllSixValuesDeserialize() + { + // B-34a: campo event nao existia no model. + // Schema: PAYMENT_CREATED, PAYMENT_UPDATED, PAYMENT_RECEIVED, + // PAYMENT_OVERDUE, PAYMENT_DUEDATE_WARNING, SEND_LINHA_DIGITAVEL + foreach (var ev in new[] { + "PAYMENT_CREATED", "PAYMENT_UPDATED", "PAYMENT_RECEIVED", + "PAYMENT_OVERDUE", "PAYMENT_DUEDATE_WARNING", "SEND_LINHA_DIGITAVEL" }) + { + var json = $"{{\"id\":\"x\",\"event\":\"{ev}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Event); + Assert.Equal(ev, result.Event.ToString()); + } + } + + [Fact] + public void UpdateRequest_BoolsAreNullableNotForcingFalse() + { + // B-34b: bool non-nullable forcava false em todo update parcial. + var request = new UpdateNotificationRequest { Enabled = true }; + + JsonContractAssert.SerializesWithKeys(request, "enabled"); + // Outros bools nao setados nao devem aparecer no JSON + JsonContractAssert.DoesNotSerializeKey(request, "emailEnabledForProvider"); + JsonContractAssert.DoesNotSerializeKey(request, "smsEnabledForProvider"); + JsonContractAssert.DoesNotSerializeKey(request, "phoneCallEnabledForCustomer"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PaymentContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PaymentContractTests.cs new file mode 100644 index 0000000..7e6f2bb --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PaymentContractTests.cs @@ -0,0 +1,262 @@ +using System; +using System.Text.Json; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Invoice.Enums; +using Codout.Apis.Asaas.Models.Payment; +using Codout.Apis.Asaas.Models.Payment.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para endpoints novos do PaymentManager auditados na +/// rodada final (/v3/payments/limits e /v3/payments/simulate). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class PaymentContractTests +{ + // ───────────────────────────────────────────────────────────── + // GET /v3/payments/limits -> PaymentLimits + // Schema: { creation: { daily: { limit, used, wasReached } } } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentLimits_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Payment/limits-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.NotNull(result.Creation); + Assert.NotNull(result.Creation.Daily); + Assert.Equal(10, result.Creation.Daily.Limit); + Assert.Equal(5, result.Creation.Daily.Used); + Assert.False(result.Creation.Daily.WasReached); + } + + // ───────────────────────────────────────────────────────────── + // POST /v3/payments/simulate -> SimulatedPayment + // Request: { value, billingTypes: [...], installmentCount? } + // Response: { value, creditCard?: {...}, bankSlip?: {...}, pix?: {...} } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void SimulatePaymentRequest_Minimal_HasExactKeys() + { + var request = new SimulatePaymentRequest + { + Value = 100m, + BillingTypes = [BillingType.CREDIT_CARD, BillingType.BOLETO, BillingType.PIX] + }; + + JsonContractAssert.SerializesWithKeys(request, "value", "billingTypes"); + } + + [Fact] + public void SimulatePaymentRequest_BillingTypesIsArray_NotSingular() + { + var request = new SimulatePaymentRequest + { + Value = 100m, + BillingTypes = [BillingType.PIX] + }; + + // Regressao B-03: havia campo BillingType singular antes. + JsonContractAssert.SerializesWithKeys(request, "billingTypes"); + JsonContractAssert.DoesNotSerializeKey(request, "billingType"); + JsonContractAssert.DoesNotSerializeKey(request, "discountValue"); + JsonContractAssert.DoesNotSerializeKey(request, "splits"); + } + + [Fact] + public void SimulatePaymentRequest_BillingTypesSerializesAsUppercaseEnums() + { + var request = new SimulatePaymentRequest + { + Value = 100m, + BillingTypes = [BillingType.CREDIT_CARD, BillingType.BOLETO] + }; + + var json = JsonSerializer.Serialize(request, JsonSerializerConfiguration.Options); + Assert.Contains("\"CREDIT_CARD\"", json); + Assert.Contains("\"BOLETO\"", json); + } + + [Fact] + public void SimulatedPaymentResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Payment/simulate-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal(100m, result.Value); + + Assert.NotNull(result.CreditCard); + Assert.Equal(100m, result.CreditCard.NetValue); + Assert.Equal(2.49m, result.CreditCard.FeePercentage); + Assert.Equal(0.49m, result.CreditCard.OperationFee); + Assert.Equal(50m, result.CreditCard.Installment.PaymentValue); + Assert.Equal(48.52m, result.CreditCard.Installment.PaymentNetValue); + + Assert.NotNull(result.BankSlip); + Assert.Equal(98.02m, result.BankSlip.NetValue); + Assert.Equal(0.99m, result.BankSlip.FeeValue); + + Assert.NotNull(result.Pix); + Assert.Equal(98.02m, result.Pix.NetValue); + Assert.Null(result.Pix.FeePercentage); + Assert.Equal(0.99m, result.Pix.FeeValue); + } + + // ───────────────────────────────────────────────────────────── + // Error envelope: { errors: [{ code, description }] } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ErrorResponse_FromOfficialFixture_DeserializesViaResponseObject() + { + var json = FixtureLoader.Load("error-response.json"); + + var response = new ResponseObject( + System.Net.HttpStatusCode.BadRequest, json); + + Assert.False(response.WasSuccessful()); + Assert.Single(response.Errors); + Assert.Equal("invalid_object", response.Errors[0].Code); + Assert.Equal("Informe o número de parcelas.", response.Errors[0].Description); + } + + // ───────────────────────────────────────────────────────────── + // B-26: Payment response + List filter audit (fase 8) + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Payment/payment-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("payment", result.Object); + Assert.Equal("pay_080225913252", result.Id); + Assert.Equal(new DateTime(2017, 3, 10), result.DateCreated); + Assert.Equal("cus_G7Dvo4iphUNk", result.CustomerId); + Assert.Equal(129.9m, result.Value); + Assert.Equal(124.9m, result.NetValue); + Assert.Equal(BillingType.BOLETO, result.BillingType); + Assert.Equal(PaymentStatus.PENDING, result.Status); + Assert.Equal(new DateTime(2017, 6, 10), result.DueDate); + Assert.Equal(new DateTime(2017, 6, 10), result.OriginalDueDate); + Assert.Equal("https://www.asaas.com/i/080225913252", result.InvoiceUrl); + Assert.Equal("6453", result.NossoNumero); + Assert.False(result.Deleted); + } + + [Fact] + public void PaymentResponse_NullableDatesHandleMissing() + { + // B-26a/b/c regression: DateCreated/DueDate/OriginalDueDate eram non-nullable. + var json = "{\"id\":\"pay_x\",\"status\":\"PENDING\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.DateCreated); + Assert.Null(result.DueDate); + Assert.Null(result.OriginalDueDate); + } + + [Fact] + public void PaymentsList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("Payment/payments-list-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Single(response.Data); + } + + [Fact] + public void BillingType_AllSevenValuesDeserialize() + { + foreach (var bt in new[] { + "UNDEFINED", "BOLETO", "CREDIT_CARD", "DEBIT_CARD", + "TRANSFER", "DEPOSIT", "PIX" }) + { + var json = $"{{\"id\":\"x\",\"billingType\":\"{bt}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(bt, result.BillingType.ToString()); + } + } + + [Fact] + public void PaymentStatus_AllFourteenValuesDeserialize() + { + var values = new[] { + "PENDING", "RECEIVED", "CONFIRMED", "OVERDUE", "REFUNDED", + "RECEIVED_IN_CASH", "REFUND_REQUESTED", "REFUND_IN_PROGRESS", + "CHARGEBACK_REQUESTED", "CHARGEBACK_DISPUTE", "AWAITING_CHARGEBACK_REVERSAL", + "DUNNING_REQUESTED", "DUNNING_RECEIVED", "AWAITING_RISK_ANALYSIS" + }; + foreach (var status in values) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void ListFilter_NewFieldsSerialize() + { + // B-26d: customerGroupName, invoiceStatus, estimatedCreditDate, + // pixQrCodeId, anticipable, user, checkoutSession - estavam faltando. + var filter = new PaymentListFilter + { + CustomerGroupName = "vip", + InvoiceStatus = InvoiceStatus.AUTHORIZED, + EstimatedCreditDate = new DateTime(2024, 1, 1), + PixQrCodeId = "qr_42", + Anticipable = true, + User = "operator@example.com", + CheckoutSession = "co_42" + }; + + JsonContractAssert.QueryParamEquals(filter, "customerGroupName", "vip"); + JsonContractAssert.QueryParamEquals(filter, "invoiceStatus", "AUTHORIZED"); + JsonContractAssert.QueryParamEquals(filter, "estimatedCreditDate", "2024-01-01"); + JsonContractAssert.QueryParamEquals(filter, "pixQrCodeId", "qr_42"); + JsonContractAssert.QueryParamEquals(filter, "anticipable", "true"); + JsonContractAssert.QueryParamEquals(filter, "user", "operator@example.com"); + JsonContractAssert.QueryParamEquals(filter, "checkoutSession", "co_42"); + } + + [Fact] + public void ListFilter_AllDateRangeFiltersUseLowercaseGeLe() + { + // Schema oficial Payment usa [ge]/[le] LOWERCASE (diferente de Invoice + // que usa [Ge]/[Le] uppercase). Este teste congela esse padrao. + var filter = new PaymentListFilter + { + DateCreatedGE = new DateTime(2024, 1, 1), + DateCreatedLE = new DateTime(2024, 12, 31), + PaymentDateGE = new DateTime(2024, 2, 1), + PaymentDateLE = new DateTime(2024, 11, 30), + EstimatedCreditDateGE = new DateTime(2024, 3, 1), + EstimatedCreditDateLE = new DateTime(2024, 10, 31), + DueDateGE = new DateTime(2024, 4, 1), + DueDateLE = new DateTime(2024, 9, 30) + }; + + JsonContractAssert.QueryParamEquals(filter, "dateCreated[ge]", "2024-01-01"); + JsonContractAssert.QueryParamEquals(filter, "dateCreated[le]", "2024-12-31"); + JsonContractAssert.QueryParamEquals(filter, "paymentDate[ge]", "2024-02-01"); + JsonContractAssert.QueryParamEquals(filter, "paymentDate[le]", "2024-11-30"); + JsonContractAssert.QueryParamEquals(filter, "estimatedCreditDate[ge]", "2024-03-01"); + JsonContractAssert.QueryParamEquals(filter, "estimatedCreditDate[le]", "2024-10-31"); + JsonContractAssert.QueryParamEquals(filter, "dueDate[ge]", "2024-04-01"); + JsonContractAssert.QueryParamEquals(filter, "dueDate[le]", "2024-09-30"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PaymentDunningContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PaymentDunningContractTests.cs new file mode 100644 index 0000000..ee40634 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PaymentDunningContractTests.cs @@ -0,0 +1,225 @@ +using System; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.PaymentDunning; +using Codout.Apis.Asaas.Models.PaymentDunning.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para PaymentDunningManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/paymentDunnings (multipart, SaveRequest com 9 campos obrigatorios) +/// - GET /v3/paymentDunnings (envelope padrao, filtros status/type/payment/datas) +/// - POST /v3/paymentDunnings/simulate (payment como QUERY param, body vazio!) +/// - GET /v3/paymentDunnings/{id} +/// - GET /v3/paymentDunnings/{id}/history (status enum NEGOTIATED/PAID/etc) +/// - GET /v3/paymentDunnings/{id}/partialPayments +/// - GET /v3/paymentDunnings/paymentsAvailableForDunning +/// - POST /v3/paymentDunnings/{id}/cancel (body vazio) +/// +public class PaymentDunningContractTests +{ + // ───────────────────────────────────────────────────────────── + // PaymentDunning response shape - B-22a/b/c/d regression coverage + // ───────────────────────────────────────────────────────────── + + [Fact] + public void DunningResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PaymentDunning/dunning-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("ce35702d-0d9f-475a-ba46-e251ad265c91", result.Id); + // B-22a: DunningNumber agora e int? (schema integer), antes era string. + Assert.Equal(15, result.DunningNumber); + Assert.Equal(PaymentDunningStatus.PENDING, result.Status); + Assert.Equal(PaymentDunningType.CREDIT_BUREAU, result.Type); + Assert.Equal(new DateTime(2020, 5, 26), result.RequestDate); + Assert.Equal(80m, result.Value); + Assert.Equal(8m, result.FeeValue); + Assert.Equal(72m, result.NetValue); + Assert.Equal("pay_080225913252", result.PaymentId); + // B-22c/d: nullable bools + Assert.True(result.CanBeCancelled); + Assert.False(result.IsNecessaryResendDocumentation); + // B-22b: CannotBeCancelledReason novo campo + Assert.Null(result.CannotBeCancelledReason); + } + + [Fact] + public void DunningResponse_HandlesNullableBools() + { + // B-22c/d: canBeCancelled e isNecessaryResendDocumentation podem vir + // omitidos no JSON. Antes do fix, eram bool nao-nulavel, o que + // forcaria false silenciosamente. + var json = "{\"id\":\"x\",\"status\":\"PENDING\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.CanBeCancelled); + Assert.Null(result.IsNecessaryResendDocumentation); + } + + [Fact] + public void DunningsList_UsesStandardEnvelopeWithPagination() + { + var json = FixtureLoader.Load("PaymentDunning/dunnings-list-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Single(response.Data); + } + + [Fact] + public void DunningStatus_AllEightValuesDeserialize() + { + foreach (var status in new[] { + "PENDING", "AWAITING_APPROVAL", "AWAITING_CANCELLATION", "PROCESSED", + "PAID", "PARTIALLY_PAID", "DENIED", "CANCELLED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void DunningType_BothValuesDeserialize() + { + // Save/response so retornam CREDIT_BUREAU. DEBT_RECOVERY_ASSISTANCE + // existe apenas no filter, mas o enum precisa aceitar ambos para + // que o filter compile. + foreach (var type in new[] { "CREDIT_BUREAU", "DEBT_RECOVERY_ASSISTANCE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"PENDING\",\"type\":\"{type}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(type, result.Type.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // PaymentDunningEventHistory - B-22f regression + // Status era string, deve ser PaymentDunningHistoryStatus enum. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void HistoryResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PaymentDunning/history-list-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Single(response.Data); + Assert.Equal(PaymentDunningHistoryStatus.NEGOTIATED, response.Data[0].Status); + Assert.Equal(new DateTime(2019, 2, 20), response.Data[0].EventDate); + } + + [Fact] + public void HistoryStatus_AllFourValuesDeserialize() + { + // Schema: IN_NEGOTIATION, NEGOTIATION_FAIL, NEGOTIATED, PAID + foreach (var status in new[] { "IN_NEGOTIATION", "NEGOTIATION_FAIL", "NEGOTIATED", "PAID" }) + { + var json = $"{{\"status\":\"{status}\",\"eventDate\":\"2024-01-01\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // Partial payments response (renegotiation) + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PartialPaymentsResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PaymentDunning/partial-payments-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Single(response.Data); + Assert.Equal(800m, response.Data[0].Value); + Assert.Equal(new DateTime(2020, 2, 10), response.Data[0].PaymentDate); + } + + // ───────────────────────────────────────────────────────────── + // Payments available for dunning + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentsAvailableResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PaymentDunning/payments-available-response.json"); + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Single(response.Data); + + var item = response.Data[0]; + Assert.Equal("pay_856437540297", item.PaymentId); + Assert.Equal("cus_000000001663", item.CustomerId); + Assert.Equal(250m, item.Value); + Assert.Equal(new DateTime(2020, 5, 18), item.DueDate); + Assert.NotNull(item.TypeSimulations); + } + + // ───────────────────────────────────────────────────────────── + // Simulate response + // ───────────────────────────────────────────────────────────── + + [Fact] + public void SimulateResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PaymentDunning/simulate-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("pay_080225913252", result.PaymentId); + Assert.Equal(80m, result.Value); + // B-22m: typeSimulations e ARRAY no schema, nao objeto unico. + Assert.Equal(2, result.TypeSimulations.Count); + Assert.True(result.TypeSimulations[0].IsAllowed); + Assert.False(result.TypeSimulations[1].IsAllowed); + Assert.NotEmpty(result.TypeSimulations[1].NotAllowedReason); + } + + // ───────────────────────────────────────────────────────────── + // Create request - 9 campos obrigatorios + opcionais + documents + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateRequest_UsesPaymentNotPaymentId() + { + // Regressao: campo deve serializar como "payment" (do schema), + // nao "paymentId". + var request = new CreatePaymentDunningRequest { PaymentId = "pay_x" }; + JsonContractAssert.SerializesWithKeys(request, "payment"); + JsonContractAssert.DoesNotSerializeKey(request, "paymentId"); + } + + // ───────────────────────────────────────────────────────────── + // List filter + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ListFilter_SerializesAllFieldsWithCorrectNames() + { + var filter = new PaymentDunningListFilter + { + Status = PaymentDunningStatus.PAID, + Type = PaymentDunningType.CREDIT_BUREAU, + PaymentId = "pay_1", + RequestStartDate = new DateTime(2024, 1, 1), + RequestEndDate = new DateTime(2024, 12, 31) + }; + + JsonContractAssert.QueryParamEquals(filter, "status", "PAID"); + JsonContractAssert.QueryParamEquals(filter, "type", "CREDIT_BUREAU"); + JsonContractAssert.QueryParamEquals(filter, "payment", "pay_1"); + JsonContractAssert.QueryParamEquals(filter, "requestStartDate", "2024-01-01"); + JsonContractAssert.QueryParamEquals(filter, "requestEndDate", "2024-12-31"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PaymentLinkContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PaymentLinkContractTests.cs new file mode 100644 index 0000000..7ab8650 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PaymentLinkContractTests.cs @@ -0,0 +1,65 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.PaymentLink; +using Codout.Apis.Asaas.Models.PaymentLink.Enums; +using Codout.Apis.Asaas.Models.Subscription.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para PaymentLinkManager (B-36). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class PaymentLinkContractTests +{ + [Fact] + public void PaymentLinkResponse_DeserializesAllFields() + { + var json = "{\"id\":\"725104409743\",\"name\":\"Book sales\",\"value\":50,\"active\":true,\"chargeType\":\"DETACHED\",\"url\":\"https://www.asaas.com/c/291089675759\",\"billingType\":\"UNDEFINED\",\"subscriptionCycle\":\"MONTHLY\",\"description\":\"Any book for just R$: 50.00\",\"endDate\":\"2024-09-05\",\"deleted\":false,\"viewCount\":0,\"maxInstallmentCount\":1,\"dueDateLimitDays\":10,\"notificationEnabled\":true,\"isAddressRequired\":true,\"externalReference\":\"056984\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("725104409743", result.Id); + Assert.Equal("Book sales", result.Name); + Assert.Equal(50m, result.Value); + Assert.True(result.Active); + Assert.Equal(ChargeType.DETACHED, result.ChargeType); + Assert.Equal("https://www.asaas.com/c/291089675759", result.Url); + Assert.Equal(BillingType.UNDEFINED, result.BillingType); + Assert.Equal(Cycle.MONTHLY, result.SubscriptionCycle); + Assert.Equal(new DateTime(2024, 9, 5), result.EndDate); + Assert.False(result.Deleted); + Assert.Equal(0, result.ViewCount); + Assert.Equal(1, result.MaxInstallmentCount); + Assert.Equal(10, result.DueDateLimitDays); + Assert.True(result.NotificationEnabled); + Assert.True(result.IsAddressRequired); + Assert.Equal("056984", result.ExternalReference); + } + + [Fact] + public void ChargeType_AllThreeValuesDeserialize() + { + foreach (var ct in new[] { "DETACHED", "RECURRENT", "INSTALLMENT" }) + { + var json = $"{{\"id\":\"x\",\"chargeType\":\"{ct}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(ct, result.ChargeType.ToString()); + } + } + + [Fact] + public void SubscriptionCycle_AllSevenValuesDeserialize() + { + // B-36a: SubscriptionCycle era string. Schema e Cycle enum. + foreach (var cycle in new[] { + "WEEKLY", "BIWEEKLY", "MONTHLY", "BIMONTHLY", + "QUARTERLY", "SEMIANNUALLY", "YEARLY" }) + { + var json = $"{{\"id\":\"x\",\"subscriptionCycle\":\"{cycle}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.SubscriptionCycle); + Assert.Equal(cycle, result.SubscriptionCycle.ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PixAutomaticContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PixAutomaticContractTests.cs new file mode 100644 index 0000000..39570bd --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PixAutomaticContractTests.cs @@ -0,0 +1,187 @@ +using System; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.PixAutomatic; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para PixAutomaticManager. +/// Schemas verificados via MCP em 2026-05-24. +/// +public class PixAutomaticContractTests +{ + // ───────────────────────────────────────────────────────────── + // POST /v3/pix/automatic/authorizations + // Required: frequency, contractId, startDate, customerId, immediateQrCode + // ───────────────────────────────────────────────────────────── + + [Fact] + public void CreateAuthorizationRequest_Minimal_HasRequiredFields() + { + var request = new CreatePixAutomaticAuthorizationRequest + { + Frequency = PixAutomaticRecurringFrequency.MONTHLY, + ContractId = "CONTRACT-123", + StartDate = new DateTime(2024, 1, 1), + CustomerId = "cus_000005735721", + ImmediateQrCode = new CreatePixAutomaticImmediateQrCodeRequest + { + ExpirationSeconds = 3600, + OriginalValue = 100m + } + }; + + JsonContractAssert.SerializesWithKeys(request, + "frequency", "contractId", "startDate", "customerId", "immediateQrCode"); + } + + [Fact] + public void CreateAuthorizationRequest_NoFakeFields() + { + var request = new CreatePixAutomaticAuthorizationRequest + { + Frequency = PixAutomaticRecurringFrequency.MONTHLY, + ContractId = "x", + StartDate = new DateTime(2026, 1, 1), + CustomerId = "cus_x", + ImmediateQrCode = new CreatePixAutomaticImmediateQrCodeRequest { ExpirationSeconds = 60, OriginalValue = 1m } + }; + + // Regressao B-06: havia FixedValue, MaximumValue, Periodicity, ExpirationDate, Customer (sem Id) + JsonContractAssert.DoesNotSerializeKey(request, "fixedValue"); + JsonContractAssert.DoesNotSerializeKey(request, "maximumValue"); + JsonContractAssert.DoesNotSerializeKey(request, "periodicity"); + JsonContractAssert.DoesNotSerializeKey(request, "expirationDate"); + JsonContractAssert.DoesNotSerializeKey(request, "customer"); + } + + [Fact] + public void AuthorizationResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PixAutomatic/authorization-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("a33047b1-fb19-4b68-9373-a7ba8a8162aa", result.Id); + Assert.Equal(50m, result.MinLimitValue); + Assert.Null(result.CancellationDate); + Assert.Equal("XXXYYYY1234", result.ContractId); + Assert.Equal("cus_000005735721", result.CustomerId); + Assert.Equal("Music and Movie Streaming Services", result.Description); + Assert.Equal(new DateTime(2024, 12, 31), result.FinishDate); + Assert.Equal(PixAutomaticRecurringFrequency.MONTHLY, result.Frequency); + Assert.Equal("RR1234567820240115abcdefghijk", result.EndToEndIdentifier); + Assert.Equal(new DateTime(2024, 1, 1), result.StartDate); + Assert.Equal(PixAutomaticAuthorizationStatus.ACTIVE, result.Status); + Assert.Equal(100m, result.Value); + + Assert.NotNull(result.ImmediateQrCode); + Assert.Equal("E12345678202401011234567890123456", result.ImmediateQrCode.ConciliationIdentifier); + + Assert.Equal(PixAutomaticOriginType.IMMEDIATE_PAYMENT_AND_RECURRING_QR_CODE, result.OriginType); + Assert.Equal("sub_000005735721", result.SubscriptionId); + } + + [Fact] + public void AuthorizationsListResponse_UsesStandardEnvelopeWithPagination() + { + // Diferente de PixRecurring/items e MyAccount/documents que usam {data:[...]}, + // este endpoint usa o envelope padrao com hasMore/totalCount/limit/offset. + var json = FixtureLoader.Load("PixAutomatic/authorizations-list-response.json"); + + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Equal(10, response.Limit); + Assert.Equal(0, response.Offset); + Assert.Single(response.Data); + Assert.Equal("a33047b1-fb19-4b68-9373-a7ba8a8162aa", response.Data[0].Id); + } + + [Fact] + public void AuthorizationStatus_AllFiveValuesDeserialize() + { + foreach (var status in new[] { "CREATED", "ACTIVE", "CANCELLED", "REFUSED", "EXPIRED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void AuthorizationFrequency_AllFiveValuesDeserialize() + { + foreach (var freq in new[] { "WEEKLY", "MONTHLY", "QUARTERLY", "SEMIANNUALLY", "ANNUALLY" }) + { + var json = $"{{\"id\":\"x\",\"frequency\":\"{freq}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Frequency); + Assert.Equal(freq, result.Frequency.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // PaymentInstruction (B-16 regression coverage) + // Schema: { id, endToEndIdentifier, authorization: {id, e2e, customerId}, + // dueDate, status enum, paymentId, refusalReason } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentInstruction_DeserializesFromOfficialFixture_WithNestedAuthorization() + { + var json = FixtureLoader.Load("PixAutomatic/payment-instruction-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("d1c9c4b2-6a97-4573-9d6d-26bb64f04c28", result.Id); + Assert.Equal("E00416968202111161635q5bk0brYk2C", result.EndToEndIdentifier); + Assert.Equal(new DateTime(2020, 1, 31), result.DueDate); + Assert.Equal(PixAutomaticPaymentInstructionStatus.SCHEDULED, result.Status); + Assert.Equal("pay_tsp88gie3b5e6o2p", result.PaymentId); + Assert.Null(result.RefusalReason); + + Assert.NotNull(result.Authorization); + Assert.Equal("35363f6e-93e2-11ec-b9d9-96f4053b1bd4", result.Authorization.Id); + Assert.Equal("RR1234567820240115abcdefghijk", result.Authorization.EndToEndIdentifier); + Assert.Equal("cus_000005735721", result.Authorization.CustomerId); + } + + [Fact] + public void PaymentInstructionStatus_AllFiveValuesDeserialize() + { + foreach (var status in new[] { "AWAITING_REQUEST", "SCHEDULED", "DONE", "CANCELLED", "REFUSED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // PaymentInstructionListFilter — regression B-17 + // API espera: authorizationId, customerId, paymentId, status + // Antes: authorization, status (errado) + // ───────────────────────────────────────────────────────────── + + [Fact] + public void PaymentInstructionListFilter_SerializesAuthorizationIdNotAuthorization() + { + var filter = new PixAutomaticPaymentInstructionListFilter + { + AuthorizationId = "auth_1", + CustomerId = "cus_1", + PaymentId = "pay_1", + Status = PixAutomaticPaymentInstructionStatus.SCHEDULED + }; + + JsonContractAssert.QueryParamEquals(filter, "authorizationId", "auth_1"); + JsonContractAssert.QueryParamEquals(filter, "customerId", "cus_1"); + JsonContractAssert.QueryParamEquals(filter, "paymentId", "pay_1"); + JsonContractAssert.QueryParamEquals(filter, "status", "SCHEDULED"); + Assert.False(filter.ContainsKey("authorization")); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PixContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PixContractTests.cs new file mode 100644 index 0000000..9e016e9 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PixContractTests.cs @@ -0,0 +1,138 @@ +using System; +using Codout.Apis.Asaas.Models.Pix; +using Codout.Apis.Asaas.Models.Pix.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para PixManager (B-28). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class PixContractTests +{ + [Fact] + public void TransactionResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Pix/transaction-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("35363f6e-93e2-11ec-b9d9-96f4053b1bd4", result.Id); + Assert.Equal("E00416968202111161635q5bk0brYk2C", result.EndToEndIdentifier); + Assert.Equal(PixTransactionFinality.WITHDRAWAL, result.Finality); + Assert.Equal(10m, result.Value); + Assert.Equal(0m, result.RefundedValue); + Assert.Equal(new DateTime(2022, 1, 13, 10, 49, 59), result.EffectiveDate); + Assert.Equal(new DateTime(2022, 10, 18), result.ScheduledDate); + Assert.Equal(PixTransactionStatus.DONE, result.Status); + Assert.Equal(PixTransactionType.DEBIT, result.Type); + Assert.Equal(PixTransactionOriginType.DYNAMIC_QRCODE, result.OriginType); + Assert.Equal("dcabae5bbfb6nffbb87c693883656483", result.ConciliationIdentifier); + Assert.True(result.CanBeCanceled); + Assert.True(result.CanBeRefunded); + Assert.Equal(0.99m, result.ChargedFeeValue); + Assert.Equal("pay_0491859546906926", result.Payment); + Assert.Equal(new DateTime(2023, 2, 14, 10, 42, 55), result.DateCreated); + + Assert.NotNull(result.ExternalAccount); + Assert.Equal("416968", result.ExternalAccount.Ispb); + Assert.Equal("Example Bank S.A", result.ExternalAccount.IspbName); + Assert.Equal("John Doe", result.ExternalAccount.Name); + Assert.Equal(PixAddressKeyType.CPF, result.ExternalAccount.AddressKeyType); + } + + [Fact] + public void TransactionStatus_AllElevenValuesDeserialize() + { + // B-28a regression: enum tinha apenas 5 valores (PENDING, DONE, CANCELLED, + // SCHEDULED, FAILED). PENDING e FAILED nem existem no schema; faltavam 8 + // valores corretos. Sem o fix, deserializar AWAITING_BALANCE_VALIDATION + // ou REQUESTED ou REFUSED lancava exception. + foreach (var status in new[] { + "AWAITING_BALANCE_VALIDATION", "AWAITING_INSTANT_PAYMENT_ACCOUNT_BALANCE", + "AWAITING_CRITICAL_ACTION_AUTHORIZATION", "AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST", + "AWAITING_CASH_IN_RISK_ANALYSIS_REQUEST", "SCHEDULED", "AWAITING_REQUEST", + "REQUESTED", "DONE", "REFUSED", "CANCELLED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void TransactionType_AllFiveValuesDeserialize() + { + foreach (var type in new[] { + "DEBIT", "CREDIT", "CREDIT_REFUND", "DEBIT_REFUND", "DEBIT_REFUND_CANCELLATION" }) + { + var json = $"{{\"id\":\"x\",\"type\":\"{type}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Type); + Assert.Equal(type, result.Type.ToString()); + } + } + + [Fact] + public void TransactionOriginType_AllSixValuesDeserialize() + { + foreach (var origin in new[] { + "MANUAL", "ADDRESS_KEY", "STATIC_QRCODE", "DYNAMIC_QRCODE", + "PAYMENT_INITIATION_SERVICE", "AUTOMATIC_RECURRING" }) + { + var json = $"{{\"id\":\"x\",\"originType\":\"{origin}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.OriginType); + Assert.Equal(origin, result.OriginType.ToString()); + } + } + + [Fact] + public void AddressKeyResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Pix/address-key-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("a33047b1-fb19-4b68-9373-a7ba8a8162aa", result.Id); + Assert.Equal("b6295ee1-f054-47d1-9e90-ee57b74f60d9", result.Key); + Assert.Equal(PixAddressKeyType.EVP, result.Type); + Assert.Equal(PixAddressKeyStatus.ACTIVE, result.Status); + Assert.Equal(new DateTime(2022, 2, 7, 17, 17, 48), result.DateCreated); + Assert.True(result.CanBeDeleted); + + Assert.NotNull(result.QrCode); + Assert.Equal("QRCODE IMAGE IN BASE64", result.QrCode.EncodedImage); + Assert.NotEmpty(result.QrCode.Payload); + } + + [Fact] + public void AddressKeyStatus_AllSixValuesDeserialize() + { + // B-28c: Status era string em vez de enum. + foreach (var status in new[] { + "AWAITING_ACTIVATION", "ACTIVE", "AWAITING_DELETION", + "AWAITING_ACCOUNT_DELETION", "DELETED", "ERROR" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void ListTransactionsFilter_NewFiltersSerialize() + { + // B-28e: ListTransactions nao aceitava filtro. + var filter = new PixTransactionListFilter + { + Status = PixTransactionStatus.DONE, + Type = PixTransactionType.CREDIT, + EndToEndIdentifier = "E00416968202111161635q5bk0brYk2C" + }; + + JsonContractAssert.QueryParamEquals(filter, "status", "DONE"); + JsonContractAssert.QueryParamEquals(filter, "type", "CREDIT"); + JsonContractAssert.QueryParamEquals(filter, "endToEndIdentifier", "E00416968202111161635q5bk0brYk2C"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/PixRecurringContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/PixRecurringContractTests.cs new file mode 100644 index 0000000..aa1a9b2 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/PixRecurringContractTests.cs @@ -0,0 +1,227 @@ +using System; +using System.Text.Json; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.PixRecurring; +using Codout.Apis.Asaas.Models.PixRecurring.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para PixRecurringManager. +/// Schemas verificados via MCP em 2026-05-24: +/// - GET /v3/pix/transactions/recurrings (envelope padrao, filtros status/value/searchText) +/// - GET /v3/pix/transactions/recurrings/{id} +/// - POST /v3/pix/transactions/recurrings/{id}/cancel (body vazio, retorna transaction) +/// - GET /v3/pix/transactions/recurrings/{id}/items (envelope minimalista {data:[...]} sem hasMore) +/// - POST /v3/pix/transactions/recurrings/items/{id}/cancel (body vazio, retorna item) +/// +public class PixRecurringContractTests +{ + // ───────────────────────────────────────────────────────────── + // PixRecurringTransaction response (B-12/B-13 regression coverage) + // Schema: { id, status, origin, value, frequency, quantity, + // startDate, finishDate, canBeCancelled, externalAccount } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void TransactionResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PixRecurring/transaction-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("35363f6e-93e2-11ec-b9d9-96f4053b1bd4", result.Id); + Assert.Equal(PixRecurringStatus.PENDING, result.Status); + Assert.Equal(PixRecurringOrigin.PIX, result.Origin); + Assert.Equal(0.02m, result.Value); + Assert.Equal(PixRecurringFrequency.WEEKLY, result.Frequency); + Assert.Equal(2, result.Quantity); + Assert.Equal(new DateTime(2024, 9, 18), result.StartDate); + Assert.Equal(new DateTime(2024, 9, 25), result.FinishDate); + Assert.True(result.CanBeCancelled); + + Assert.NotNull(result.ExternalAccount); + Assert.Equal("John Doe", result.ExternalAccount.Name); + Assert.Equal("Example bank S.A", result.ExternalAccount.FinancialInstitutionName); + Assert.Equal("***.456.789-**", result.ExternalAccount.CpfCnpj); + Assert.Equal("***.456.789-**", result.ExternalAccount.PixKey); + } + + [Fact] + public void TransactionsList_UsesStandardEnvelopeWithPagination() + { + // Diferente de /items que usa {data:[...]} sem paginacao, /recurrings + // usa o envelope padrao com hasMore/totalCount/limit/offset. + var json = FixtureLoader.Load("PixRecurring/transactions-list-response.json"); + + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Equal(1, response.TotalCount); + Assert.False(response.HasMore); + Assert.Equal(10, response.Limit); + Assert.Equal(0, response.Offset); + Assert.Single(response.Data); + Assert.Equal("35363f6e-93e2-11ec-b9d9-96f4053b1bd4", response.Data[0].Id); + } + + [Fact] + public void TransactionStatus_AllFiveValuesDeserialize() + { + // Schema: AWAITING_CRITICAL_ACTION_AUTHORIZATION, PENDING, SCHEDULED, CANCELLED, DONE + foreach (var status in new[] { + "AWAITING_CRITICAL_ACTION_AUTHORIZATION", "PENDING", "SCHEDULED", "CANCELLED", "DONE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void TransactionFrequency_BothValuesDeserialize() + { + foreach (var freq in new[] { "WEEKLY", "MONTHLY" }) + { + var json = $"{{\"id\":\"x\",\"frequency\":\"{freq}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Frequency); + Assert.Equal(freq, result.Frequency.ToString()); + } + } + + [Fact] + public void TransactionOrigin_PixDeserializes() + { + var json = "{\"id\":\"x\",\"origin\":\"PIX\"}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.Origin); + Assert.Equal(PixRecurringOrigin.PIX, result.Origin); + } + + // ───────────────────────────────────────────────────────────── + // PixRecurringItem response + // Schema: { id, status, scheduledDate, canBeCancelled, recurrenceNumber, + // quantity, value, refusalReasonDescription, externalAccount } + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ItemResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("PixRecurring/item-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("71ae9d73-468f-4d04-8b87-a541128f9c46", result.Id); + Assert.Equal(PixRecurringItemStatus.PENDING, result.Status); + Assert.Equal(new DateTime(2024, 10, 23), result.ScheduledDate); + Assert.True(result.CanBeCancelled); + Assert.Equal(1, result.RecurrenceNumber); + Assert.Equal(2, result.Quantity); + Assert.Equal(0.02m, result.Value); + Assert.Null(result.RefusalReasonDescription); + + Assert.NotNull(result.ExternalAccount); + Assert.Equal("John Doe", result.ExternalAccount.Name); + } + + [Fact] + public void ItemStatus_AllFourValuesDeserialize() + { + // Schema: PENDING, CANCELLED, REFUSED, DONE + foreach (var status in new[] { "PENDING", "CANCELLED", "REFUSED", "DONE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + // ───────────────────────────────────────────────────────────── + // Items envelope — B-14 regression: API retorna {data:[...]} sem + // hasMore/totalCount/limit/offset, ao contrario de transactions list. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void ItemsListEnvelope_UsesMinimalDataOnlyShape() + { + var json = FixtureLoader.Load("PixRecurring/items-list-envelope.json"); + + JsonContractAssert.HasRootProperty(json, "data", JsonValueKind.Array); + + // Assert that the minimal envelope does NOT have pagination fields + // (regressao B-14: codigo antigo tentava usar ResponseList aqui e quebrava) + using var doc = JsonDocument.Parse(json); + Assert.False(doc.RootElement.TryGetProperty("hasMore", out _)); + Assert.False(doc.RootElement.TryGetProperty("totalCount", out _)); + Assert.False(doc.RootElement.TryGetProperty("limit", out _)); + Assert.False(doc.RootElement.TryGetProperty("offset", out _)); + + var wrapper = JsonContractAssert.DeserializeFixture(json); + Assert.Single(wrapper.Data); + Assert.Equal("71ae9d73-468f-4d04-8b87-a541128f9c46", wrapper.Data[0].Id); + } + + // ───────────────────────────────────────────────────────────── + // List filter — NOVO em Fase 3e + // API espera: status (enum), value (number invariant), searchText (string) + // Antes: List(offset,limit) nao aceitava filter algum. + // ───────────────────────────────────────────────────────────── + + [Fact] + public void TransactionListFilter_SerializesAllFieldsWithCorrectNames() + { + var filter = new PixRecurringTransactionListFilter + { + Status = PixRecurringStatus.SCHEDULED, + Value = 12.5m, + SearchText = "John" + }; + + JsonContractAssert.QueryParamEquals(filter, "status", "SCHEDULED"); + JsonContractAssert.QueryParamEquals(filter, "value", "12.5"); + JsonContractAssert.QueryParamEquals(filter, "searchText", "John"); + } + + [Fact] + public void TransactionListFilter_DecimalUsesInvariantCulture() + { + // Em pt-BR, decimal.ToString() vira "12,5" sem cultura invariante. + // RequestParameters.Add(decimal?) forca InvariantCulture (ponto, nao virgula). + var prevCulture = System.Threading.Thread.CurrentThread.CurrentCulture; + try + { + System.Threading.Thread.CurrentThread.CurrentCulture = + new System.Globalization.CultureInfo("pt-BR"); + + var filter = new PixRecurringTransactionListFilter { Value = 99.99m }; + + Assert.Equal("99.99", filter["value"]); + } + finally + { + System.Threading.Thread.CurrentThread.CurrentCulture = prevCulture; + } + } + + [Fact] + public void TransactionListFilter_StatusEnumSerializesAsUppercase() + { + var filter = new PixRecurringTransactionListFilter + { + Status = PixRecurringStatus.AWAITING_CRITICAL_ACTION_AUTHORIZATION + }; + + JsonContractAssert.QueryParamEquals(filter, "status", "AWAITING_CRITICAL_ACTION_AUTHORIZATION"); + } + + [Fact] + public void TransactionListFilter_NullValuesAreOmitted() + { + var filter = new PixRecurringTransactionListFilter(); + + Assert.False(filter.ContainsKey("status")); + Assert.False(filter.ContainsKey("value")); + Assert.False(filter.ContainsKey("searchText")); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/RequestParametersContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/RequestParametersContractTests.cs new file mode 100644 index 0000000..98b43c1 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/RequestParametersContractTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Globalization; +using System.Threading; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests sistemicos para a serializacao de query params em +/// RequestParameters. Detecta regressoes em casing, cultura, formato de +/// data — tudo que ja causou bug no SDK (B-15) ou poderia causar. +/// +public class RequestParametersContractTests +{ + [Fact] + public void Bool_True_SerializesAsLowercaseTrue() + { + var parameters = new RequestParameters(); + parameters.Add("enabled", (bool?)true); + + JsonContractAssert.QueryParamEquals(parameters, "enabled", "true"); + } + + [Fact] + public void Bool_False_SerializesAsLowercaseFalse() + { + var parameters = new RequestParameters(); + parameters.Add("enabled", (bool?)false); + + JsonContractAssert.QueryParamEquals(parameters, "enabled", "false"); + } + + [Fact] + public void Bool_Null_RemovesKey() + { + var parameters = new RequestParameters(); + parameters.Add("enabled", (bool?)null); + + Assert.False(parameters.ContainsKey("enabled")); + } + + [Theory] + [InlineData("pt-BR")] + [InlineData("es-ES")] + [InlineData("de-DE")] + [InlineData("en-US")] + public void Decimal_SerializesWithDotInAllCultures(string cultureName) + { + var previousCulture = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName); + + var parameters = new RequestParameters(); + parameters.Add("value", (decimal?)12.5m); + + // Em pt-BR/es-ES/de-DE, decimal.ToString() retorna "12,5" (virgula) + // sem CultureInfo.InvariantCulture. API Asaas (JSON) exige ponto. + JsonContractAssert.QueryParamEquals(parameters, "value", "12.5"); + } + finally + { + Thread.CurrentThread.CurrentCulture = previousCulture; + } + } + + [Theory] + [InlineData("pt-BR")] + [InlineData("en-US")] + [InlineData("de-DE")] + public void DateTime_SerializesAsIsoYyyyMmDdInAllCultures(string cultureName) + { + var previousCulture = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureName); + + var parameters = new RequestParameters(); + parameters.Add("paymentDate", new DateTime(2026, 3, 15)); + + JsonContractAssert.QueryParamEquals(parameters, "paymentDate", "2026-03-15"); + } + finally + { + Thread.CurrentThread.CurrentCulture = previousCulture; + } + } + + [Fact] + public void Enum_SerializesAsUppercaseAsaasName() + { + var parameters = new RequestParameters(); + parameters.Add("billingType", BillingType.PIX); + + // Asaas enums sao em UPPERCASE e BillingType (C#) usa os mesmos + // identificadores — Enum.ToString() retorna "PIX". Se um futuro + // enum diferir entre C# e API, este teste falha e nos forca a + // adicionar um conversor explicito. + JsonContractAssert.QueryParamEquals(parameters, "billingType", "PIX"); + } + + [Fact] + public void Build_BuildsCorrectQueryStringWithEscaping() + { + var parameters = new RequestParameters(); + parameters.Add("name", "John & Maria"); + parameters.Add("enabled", (bool?)true); + + var query = parameters.Build(); + + Assert.StartsWith("?", query); + Assert.Contains("name=John%20%26%20Maria", query); + Assert.Contains("enabled=true", query); + } + + [Fact] + public void Build_NoParams_ReturnsEmpty() + { + var parameters = new RequestParameters(); + Assert.Equal(string.Empty, parameters.Build()); + } + + [Fact] + public void Add_OverwritesExistingKey() + { + var parameters = new RequestParameters(); + parameters.Add("enabled", (bool?)true); + parameters.Add("enabled", (bool?)false); + + JsonContractAssert.QueryParamEquals(parameters, "enabled", "false"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/SandboxContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/SandboxContractTests.cs new file mode 100644 index 0000000..f41f6a6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/SandboxContractTests.cs @@ -0,0 +1,26 @@ +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para SandboxManager (B-42, modelo ja correto). +/// Schemas verificados via MCP em 2026-05-24: +/// - POST /v3/sandbox/myAccount/approve +/// - POST /v3/sandbox/payment/{id}/confirm +/// - POST /v3/sandbox/payment/{id}/overdue +/// Todos os 3 endpoints expostos pelo manager batem com o schema. +/// EnsureSandbox() bloqueia uso em produção (verificado em unit tests). +/// +public class SandboxContractTests +{ + [Fact] + public void SandboxManager_HasAllThreeExpectedEndpoints() + { + // Sanity check via reflexao: garante que o manager expoe exatamente + // 3 metodos publicos (ApproveAccount, ConfirmPayment, ForceOverdue). + var methods = typeof(Codout.Apis.Asaas.Managers.SandboxManager) + .GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly); + + Assert.Contains(methods, m => m.Name == "ApproveAccount"); + Assert.Contains(methods, m => m.Name == "ConfirmPayment"); + Assert.Contains(methods, m => m.Name == "ForceOverdue"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/SubscriptionContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/SubscriptionContractTests.cs new file mode 100644 index 0000000..52dd24e --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/SubscriptionContractTests.cs @@ -0,0 +1,101 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Subscription; +using Codout.Apis.Asaas.Models.Subscription.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para SubscriptionManager (B-27). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class SubscriptionContractTests +{ + [Fact] + public void SubscriptionResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Subscription/subscription-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("subscription", result.Object); + Assert.Equal("sub_VXJBYgP2u0eO", result.Id); + Assert.Equal(new DateTime(2017, 3, 17), result.DateCreated); + Assert.Equal("cus_0T1mdomVMi39", result.CustomerId); + Assert.Equal(BillingType.BOLETO, result.BillingType); + Assert.Equal(Cycle.MONTHLY, result.Cycle); + Assert.Equal(19.9m, result.Value); + Assert.Equal(new DateTime(2017, 6, 15), result.NextDueDate); + Assert.Equal(new DateTime(2018, 6, 15), result.EndDate); + Assert.Equal(SubscriptionStatus.ACTIVE, result.Status); + Assert.False(result.Deleted); + Assert.Equal(12, result.MaxPayments); + Assert.Equal("356eb0c4-9eb7-4b7f-b2be-d9479af1d29f", result.CheckoutSession); + } + + [Fact] + public void SubscriptionResponse_NullableDatesHandleMissing() + { + // B-27b/c: DateCreated e NextDueDate eram non-nullable. + var json = "{\"id\":\"sub_x\",\"status\":\"INACTIVE\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.DateCreated); + Assert.Null(result.NextDueDate); + } + + [Fact] + public void SubscriptionStatus_AllThreeValuesDeserialize() + { + // B-27a: INACTIVE estava faltando no enum (so tinha ACTIVE+EXPIRED). + foreach (var status in new[] { "ACTIVE", "EXPIRED", "INACTIVE" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void Cycle_AllSevenValuesDeserialize() + { + foreach (var cycle in new[] { + "WEEKLY", "BIWEEKLY", "MONTHLY", "BIMONTHLY", + "QUARTERLY", "SEMIANNUALLY", "YEARLY" }) + { + var json = $"{{\"id\":\"x\",\"cycle\":\"{cycle}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(cycle, result.Cycle.ToString()); + } + } + + [Fact] + public void ListFilter_AllNewFieldsSerialize() + { + // B-27e: customerGroupName, status enum, deletedOnly, + // externalReference, order, sort estavam faltando. + var filter = new SubscriptionListFilter + { + CustomerId = "cus_1", + CustomerGroupName = "vip", + BillingType = BillingType.PIX, + Status = SubscriptionStatus.ACTIVE, + IncludeDeleted = false, + DeletedOnly = true, + ExternalReference = "ext_42", + Order = "asc", + Sort = "dateCreated" + }; + + JsonContractAssert.QueryParamEquals(filter, "customer", "cus_1"); + JsonContractAssert.QueryParamEquals(filter, "customerGroupName", "vip"); + JsonContractAssert.QueryParamEquals(filter, "billingType", "PIX"); + JsonContractAssert.QueryParamEquals(filter, "status", "ACTIVE"); + JsonContractAssert.QueryParamEquals(filter, "includeDeleted", "false"); + JsonContractAssert.QueryParamEquals(filter, "deletedOnly", "true"); + JsonContractAssert.QueryParamEquals(filter, "externalReference", "ext_42"); + JsonContractAssert.QueryParamEquals(filter, "order", "asc"); + JsonContractAssert.QueryParamEquals(filter, "sort", "dateCreated"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/TransferContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/TransferContractTests.cs new file mode 100644 index 0000000..08634d5 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/TransferContractTests.cs @@ -0,0 +1,93 @@ +using System; +using Codout.Apis.Asaas.Models.Transfer; +using Codout.Apis.Asaas.Models.Transfer.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para TransferManager (B-29). +/// Schemas verificados via MCP em 2026-05-24. +/// +public class TransferContractTests +{ + [Fact] + public void BankAccountTransferResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Transfer/transfer-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("transfer", result.Object); + Assert.Equal("777eb7c8-b1a2-4356-8fd8-a1b0644b5282", result.Id); + Assert.Equal(new DateTime(2019, 5, 2), result.DateCreated); + Assert.Equal(1000m, result.Value); + Assert.Equal(1000m, result.NetValue); + Assert.Equal(BankAccountTransferStatus.PENDING, result.Status); + Assert.Equal(0m, result.TransferFee); + Assert.Equal(new DateTime(2019, 5, 2), result.EffectiveDate); + Assert.True(result.Authorized); + Assert.Equal(TransferOperationType.TED, result.OperationType); + Assert.NotNull(result.BankAccount); + Assert.Equal("Banco do Brasil", result.BankAccount.Bank.Name); + Assert.Equal("001", result.BankAccount.Bank.Code); + } + + [Fact] + public void TransferResponse_NullableDatesAndAuthorizedHandleMissing() + { + // B-29b/c: DateCreated era non-nullable, Authorized era bool non-nullable. + var json = "{\"id\":\"t_x\",\"status\":\"PENDING\"}"; + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Null(result.DateCreated); + Assert.Null(result.Authorized); + } + + [Fact] + public void AsaasAccountTransferStatus_AllFiveValuesDeserialize() + { + // B-29d: AsaasAccountTransferStatus tinha apenas PENDING/DONE/CANCELLED. + // Schema unifica todos os transfers no mesmo enum de 5 valores. + foreach (var status in new[] { + "PENDING", "BANK_PROCESSING", "DONE", "CANCELLED", "FAILED" }) + { + var json = $"{{\"id\":\"x\",\"status\":\"{status}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(status, result.Status.ToString()); + } + } + + [Fact] + public void TransferOperationType_AllThreeValuesDeserialize() + { + // B-29e: campo novo operationType (PIX/TED/INTERNAL) nao existia no model. + foreach (var op in new[] { "PIX", "TED", "INTERNAL" }) + { + var json = $"{{\"id\":\"x\",\"operationType\":\"{op}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.NotNull(result.OperationType); + Assert.Equal(op, result.OperationType.ToString()); + } + } + + [Fact] + public void ListFilter_DateRangeFiltersUseLowercaseGeLe() + { + // B-29h: faltavam dateCreated[ge]/[le] e transferDate[ge]/[le]. + var filter = new TransferListFilter + { + DateCreatedGE = new DateTime(2024, 1, 1), + DateCreatedLE = new DateTime(2024, 12, 31), + TransferDateGE = new DateTime(2024, 2, 1), + TransferDateLE = new DateTime(2024, 11, 30), + TransferType = TransferType.BANK_ACCOUNT + }; + + JsonContractAssert.QueryParamEquals(filter, "dateCreated[ge]", "2024-01-01"); + JsonContractAssert.QueryParamEquals(filter, "dateCreated[le]", "2024-12-31"); + JsonContractAssert.QueryParamEquals(filter, "transferDate[ge]", "2024-02-01"); + JsonContractAssert.QueryParamEquals(filter, "transferDate[le]", "2024-11-30"); + JsonContractAssert.QueryParamEquals(filter, "type", "BANK_ACCOUNT"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/WalletContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/WalletContractTests.cs new file mode 100644 index 0000000..783003d --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/WalletContractTests.cs @@ -0,0 +1,24 @@ +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Wallet; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para WalletManager (B-33). +/// Schemas verificados via MCP em 2026-05-24. +/// Wallet e estrutura minima: {object, id}. +/// +public class WalletContractTests +{ + [Fact] + public void WalletList_DeserializesEnvelopeWithWalletItems() + { + var json = "{\"object\":\"list\",\"hasMore\":false,\"totalCount\":1,\"limit\":10,\"offset\":0,\"data\":[{\"object\":\"wallet\",\"id\":\"0000c712-0a0b-a0b0-0000-031e7ac51a2\"}]}"; + var response = new ResponseList(System.Net.HttpStatusCode.OK, json); + + Assert.True(response.WasSuccessful()); + Assert.Single(response.Data); + Assert.Equal("wallet", response.Data[0].Object); + Assert.Equal("0000c712-0a0b-a0b0-0000-031e7ac51a2", response.Data[0].Id); + } +} diff --git a/Codout.Apis.Asaas.Tests/Contract/WebhookContractTests.cs b/Codout.Apis.Asaas.Tests/Contract/WebhookContractTests.cs new file mode 100644 index 0000000..4620992 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Contract/WebhookContractTests.cs @@ -0,0 +1,63 @@ +using Codout.Apis.Asaas.Models.Webhook; +using Codout.Apis.Asaas.Models.Webhook.Enums; + +namespace Codout.Apis.Asaas.Tests.Contract; + +/// +/// Contract tests para WebhookManager (B-32). +/// Schemas verificados via MCP em 2026-05-24. +/// Schema oficial confirma todos os 110+ valores do enum WebhookEvent. +/// +public class WebhookContractTests +{ + [Fact] + public void WebhookResponse_DeserializesFromOfficialFixture() + { + var json = FixtureLoader.Load("Webhook/webhook-response.json"); + + var result = JsonContractAssert.DeserializeFixture(json); + + Assert.Equal("bbf67496-1379-4b6d-a348-fd5fa229f1c", result.Id); + Assert.Equal("Name Example", result.Name); + Assert.Equal("https://www.example.com/webhook/asaas", result.Url); + Assert.Equal("john.doe@asaas.com.br", result.Email); + Assert.True(result.Enabled); + Assert.False(result.Interrupted); + Assert.Equal(3, result.ApiVersion); + Assert.True(result.HasAuthToken); + Assert.Equal(WebhookSendType.SEQUENTIALLY, result.SendType); + Assert.Equal(0, result.PenalizedRequestsCount); + Assert.Equal(2, result.Events.Count); + Assert.Contains(WebhookEvent.PAYMENT_RECEIVED, result.Events); + Assert.Contains(WebhookEvent.PAYMENT_CONFIRMED, result.Events); + } + + [Fact] + public void SendType_BothValuesDeserialize() + { + foreach (var sendType in new[] { "SEQUENTIALLY", "NON_SEQUENTIALLY" }) + { + var json = $"{{\"id\":\"x\",\"sendType\":\"{sendType}\"}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Equal(sendType, result.SendType.ToString()); + } + } + + [Fact] + public void WebhookEvent_SamplePaymentEventsDeserialize() + { + // Sample crítico: garantir que os eventos mais comuns deserializam. + // Lista completa (110+ valores) e confirmada em CONFORMANCE. + foreach (var ev in new[] { + "PAYMENT_CREATED", "PAYMENT_RECEIVED", "PAYMENT_CONFIRMED", + "PAYMENT_OVERDUE", "PAYMENT_REFUNDED", "PAYMENT_CHARGEBACK_REQUESTED", + "INVOICE_AUTHORIZED", "TRANSFER_DONE", "SUBSCRIPTION_DELETED", + "PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED" }) + { + var json = $"{{\"id\":\"x\",\"events\":[\"{ev}\"]}}"; + var result = JsonContractAssert.DeserializeFixture(json); + Assert.Single(result.Events); + Assert.Equal(ev, result.Events[0].ToString()); + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Core/RequestParametersTests.cs b/Codout.Apis.Asaas.Tests/Core/RequestParametersTests.cs index da75fc4..934065e 100644 --- a/Codout.Apis.Asaas.Tests/Core/RequestParametersTests.cs +++ b/Codout.Apis.Asaas.Tests/Core/RequestParametersTests.cs @@ -149,23 +149,25 @@ public void Add_EnumValue_AddsEnumString() #region Add bool overload [Fact] - public void Add_BoolTrueValue_AddsStringTrue() + public void Add_BoolTrueValue_AddsLowercaseTrue() { var parameters = new RequestParameters(); parameters.Add("active", (bool?)true); - Assert.Equal("True", parameters["active"]); + // API Asaas espera "true"/"false" lowercase (padrao JSON/HTTP), + // nao "True"/"False" do bool.ToString() do .NET. + Assert.Equal("true", parameters["active"]); } [Fact] - public void Add_BoolFalseValue_AddsStringFalse() + public void Add_BoolFalseValue_AddsLowercaseFalse() { var parameters = new RequestParameters(); parameters.Add("active", (bool?)false); - Assert.Equal("False", parameters["active"]); + Assert.Equal("false", parameters["active"]); } [Fact] @@ -184,14 +186,15 @@ public void Add_NullBoolValue_RemovesKey() #region Add decimal overload [Fact] - public void Add_DecimalValue_AddsString() + public void Add_DecimalValue_AddsInvariantCultureString() { var parameters = new RequestParameters(); parameters.Add("amount", (decimal?)99.99m); - Assert.NotNull(parameters["amount"]); - Assert.Contains(99.99m.ToString(), parameters["amount"]); + // Sempre com ponto (.) decimal, independente da cultura corrente. + // Cobertura mais ampla por cultura esta em RequestParametersContractTests. + Assert.Equal("99.99", parameters["amount"]); } [Fact] diff --git a/Codout.Apis.Asaas.Tests/Core/Response/ResponseListTests.cs b/Codout.Apis.Asaas.Tests/Core/Response/ResponseListTests.cs index 0e8e414..ec395e9 100644 --- a/Codout.Apis.Asaas.Tests/Core/Response/ResponseListTests.cs +++ b/Codout.Apis.Asaas.Tests/Core/Response/ResponseListTests.cs @@ -5,7 +5,7 @@ using Codout.Apis.Asaas.Models.Common.Enums; using Codout.Apis.Asaas.Models.Pix; using Codout.Apis.Asaas.Models.Pix.Enums; -using Codout.Apis.Asaas.Models.CustomerFiscalInfo; +using Codout.Apis.Asaas.Models.FiscalInfo; namespace Codout.Apis.Asaas.Tests.Core.Response; @@ -76,7 +76,7 @@ public void Constructor_WithOkStatus_IsSuccessful() var response = new ResponseList(HttpStatusCode.OK, json); - Assert.True(response.WasSucessfull()); + Assert.True(response.WasSuccessful()); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -93,12 +93,12 @@ public void Constructor_WithOkStatus_StoresRawResponse() [Fact] public void Constructor_WithOkStatus_DeserializesEnumsInList() { - var json = BuildListJson("[{\"id\":\"tx_1\",\"status\":\"PENDING\",\"value\":100},{\"id\":\"tx_2\",\"status\":\"DONE\",\"value\":200}]", totalCount: 2); + var json = BuildListJson("[{\"id\":\"tx_1\",\"status\":\"SCHEDULED\",\"value\":100},{\"id\":\"tx_2\",\"status\":\"DONE\",\"value\":200}]", totalCount: 2); var response = new ResponseList(HttpStatusCode.OK, json); Assert.Equal(2, response.Data.Count); - Assert.Equal(PixTransactionStatus.PENDING, response.Data[0].Status); + Assert.Equal(PixTransactionStatus.SCHEDULED, response.Data[0].Status); Assert.Equal(PixTransactionStatus.DONE, response.Data[1].Status); } @@ -137,7 +137,7 @@ public void Constructor_WithBadRequest_IsNotSuccessful() var response = new ResponseList(HttpStatusCode.BadRequest, json); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } [Fact] diff --git a/Codout.Apis.Asaas.Tests/Core/Response/ResponseObjectTests.cs b/Codout.Apis.Asaas.Tests/Core/Response/ResponseObjectTests.cs index ebad5b7..2884bb1 100644 --- a/Codout.Apis.Asaas.Tests/Core/Response/ResponseObjectTests.cs +++ b/Codout.Apis.Asaas.Tests/Core/Response/ResponseObjectTests.cs @@ -44,7 +44,7 @@ public void Constructor_WithOkStatus_IsSuccessful() var response = new ResponseObject(HttpStatusCode.OK, json); - Assert.True(response.WasSucessfull()); + Assert.True(response.WasSuccessful()); } [Fact] @@ -110,7 +110,7 @@ public void Constructor_WithBadRequest_IsNotSuccessful() var response = new ResponseObject(HttpStatusCode.BadRequest, json); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } [Fact] @@ -145,7 +145,7 @@ public void Constructor_WithNotFound_SetsCorrectStatusCode() var response = new ResponseObject(HttpStatusCode.NotFound, json); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } [Fact] @@ -156,7 +156,7 @@ public void Constructor_WithInternalServerError_SetsCorrectStatusCode() var response = new ResponseObject(HttpStatusCode.InternalServerError, json); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } [Fact] @@ -166,7 +166,7 @@ public void Constructor_WithUnauthorized_IsNotSuccessful() var response = new ResponseObject(HttpStatusCode.Unauthorized, json); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } [Fact] @@ -189,7 +189,7 @@ public void Constructor_WithErrorAndNoErrorsProperty_CreatesUnknownError() // The JSON is valid but doesn't have "errors" property, so Errors list stays empty var response = new ResponseObject(HttpStatusCode.BadRequest, json); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); Assert.Empty(response.Errors); } @@ -207,7 +207,7 @@ public void Constructor_With2xxStatus_IsSuccessful(HttpStatusCode statusCode) var response = new ResponseObject(statusCode, json); - Assert.True(response.WasSucessfull()); + Assert.True(response.WasSuccessful()); } [Theory] @@ -222,7 +222,7 @@ public void Constructor_WithNon2xxStatus_IsNotSuccessful(HttpStatusCode statusCo var response = new ResponseObject(statusCode, json); - Assert.False(response.WasSucessfull()); + Assert.False(response.WasSuccessful()); } #endregion diff --git a/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/delete-response.json b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/delete-response.json new file mode 100644 index 0000000..8e6cf52 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/delete-response.json @@ -0,0 +1,4 @@ +{ + "deleted": true, + "id": "8d257732-2220-11ec-b695-b6af4a64184d" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/document-response.json b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/document-response.json new file mode 100644 index 0000000..13d68af --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/document-response.json @@ -0,0 +1,4 @@ +{ + "id": "8d257732-2220-11ec-b695-b6af4a64184d", + "status": "PENDING" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/pending-documents-response.json b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/pending-documents-response.json new file mode 100644 index 0000000..9021109 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/AccountDocument/pending-documents-response.json @@ -0,0 +1,24 @@ +{ + "rejectReasons": null, + "data": [ + { + "id": "172ed152-4fa4-43ad-9b69-39c323e9526c", + "status": "NOT_SENT", + "type": "MINUTES_OF_CONSTITUTION", + "title": "Minutes of election of the last board", + "description": "No description", + "responsible": { + "name": "John Doe", + "type": ["ASSOCIATION"] + }, + "onboardingUrl": "https://example.com/cadastro.io/8ad196d6cbfcc5d05bfabcbb5c730f6a", + "onboardingUrlExpirationDate": "2025-03-04 00:00:00", + "documents": [ + { + "id": "8d257732-2220-11ec-b695-b6af4a64184d", + "status": "PENDING" + } + ] + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Anticipation/anticipation-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Anticipation/anticipation-response.json new file mode 100644 index 0000000..64de21f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Anticipation/anticipation-response.json @@ -0,0 +1,16 @@ +{ + "object": "receivableAnticipation", + "id": "9e7d8639-350f-45c0-8bc3-d4ddc5f4ebac", + "installment": null, + "payment": "pay_626366773834", + "status": "PENDING", + "anticipationDate": "2019-05-20", + "dueDate": "2019-05-26", + "requestDate": "2019-05-14", + "fee": 2.33, + "anticipationDays": 5, + "netValue": 73.68, + "totalValue": 80, + "value": 76.01, + "denialObservation": null +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bill-response.json b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bill-response.json new file mode 100644 index 0000000..6d941f1 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bill-response.json @@ -0,0 +1,20 @@ +{ + "object": "bill", + "id": "f1bce822-6f37-4905-8de8-f1af9f2f4bab", + "status": "PENDING", + "value": 29, + "discount": 0, + "interest": 0, + "fine": 0, + "identificationField": "03399.77779 29900.000000 04751.101017 1 81510000002990", + "dueDate": "2020-01-31", + "scheduleDate": "2020-01-31", + "paymentDate": null, + "fee": 0, + "description": "Celular 01/12", + "companyName": null, + "transactionReceiptUrl": "https://www.asaas.com/comprovantes/00016578", + "canBeCancelled": false, + "externalReference": null, + "failReasons": [] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bills-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bills-list-response.json new file mode 100644 index 0000000..81b7c3e --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/bills-list-response.json @@ -0,0 +1,23 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "object": "bill", + "id": "f1bce822-6f37-4905-8de8-f1af9f2f4bab", + "status": "PENDING", + "value": 29, + "discount": 0, + "interest": 0, + "fine": 0, + "identificationField": "03399.77779 29900.000000 04751.101017 1 81510000002990", + "dueDate": "2020-01-31", + "scheduleDate": "2020-01-31", + "fee": 0, + "canBeCancelled": false + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/simulate-response.json b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/simulate-response.json new file mode 100644 index 0000000..6707e95 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/BillPayment/simulate-response.json @@ -0,0 +1,23 @@ +{ + "minimumScheduleDate": "2021-11-22", + "fee": 0, + "bankSlipInfo": { + "identificationField": "03399201595100529040147600301023888440000421177", + "value": 4211.77, + "dueDate": "2021-12-24", + "companyName": null, + "bank": "033", + "beneficiaryCpfCnpj": "19.540.550/0001-21", + "beneficiaryName": "ASAAS GESTAO FINANCEIRA S.A.", + "allowChangeValue": false, + "minValue": 4211.77, + "maxValue": 4211.77, + "discountValue": 0, + "interestValue": 0, + "fineValue": 0, + "originalValue": 4211.77, + "totalDiscountValue": 0, + "totalAdditionalValue": 0, + "isOverdue": false + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-minimal.json b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-minimal.json new file mode 100644 index 0000000..dcbf446 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-minimal.json @@ -0,0 +1,16 @@ +{ + "billingTypes": ["CREDIT_CARD"], + "chargeTypes": ["DETACHED"], + "callback": { + "successUrl": "https://example.com/asaas/checkout/success", + "cancelUrl": "https://example.com/asaas/checkout/cancel" + }, + "items": [ + { + "imageBase64": "IMAGE IN BASE64", + "name": "Roupas", + "quantity": 2, + "value": 100 + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-recurrent.json b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-recurrent.json new file mode 100644 index 0000000..0a3646f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/create-request-recurrent.json @@ -0,0 +1,38 @@ +{ + "billingTypes": ["CREDIT_CARD"], + "chargeTypes": ["RECURRENT"], + "minutesToExpire": 100, + "externalReference": "dcf4dff9-b080-425c-b234-765f2ffac0ae", + "callback": { + "successUrl": "https://example.com/asaas/checkout/success", + "cancelUrl": "https://example.com/asaas/checkout/cancel", + "expiredUrl": "https://example.com/asaas/checkout/expired" + }, + "items": [ + { + "externalReference": "776106a1-d578-4143-936a-d07549b0cc79", + "description": "Camisetas", + "imageBase64": "IMAGE IN BASE64", + "name": "Roupas", + "quantity": 2, + "value": 100 + } + ], + "customerData": { + "name": "John Doe", + "cpfCnpj": "24971563792", + "email": "john.doe@asaas.com.br", + "phone": "4738010919", + "address": "Av. Paulista", + "addressNumber": 150, + "complement": "Sala 201", + "province": "Centro", + "postalCode": "01310-000", + "city": 12987382 + }, + "subscription": { + "cycle": "MONTHLY", + "endDate": "2025-01-01", + "nextDueDate": "2025-01-01" + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Checkout/response.json b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/response.json new file mode 100644 index 0000000..c03dea0 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Checkout/response.json @@ -0,0 +1,43 @@ +{ + "id": "131ca662-56c8-4479-b5b3-fd61a413fce7", + "link": "https://sandbox.asaas.com/checkoutSession/show/131ca662-56c8-4479-b5b3-fd61a413fce7", + "status": "ACTIVE", + "billingTypes": ["CREDIT_CARD"], + "chargeTypes": ["RECURRENT"], + "minutesToExpire": 100, + "externalReference": "dcf4dff9-b080-425c-b234-765f2ffac0ae", + "callback": { + "successUrl": "https://example.com/asaas/checkout/success", + "cancelUrl": "https://example.com/asaas/checkout/cancel", + "expiredUrl": "https://example.com/asaas/checkout/expired" + }, + "items": [ + { + "externalReference": "776106a1-d578-4143-936a-d07549b0cc79", + "description": "Camisetas", + "imageBase64": "IMAGE IN BASE64", + "name": "Roupas", + "quantity": 2, + "value": 100 + } + ], + "customerData": { + "name": "John Doe", + "cpfCnpj": "24971563792", + "email": "john.doe@asaas.com.br", + "phone": "4738010919", + "address": "Av. Paulista", + "addressNumber": 150, + "complement": "Sala 201", + "province": "Centro", + "postalCode": "01310-000", + "city": 12987382 + }, + "subscription": { + "cycle": "MONTHLY", + "endDate": "2025-01-01", + "nextDueDate": "2025-01-01" + }, + "installment": null, + "split": [] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-create-response.json b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-create-response.json new file mode 100644 index 0000000..19deb91 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-create-response.json @@ -0,0 +1,8 @@ +{ + "id": "6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "dateCreated": "2025-05-30", + "cpfCnpj": "05666663755", + "customer": "cus_000000001766", + "downloadUrl": "https://www.asaas.com.br/creditBureauReport/download/6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "reportFile": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3Ro" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-response.json b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-response.json new file mode 100644 index 0000000..74dc229 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/report-response.json @@ -0,0 +1,8 @@ +{ + "id": "6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "dateCreated": "2025-05-30", + "cpfCnpj": "05666663755", + "customer": "cus_000000001766", + "downloadUrl": "https://www.asaas.com.br/creditBureauReport/download/6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "reportFile": null +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/reports-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/reports-list-response.json new file mode 100644 index 0000000..124f28d --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/CreditBureauReport/reports-list-response.json @@ -0,0 +1,17 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "id": "6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "dateCreated": "2025-05-30", + "cpfCnpj": "05666663755", + "customer": "cus_000000001766", + "downloadUrl": "https://www.asaas.com.br/creditBureauReport/download/6c5e73fa-9efd-4a75-b60c-1cafb8d1c7ed", + "reportFile": null + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Customer/create-request.json b/Codout.Apis.Asaas.Tests/Fixtures/Customer/create-request.json new file mode 100644 index 0000000..2109070 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Customer/create-request.json @@ -0,0 +1,12 @@ +{ + "name": "John Doe", + "cpfCnpj": "24971563792", + "email": "john.doe@asaas.com.br", + "mobilePhone": "4799376637", + "address": "Av. Paulista", + "addressNumber": "150", + "complement": "Sala 201", + "province": "Centro", + "postalCode": "01310-000", + "externalReference": "12987382" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Customer/customer-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Customer/customer-response.json new file mode 100644 index 0000000..aa2d09d --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Customer/customer-response.json @@ -0,0 +1,26 @@ +{ + "object": "customer", + "id": "cus_000005401844", + "dateCreated": "2024-07-12", + "name": "John Doe", + "email": "john.doe@asaas.com.br", + "phone": "90999999999", + "mobilePhone": "90999999999", + "address": "Av. Paulista", + "addressNumber": "150", + "complement": "Sala 201", + "province": "Centro", + "city": 12565, + "cityName": "São Paulo", + "state": "SP", + "country": "Brasil", + "postalCode": "01310000", + "cpfCnpj": "24971563792", + "personType": "FISICA", + "deleted": false, + "additionalEmails": "john.doe@asaas.com,john.doe.silva@asaas.com.br", + "externalReference": "12987382", + "notificationDisabled": false, + "observations": "great payer, no problems so far", + "foreignCustomer": false +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Customer/customers-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Customer/customers-list-response.json new file mode 100644 index 0000000..5d0d249 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Customer/customers-list-response.json @@ -0,0 +1,20 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "object": "customer", + "id": "cus_000005401844", + "dateCreated": "2024-07-12", + "name": "John Doe", + "email": "john.doe@asaas.com.br", + "cpfCnpj": "24971563792", + "personType": "FISICA", + "deleted": false, + "notificationDisabled": false + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-request.json b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-request.json new file mode 100644 index 0000000..b0ea56b --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-request.json @@ -0,0 +1,5 @@ +{ + "daysToExpire": 30, + "enabled": true, + "isFeePayer": false +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-response.json new file mode 100644 index 0000000..b0ea56b --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/config-response.json @@ -0,0 +1,5 @@ +{ + "daysToExpire": 30, + "enabled": true, + "isFeePayer": false +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Escrow/payment-escrow-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/payment-escrow-response.json new file mode 100644 index 0000000..33b741c --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Escrow/payment-escrow-response.json @@ -0,0 +1,7 @@ +{ + "id": "4f468235-cec3-482f-b3d0-348af4c7194", + "status": "ACTIVE", + "expirationDate": "2024-06-10", + "finishDate": "2024-06-10", + "finishReason": "EXPIRED" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Installment/installment-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Installment/installment-response.json new file mode 100644 index 0000000..8fd14c5 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Installment/installment-response.json @@ -0,0 +1,18 @@ +{ + "object": "installment", + "id": "2765d086-c7c5-5cca-898a-4262d212587c", + "value": 360, + "netValue": 312.12, + "paymentValue": 30, + "installmentCount": 12, + "billingType": "CREDIT_CARD", + "paymentDate": null, + "description": "Order 056984", + "expirationDay": 31, + "dateCreated": "2021-01-19", + "customer": "cus_000000001645", + "paymentLink": "997152082166122", + "checkoutSession": "5a3e564f-81cd-4f4e-9c8e-af43f9ac4287", + "deleted": false, + "refunds": [] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response.json new file mode 100644 index 0000000..02c5db2 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/invoice-response.json @@ -0,0 +1,47 @@ +{ + "object": "invoice", + "id": "inv_000000000232", + "status": "SCHEDULED", + "customer": "cus_000000002750", + "payment": "pay_145059895800", + "installment": null, + "type": "NFS-e", + "statusDescription": null, + "serviceDescription": "Invoice 101940.\nDescription of Services: SYSTEMS ANALYSIS AND DEVELOPMENT", + "pdfUrl": null, + "xmlUrl": null, + "rpsSerie": null, + "rpsNumber": null, + "number": null, + "validationCode": null, + "value": 300, + "deductions": 10, + "effectiveDate": "2024-08-15", + "observations": "Monthly for June work.", + "estimatedTaxesDescription": null, + "externalReference": null, + "taxes": { + "nbsCode": "1.0101.11.00", + "taxSituationCode": "011", + "taxClassificationCode": "011001", + "operationIndicatorCode": "020101", + "retainIss": true, + "iss": 2, + "pisCofinsRetentionType": "NOT_WITHHELD", + "pisCofinsTaxStatus": "STANDARD_TAXABLE_OPERATION", + "pis": 0.65, + "cofins": 3, + "csll": 9, + "inss": 11, + "ir": 1.5, + "stateIbs": 0.1, + "stateIbsValue": 0.3, + "municipalIbs": 0, + "municipalIbsValue": 0, + "cbs": 0.9, + "cbsValue": 2.7 + }, + "municipalServiceId": null, + "municipalServiceCode": "1.01", + "municipalServiceName": "Systems analysis and development" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Invoice/schedule-request.json b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/schedule-request.json new file mode 100644 index 0000000..0759a15 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Invoice/schedule-request.json @@ -0,0 +1,20 @@ +{ + "payment": "pay_637959110194", + "customer": "cus_000000002750", + "serviceDescription": "Invoice 101940", + "observations": "Monthly for June work.", + "value": 300, + "deductions": 10, + "effectiveDate": "2024-08-20", + "municipalServiceName": "Systems analysis and development", + "municipalServiceCode": "1.01", + "taxes": { + "retainIss": true, + "iss": 2, + "pis": 0.65, + "cofins": 3, + "csll": 9, + "inss": 11, + "ir": 1.5 + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/create-request.json b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/create-request.json new file mode 100644 index 0000000..e63fc3a --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/create-request.json @@ -0,0 +1,4 @@ +{ + "value": 15, + "phoneNumber": "63997365512" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/provider-response.json b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/provider-response.json new file mode 100644 index 0000000..331d31f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/provider-response.json @@ -0,0 +1,17 @@ +{ + "name": "Vivo", + "values": [ + { + "name": "R$ 12,00", + "bonus": "5.0", + "minValue": 1, + "maxValue": 5 + }, + { + "name": "R$ 30,00", + "bonus": "3.0", + "minValue": 20, + "maxValue": 50 + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharge-response.json b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharge-response.json new file mode 100644 index 0000000..debd14a --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharge-response.json @@ -0,0 +1,8 @@ +{ + "id": "37c22147-4194-11ec-8061-0242ac120002", + "value": 15, + "phoneNumber": "63997365512", + "status": "PENDING", + "canBeCancelled": true, + "operatorName": "Vivo" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharges-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharges-list-response.json new file mode 100644 index 0000000..f711251 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/MobilePhoneRecharge/recharges-list-response.json @@ -0,0 +1,17 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "id": "37c22147-4194-11ec-8061-0242ac120002", + "value": 15, + "phoneNumber": "63997365512", + "status": "PENDING", + "canBeCancelled": true, + "operatorName": "Vivo" + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/limits-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/limits-response.json new file mode 100644 index 0000000..3317ae9 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/limits-response.json @@ -0,0 +1,9 @@ +{ + "creation": { + "daily": { + "limit": 10, + "used": 5, + "wasReached": false + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/payment-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/payment-response.json new file mode 100644 index 0000000..d43bc50 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/payment-response.json @@ -0,0 +1,26 @@ +{ + "object": "payment", + "id": "pay_080225913252", + "dateCreated": "2017-03-10", + "customer": "cus_G7Dvo4iphUNk", + "checkoutSession": "356eb0c4-9eb7-4b7f-b2be-d9479af1d29f", + "value": 129.9, + "netValue": 124.9, + "description": "Pedido 056984", + "billingType": "BOLETO", + "canBePaidAfterDueDate": true, + "status": "PENDING", + "dueDate": "2017-06-10", + "originalDueDate": "2017-06-10", + "invoiceUrl": "https://www.asaas.com/i/080225913252", + "invoiceNumber": "00005101", + "externalReference": "056984", + "deleted": false, + "anticipated": false, + "anticipable": false, + "creditDate": "2017-06-10", + "estimatedCreditDate": "2017-06-10", + "nossoNumero": "6453", + "bankSlipUrl": "https://www.asaas.com/b/pdf/080225913252", + "postalService": false +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/payments-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/payments-list-response.json new file mode 100644 index 0000000..7b78f06 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/payments-list-response.json @@ -0,0 +1,21 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "object": "payment", + "id": "pay_080225913252", + "dateCreated": "2017-03-10", + "customer": "cus_G7Dvo4iphUNk", + "value": 129.9, + "netValue": 124.9, + "billingType": "BOLETO", + "status": "PENDING", + "dueDate": "2017-06-10", + "deleted": false + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-minimal.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-minimal.json new file mode 100644 index 0000000..242d9e8 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-minimal.json @@ -0,0 +1,4 @@ +{ + "value": 100, + "billingTypes": ["CREDIT_CARD", "BOLETO", "PIX"] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-with-installments.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-with-installments.json new file mode 100644 index 0000000..e1c0684 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-request-with-installments.json @@ -0,0 +1,5 @@ +{ + "value": 100, + "installmentCount": 2, + "billingTypes": ["CREDIT_CARD", "BOLETO", "PIX"] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-response.json new file mode 100644 index 0000000..989cbfe --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Payment/simulate-response.json @@ -0,0 +1,29 @@ +{ + "value": 100, + "creditCard": { + "netValue": 100, + "feePercentage": 2.49, + "operationFee": 0.49, + "installment": { + "paymentNetValue": 48.52, + "paymentValue": 50 + } + }, + "bankSlip": { + "netValue": 98.02, + "feeValue": 0.99, + "installment": { + "paymentNetValue": 48.52, + "paymentValue": 50 + } + }, + "pix": { + "netValue": 98.02, + "feePercentage": null, + "feeValue": 0.99, + "installment": { + "paymentNetValue": 48.52, + "paymentValue": 50 + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunning-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunning-response.json new file mode 100644 index 0000000..c1ce618 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunning-response.json @@ -0,0 +1,18 @@ +{ + "id": "ce35702d-0d9f-475a-ba46-e251ad265c91", + "dunningNumber": 15, + "status": "PENDING", + "type": "CREDIT_BUREAU", + "requestDate": "2020-05-26", + "description": "Duas mesas com 8 cadeiras solicitadas via encomenda no dia 01/05/2018", + "value": 80, + "feeValue": 8, + "netValue": 72, + "receivedInCashFeeValue": 0, + "denialReason": null, + "cancellationFeeValue": 0, + "canBeCancelled": true, + "cannotBeCancelledReason": null, + "isNecessaryResendDocumentation": false, + "payment": "pay_080225913252" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunnings-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunnings-list-response.json new file mode 100644 index 0000000..992c150 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/dunnings-list-response.json @@ -0,0 +1,21 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "id": "ce35702d-0d9f-475a-ba46-e251ad265c91", + "dunningNumber": 15, + "status": "PENDING", + "type": "CREDIT_BUREAU", + "requestDate": "2020-05-26", + "value": 80, + "feeValue": 8, + "netValue": 72, + "canBeCancelled": true, + "payment": "pay_080225913252" + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/history-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/history-list-response.json new file mode 100644 index 0000000..c33293e --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/history-list-response.json @@ -0,0 +1,14 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "status": "NEGOTIATED", + "description": "Negativação negociada com o cliente. O pagamento será iniciado.", + "eventDate": "2019-02-20" + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/partial-payments-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/partial-payments-response.json new file mode 100644 index 0000000..4f829e6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/partial-payments-response.json @@ -0,0 +1,14 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "value": 800, + "description": "A quitação desta cobrança foi efetuada pelo cliente.", + "paymentDate": "2020-02-10" + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/payments-available-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/payments-available-response.json new file mode 100644 index 0000000..21a6f27 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/payments-available-response.json @@ -0,0 +1,26 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "payment": "pay_856437540297", + "customer": "cus_000000001663", + "value": 250, + "status": "PENDING", + "billingType": "BOLETO", + "dueDate": "2020-05-18", + "typeSimulations": [ + { + "type": "CREDIT_BUREAU", + "isAllowed": true, + "feeValue": 0, + "netValue": 0, + "startDate": "2020-05-18" + } + ] + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/simulate-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/simulate-response.json new file mode 100644 index 0000000..ac0ac82 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PaymentDunning/simulate-response.json @@ -0,0 +1,20 @@ +{ + "payment": "pay_080225913252", + "value": 80, + "typeSimulations": [ + { + "type": "CREDIT_BUREAU", + "isAllowed": true, + "feeValue": 0, + "netValue": 0, + "startDate": "2020-05-18" + }, + { + "type": "CREDIT_BUREAU", + "isAllowed": false, + "notAllowedReason": "A negativação via Serasa não está disponível para parcelamentos de cartão de crédito.", + "feeValue": 0, + "netValue": 0 + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Pix/address-key-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Pix/address-key-response.json new file mode 100644 index 0000000..48ebdf5 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Pix/address-key-response.json @@ -0,0 +1,13 @@ +{ + "id": "a33047b1-fb19-4b68-9373-a7ba8a8162aa", + "key": "b6295ee1-f054-47d1-9e90-ee57b74f60d9", + "type": "EVP", + "status": "ACTIVE", + "dateCreated": "2022-02-07 17:17:48", + "canBeDeleted": true, + "cannotBeDeletedReason": null, + "qrCode": { + "encodedImage": "QRCODE IMAGE IN BASE64", + "payload": "00020126580014br.gov.bcb.pix0136a9fe43bc-164d-44d1-91c2-2f9b4d6956e95204000053039865802BR5925Joao da Silva6009Joinville62290525JOAOSILVA00000055ASA6304E62B" + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Pix/transaction-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Pix/transaction-response.json new file mode 100644 index 0000000..f0eb25f --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Pix/transaction-response.json @@ -0,0 +1,35 @@ +{ + "id": "35363f6e-93e2-11ec-b9d9-96f4053b1bd4", + "endToEndIdentifier": "E00416968202111161635q5bk0brYk2C", + "finality": "WITHDRAWAL", + "value": 10, + "changeValue": null, + "refundedValue": 0, + "effectiveDate": "2022-01-13 10:49:59", + "scheduledDate": "2022-10-18", + "status": "DONE", + "type": "DEBIT", + "originType": "DYNAMIC_QRCODE", + "conciliationIdentifier": "dcabae5bbfb6nffbb87c693883656483", + "description": null, + "transactionReceiptUrl": null, + "refusalReason": null, + "canBeCanceled": true, + "externalAccount": { + "ispb": "416968", + "ispbName": "Example Bank S.A", + "name": "John Doe", + "cpfCnpj": "***.456.789-**", + "addressKey": "12345678910", + "addressKeyType": "CPF" + }, + "payment": "pay_0491859546906926", + "canBeRefunded": true, + "refundDisabledReason": null, + "chargedFeeValue": 0.99, + "dateCreated": "2023-02-14 10:42:55", + "addressKey": null, + "addressKeyType": null, + "transferId": null, + "externalReference": null +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-create-request-minimal.json b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-create-request-minimal.json new file mode 100644 index 0000000..04aaddb --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-create-request-minimal.json @@ -0,0 +1,10 @@ +{ + "frequency": "MONTHLY", + "contractId": "CONTRACT-123", + "startDate": "2024-01-01", + "customerId": "cus_000005735721", + "immediateQrCode": { + "expirationSeconds": 3600, + "originalValue": 100 + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-response.json new file mode 100644 index 0000000..7919baf --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorization-response.json @@ -0,0 +1,23 @@ +{ + "id": "a33047b1-fb19-4b68-9373-a7ba8a8162aa", + "minLimitValue": 50, + "cancellationDate": null, + "cancellationReason": null, + "contractId": "XXXYYYY1234", + "customerId": "cus_000005735721", + "description": "Music and Movie Streaming Services", + "finishDate": "2024-12-31", + "frequency": "MONTHLY", + "endToEndIdentifier": "RR1234567820240115abcdefghijk", + "startDate": "2024-01-01", + "status": "ACTIVE", + "value": 100, + "payload": null, + "encodedImage": null, + "immediateQrCode": { + "conciliationIdentifier": "E12345678202401011234567890123456", + "expirationDate": "2024-01-01 12:00:00" + }, + "originType": "IMMEDIATE_PAYMENT_AND_RECURRING_QR_CODE", + "subscriptionId": "sub_000005735721" +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorizations-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorizations-list-response.json new file mode 100644 index 0000000..35cd53a --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/authorizations-list-response.json @@ -0,0 +1,19 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "id": "a33047b1-fb19-4b68-9373-a7ba8a8162aa", + "contractId": "XXXYYYY1234", + "customerId": "cus_000005735721", + "frequency": "MONTHLY", + "status": "ACTIVE", + "value": 100, + "startDate": "2024-01-01", + "finishDate": "2024-12-31" + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/payment-instruction-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/payment-instruction-response.json new file mode 100644 index 0000000..035145b --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixAutomatic/payment-instruction-response.json @@ -0,0 +1,13 @@ +{ + "id": "d1c9c4b2-6a97-4573-9d6d-26bb64f04c28", + "endToEndIdentifier": "E00416968202111161635q5bk0brYk2C", + "authorization": { + "id": "35363f6e-93e2-11ec-b9d9-96f4053b1bd4", + "endToEndIdentifier": "RR1234567820240115abcdefghijk", + "customerId": "cus_000005735721" + }, + "dueDate": "2020-01-31", + "status": "SCHEDULED", + "paymentId": "pay_tsp88gie3b5e6o2p", + "refusalReason": null +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/item-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/item-response.json new file mode 100644 index 0000000..381fc61 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/item-response.json @@ -0,0 +1,16 @@ +{ + "id": "71ae9d73-468f-4d04-8b87-a541128f9c46", + "status": "PENDING", + "scheduledDate": "2024-10-23", + "canBeCancelled": true, + "recurrenceNumber": 1, + "quantity": 2, + "value": 0.02, + "refusalReasonDescription": null, + "externalAccount": { + "name": "John Doe", + "financialInstitutionName": "Example bank S.A", + "cpfCnpj": "***.456.789-**", + "pixKey": "***.456.789-**" + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/items-list-envelope.json b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/items-list-envelope.json new file mode 100644 index 0000000..193a335 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/items-list-envelope.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "id": "71ae9d73-468f-4d04-8b87-a541128f9c46", + "status": "PENDING", + "scheduledDate": "2024-10-23", + "canBeCancelled": true, + "recurrenceNumber": 1, + "quantity": 2, + "value": 0.02, + "refusalReasonDescription": null, + "externalAccount": { + "name": "John Doe", + "financialInstitutionName": "Example bank S.A", + "cpfCnpj": "***.456.789-**", + "pixKey": "***.456.789-**" + } + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transaction-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transaction-response.json new file mode 100644 index 0000000..7ccdab6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transaction-response.json @@ -0,0 +1,17 @@ +{ + "id": "35363f6e-93e2-11ec-b9d9-96f4053b1bd4", + "status": "PENDING", + "origin": "PIX", + "value": 0.02, + "frequency": "WEEKLY", + "quantity": 2, + "startDate": "2024-09-18", + "finishDate": "2024-09-25", + "canBeCancelled": true, + "externalAccount": { + "name": "John Doe", + "financialInstitutionName": "Example bank S.A", + "cpfCnpj": "***.456.789-**", + "pixKey": "***.456.789-**" + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transactions-list-response.json b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transactions-list-response.json new file mode 100644 index 0000000..ecc8de6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/PixRecurring/transactions-list-response.json @@ -0,0 +1,26 @@ +{ + "object": "list", + "hasMore": false, + "totalCount": 1, + "limit": 10, + "offset": 0, + "data": [ + { + "id": "35363f6e-93e2-11ec-b9d9-96f4053b1bd4", + "status": "PENDING", + "origin": "PIX", + "value": 0.02, + "frequency": "WEEKLY", + "quantity": 2, + "startDate": "2024-09-18", + "finishDate": "2024-09-25", + "canBeCancelled": true, + "externalAccount": { + "name": "John Doe", + "financialInstitutionName": "Example bank S.A", + "cpfCnpj": "***.456.789-**", + "pixKey": "***.456.789-**" + } + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Subscription/subscription-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Subscription/subscription-response.json new file mode 100644 index 0000000..121a63e --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Subscription/subscription-response.json @@ -0,0 +1,19 @@ +{ + "object": "subscription", + "id": "sub_VXJBYgP2u0eO", + "dateCreated": "2017-03-17", + "customer": "cus_0T1mdomVMi39", + "paymentLink": null, + "billingType": "BOLETO", + "cycle": "MONTHLY", + "value": 19.9, + "nextDueDate": "2017-06-15", + "endDate": "2018-06-15", + "description": "Pro Plan Subscription", + "status": "ACTIVE", + "deleted": false, + "maxPayments": 12, + "externalReference": null, + "checkoutSession": "356eb0c4-9eb7-4b7f-b2be-d9479af1d29f", + "split": [] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Transfer/transfer-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Transfer/transfer-response.json new file mode 100644 index 0000000..a03b675 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Transfer/transfer-response.json @@ -0,0 +1,35 @@ +{ + "object": "transfer", + "id": "777eb7c8-b1a2-4356-8fd8-a1b0644b5282", + "type": "BANK_ACCOUNT", + "dateCreated": "2019-05-02", + "value": 1000, + "netValue": 1000, + "status": "PENDING", + "transferFee": 0, + "effectiveDate": "2019-05-02", + "scheduleDate": "2019-05-02", + "endToEndIdentifier": null, + "authorized": true, + "failReason": null, + "externalReference": null, + "transactionReceiptUrl": null, + "operationType": "TED", + "description": null, + "recurring": null, + "bankAccount": { + "bank": { + "ispb": null, + "code": "001", + "name": "Banco do Brasil" + }, + "accountName": "Banco do Brasil account", + "ownerName": "John Doe", + "cpfCnpj": "***.143.689-**", + "agency": "1263", + "agencyDigit": "3", + "account": "9999991", + "accountDigit": "1", + "pixAddressKey": null + } +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/Webhook/webhook-response.json b/Codout.Apis.Asaas.Tests/Fixtures/Webhook/webhook-response.json new file mode 100644 index 0000000..fbd603a --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/Webhook/webhook-response.json @@ -0,0 +1,13 @@ +{ + "id": "bbf67496-1379-4b6d-a348-fd5fa229f1c", + "name": "Name Example", + "url": "https://www.example.com/webhook/asaas", + "email": "john.doe@asaas.com.br", + "enabled": true, + "interrupted": false, + "apiVersion": 3, + "hasAuthToken": true, + "sendType": "SEQUENTIALLY", + "penalizedRequestsCount": 0, + "events": ["PAYMENT_RECEIVED", "PAYMENT_CONFIRMED"] +} diff --git a/Codout.Apis.Asaas.Tests/Fixtures/error-response.json b/Codout.Apis.Asaas.Tests/Fixtures/error-response.json new file mode 100644 index 0000000..8ed835b --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Fixtures/error-response.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "code": "invalid_object", + "description": "Informe o número de parcelas." + } + ] +} diff --git a/Codout.Apis.Asaas.Tests/Helpers/TestableManagerFactory.cs b/Codout.Apis.Asaas.Tests/Helpers/TestableManagerFactory.cs index 275e3eb..ce571de 100644 --- a/Codout.Apis.Asaas.Tests/Helpers/TestableManagerFactory.cs +++ b/Codout.Apis.Asaas.Tests/Helpers/TestableManagerFactory.cs @@ -186,7 +186,7 @@ protected override HttpClient BuildHttpClient() } } -public class TestableCustomerFiscalInfoManager(ApiSettings settings, HttpMessageHandler handler) : CustomerFiscalInfoManager(settings) +public class TestableFiscalInfoManager(ApiSettings settings, HttpMessageHandler handler) : FiscalInfoManager(settings) { protected override HttpClient BuildHttpClient() { @@ -205,3 +205,73 @@ protected override HttpClient BuildHttpClient() return client; } } + +public class TestableChargebackManager(ApiSettings settings, HttpMessageHandler handler) : ChargebackManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestableEscrowManager(ApiSettings settings, HttpMessageHandler handler) : EscrowManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestableCheckoutManager(ApiSettings settings, HttpMessageHandler handler) : CheckoutManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestableMobilePhoneRechargeManager(ApiSettings settings, HttpMessageHandler handler) : MobilePhoneRechargeManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestableSandboxManager(ApiSettings settings, HttpMessageHandler handler) : SandboxManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestablePixAutomaticManager(ApiSettings settings, HttpMessageHandler handler) : PixAutomaticManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} + +public class TestablePixRecurringManager(ApiSettings settings, HttpMessageHandler handler) : PixRecurringManager(settings) +{ + protected override HttpClient BuildHttpClient() + { + var client = new HttpClient(handler); + client.BaseAddress = new System.Uri("https://api-sandbox.asaas.com"); + return client; + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/AnticipationIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/AnticipationIntegrationTests.cs new file mode 100644 index 0000000..85dc85c --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/AnticipationIntegrationTests.cs @@ -0,0 +1,19 @@ +namespace Codout.Apis.Asaas.Tests.Integration; + +public class AnticipationIntegrationTests : IntegrationTestBase +{ + [IntegrationFact] + public async Task ListAnticipations_ReturnsEnvelope() + { + var result = await Asaas.Anticipation.List(0, 5); + Assert.True(result.WasSuccessful(), $"ListAnticipations falhou: {string.Join(",", result.Errors)}"); + } + + [IntegrationFact] + public async Task GetLimits_ReturnsAnticipationLimits() + { + var result = await Asaas.Anticipation.GetLimits(); + Assert.True(result.WasSuccessful(), $"GetLimits falhou: {string.Join(",", result.Errors)}"); + Assert.NotNull(result.Data); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/CustomerIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/CustomerIntegrationTests.cs new file mode 100644 index 0000000..8045472 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/CustomerIntegrationTests.cs @@ -0,0 +1,68 @@ +using System; +using Codout.Apis.Asaas.Models.Customer; + +namespace Codout.Apis.Asaas.Tests.Integration; + +/// +/// Integration tests reais contra api-sandbox.asaas.com. +/// Skip automatico se ASAAS_SANDBOX_TOKEN nao estiver definido. +/// +/// Para rodar: +/// $env:ASAAS_SANDBOX_TOKEN = "aact_YTU0...seu_token_sandbox..." +/// dotnet test --filter "Category=Integration" +/// +public class CustomerIntegrationTests : IntegrationTestBase +{ + [IntegrationFact] + public async Task Create_Find_Update_Delete_Customer_RoundTrip() + { + // Suffix unico por execucao para evitar colisao no sandbox + var stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); + + // 1. CREATE + var created = await Asaas.Customer.Create(new CreateCustomerRequest + { + Name = $"ContractTest Customer {stamp}", + Email = $"contract+{stamp}@example.com", + CpfCnpj = "24971563792", // CPF sandbox valido (geradores publicos) + MobilePhone = "11999998888", + ExternalReference = $"contract-{stamp}" + }); + + Assert.True(created.WasSuccessful(), $"Create falhou: {string.Join(",", created.Errors)}"); + Assert.NotNull(created.Data); + Assert.StartsWith("cus_", created.Data.Id); + Assert.Equal($"ContractTest Customer {stamp}", created.Data.Name); + + // 2. FIND + var found = await Asaas.Customer.Find(created.Data.Id); + Assert.True(found.WasSuccessful()); + Assert.Equal(created.Data.Id, found.Data.Id); + Assert.Equal(created.Data.Email, found.Data.Email); + + // 3. UPDATE + var updated = await Asaas.Customer.Update(created.Data.Id, new UpdateCustomerRequest + { + Name = $"ContractTest Customer {stamp} (updated)" + }); + Assert.True(updated.WasSuccessful()); + Assert.Equal($"ContractTest Customer {stamp} (updated)", updated.Data.Name); + + // 4. DELETE (cleanup) + var deleted = await Asaas.Customer.Delete(created.Data.Id); + Assert.True(deleted.WasSuccessful()); + } + + [IntegrationFact] + public async Task List_RespectsOffsetAndLimit_AndUsesStandardEnvelope() + { + var result = await Asaas.Customer.List(0, 3); + + Assert.True(result.WasSuccessful()); + Assert.NotNull(result.Data); + Assert.True(result.Data.Count <= 3, "limit=3 deveria garantir <=3 items"); + Assert.Equal(3, result.Limit); + Assert.Equal(0, result.Offset); + Assert.True(result.TotalCount >= 0); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/FinanceIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/FinanceIntegrationTests.cs new file mode 100644 index 0000000..f80a19d --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/FinanceIntegrationTests.cs @@ -0,0 +1,40 @@ +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Finance; +using Codout.Apis.Asaas.Models.Payment.Enums; + +namespace Codout.Apis.Asaas.Tests.Integration; + +public class FinanceIntegrationTests : IntegrationTestBase +{ + [IntegrationFact] + public async Task GetBalance_ReturnsValue() + { + var result = await Asaas.Finance.GetBalance(); + Assert.True(result.WasSuccessful(), $"GetBalance falhou: {string.Join(",", result.Errors)}"); + Assert.NotNull(result.Data); + } + + [IntegrationFact] + public async Task GetPaymentStatistics_WithFilter_SerializesCorrectly() + { + // B-37b regression sandbox: filtros novos enviados ao endpoint. + var filter = new PaymentStatisticsFilter + { + BillingType = BillingType.PIX, + Status = PaymentStatus.RECEIVED, + Anticipated = false + }; + + var result = await Asaas.Finance.GetPaymentStatistics(filter); + Assert.True(result.WasSuccessful(), $"GetPaymentStatistics com filter falhou: {string.Join(",", result.Errors)}"); + } + + [IntegrationFact] + public async Task GetSplitStatistics_ReturnsIncomeAndValue() + { + // B-37a regression sandbox: garantir que income/value shape funcionam. + var result = await Asaas.Finance.GetSplitStatistics(); + Assert.True(result.WasSuccessful(), $"GetSplitStatistics falhou: {string.Join(",", result.Errors)}"); + Assert.NotNull(result.Data); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/IntegrationFactAttribute.cs b/Codout.Apis.Asaas.Tests/Integration/IntegrationFactAttribute.cs new file mode 100644 index 0000000..bfff3f6 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/IntegrationFactAttribute.cs @@ -0,0 +1,28 @@ +using System; + +namespace Codout.Apis.Asaas.Tests.Integration; + +/// +/// Marca um teste como integration test que chama o sandbox real do Asaas. +/// Skip automatico se a variavel de ambiente ASAAS_SANDBOX_TOKEN nao +/// estiver definida — assim CI local e suites unit nao quebram, e o +/// pipeline com credenciais executa. +/// +/// +/// Use trait "Category=Integration" para filtrar:
+/// dotnet test --filter "Category=Integration" +///
+public sealed class IntegrationFactAttribute : FactAttribute +{ + public const string EnvVarName = "ASAAS_SANDBOX_TOKEN"; + + public IntegrationFactAttribute() + { + var token = Environment.GetEnvironmentVariable(EnvVarName); + if (string.IsNullOrWhiteSpace(token)) + { + Skip = $"Integration test skipped: variavel de ambiente {EnvVarName} nao definida. " + + "Para rodar, exporte um access_token de sandbox: $env:ASAAS_SANDBOX_TOKEN='aact_YTU0...'"; + } + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/IntegrationTestBase.cs b/Codout.Apis.Asaas.Tests/Integration/IntegrationTestBase.cs new file mode 100644 index 0000000..0f29134 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/IntegrationTestBase.cs @@ -0,0 +1,25 @@ +using System; +using Codout.Apis.Asaas.Core; + +namespace Codout.Apis.Asaas.Tests.Integration; + +/// +/// Base para testes de integracao reais contra api-sandbox.asaas.com. +/// Constroi AsaasApi de verdade (sem mocks) usando o token de sandbox da env var. +/// +[Trait("Category", "Integration")] +public abstract class IntegrationTestBase +{ + protected AsaasApi Asaas { get; } + + protected IntegrationTestBase() + { + var token = Environment.GetEnvironmentVariable(IntegrationFactAttribute.EnvVarName) + ?? throw new InvalidOperationException( + $"{IntegrationFactAttribute.EnvVarName} nao definida. " + + "IntegrationFact deveria ter dado skip antes de chegar aqui."); + + var settings = new ApiSettings(token, "AsaasSdkContractTests", AsaasEnvironment.SANDBOX); + Asaas = new AsaasApi(settings); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/PaymentIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/PaymentIntegrationTests.cs new file mode 100644 index 0000000..6372fb5 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/PaymentIntegrationTests.cs @@ -0,0 +1,96 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Customer; +using Codout.Apis.Asaas.Models.Payment; + +namespace Codout.Apis.Asaas.Tests.Integration; + +/// +/// Integration tests de Payment contra sandbox real. +/// +public class PaymentIntegrationTests : IntegrationTestBase +{ + private async Task CreateSandboxCustomer(string suffix) + { + var stamp = DateTime.UtcNow.ToString("HHmmssfff"); + var customer = await Asaas.Customer.Create(new CreateCustomerRequest + { + Name = $"ContractTest {suffix} {stamp}", + CpfCnpj = "24971563792", + Email = $"pay-{suffix}-{stamp}@example.com" + }); + Assert.True(customer.WasSuccessful(), $"Setup customer falhou: {string.Join(",", customer.Errors)}"); + return customer.Data.Id; + } + + [IntegrationFact] + public async Task CreatePayment_Boleto_ReturnsPendingPayment() + { + var customerId = await CreateSandboxCustomer("boleto"); + try + { + var result = await Asaas.Payment.Create(new CreatePaymentRequest + { + CustomerId = customerId, + BillingType = BillingType.BOLETO, + Value = 100m, + DueDate = DateTime.UtcNow.Date.AddDays(7), + Description = "Contract test boleto" + }); + + Assert.True(result.WasSuccessful(), $"Create boleto falhou: {string.Join(",", result.Errors)}"); + Assert.NotNull(result.Data); + Assert.StartsWith("pay_", result.Data.Id); + Assert.Equal(BillingType.BOLETO, result.Data.BillingType); + Assert.Equal(100m, result.Data.Value); + } + finally + { + await Asaas.Customer.Delete(customerId); + } + } + + [IntegrationFact] + public async Task CreatePayment_Pix_ReturnsPixQrCode() + { + var customerId = await CreateSandboxCustomer("pix"); + try + { + // 1. cria cobranca PIX + var payment = await Asaas.Payment.Create(new CreatePaymentRequest + { + CustomerId = customerId, + BillingType = BillingType.PIX, + Value = 50m, + DueDate = DateTime.UtcNow.Date.AddDays(3), + Description = "Contract test PIX" + }); + Assert.True(payment.WasSuccessful(), $"Create PIX falhou: {string.Join(",", payment.Errors)}"); + + // 2. recupera QR code (endpoint GET /v3/payments/{id}/pixQrCode) + var qr = await Asaas.Payment.GetPixQrCode(payment.Data.Id); + Assert.True(qr.WasSuccessful(), $"GetPixQrCode falhou: {string.Join(",", qr.Errors)}"); + Assert.NotNull(qr.Data); + Assert.NotEmpty(qr.Data.Payload); + Assert.NotEmpty(qr.Data.EncodedImage); // base64 PNG + } + finally + { + await Asaas.Customer.Delete(customerId); + } + } + + [IntegrationFact] + public async Task ListPayments_WithAnticipatedFilter_SerializesBoolCorrectly() + { + // Garantia sistemica: o filtro bool? Anticipated serializa "true" + // (lowercase) no query string. A API ignora silenciosamente o param + // se estiver com casing errado, entao o teste aqui valida que NAO + // ha erro 400 ao mandar com o casing correto. + var filter = new PaymentListFilter { Anticipated = true }; + + var result = await Asaas.Payment.List(0, 5, filter); + + Assert.True(result.WasSuccessful(), $"List com Anticipated=true falhou: {string.Join(",", result.Errors)}"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/PixIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/PixIntegrationTests.cs new file mode 100644 index 0000000..5487e78 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/PixIntegrationTests.cs @@ -0,0 +1,24 @@ +using Codout.Apis.Asaas.Models.Pix; +using Codout.Apis.Asaas.Models.Pix.Enums; + +namespace Codout.Apis.Asaas.Tests.Integration; + +public class PixIntegrationTests : IntegrationTestBase +{ + [IntegrationFact] + public async Task ListAddressKeys_ReturnsEnvelope() + { + var result = await Asaas.Pix.ListAddressKeys(0, 5); + Assert.True(result.WasSuccessful(), $"ListAddressKeys falhou: {string.Join(",", result.Errors)}"); + } + + [IntegrationFact] + public async Task ListTransactions_WithStatusFilter_SerializesEnumCorrectly() + { + // B-28a regression sandbox: garantir que filtro com PixTransactionStatus.AWAITING_REQUEST + // (novo valor adicionado) nao retorna 400. + var filter = new PixTransactionListFilter { Status = PixTransactionStatus.AWAITING_REQUEST }; + var result = await Asaas.Pix.ListTransactions(0, 5, filter); + Assert.True(result.WasSuccessful(), $"ListTransactions falhou: {string.Join(",", result.Errors)}"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/SubscriptionIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/SubscriptionIntegrationTests.cs new file mode 100644 index 0000000..f4766df --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/SubscriptionIntegrationTests.cs @@ -0,0 +1,62 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Customer; +using Codout.Apis.Asaas.Models.Subscription; +using Codout.Apis.Asaas.Models.Subscription.Enums; + +namespace Codout.Apis.Asaas.Tests.Integration; + +public class SubscriptionIntegrationTests : IntegrationTestBase +{ + private async Task CreateSandboxCustomer(string suffix) + { + var stamp = DateTime.UtcNow.ToString("HHmmssfff"); + var c = await Asaas.Customer.Create(new CreateCustomerRequest + { + Name = $"ContractTest {suffix} {stamp}", + CpfCnpj = "24971563792", + Email = $"sub-{suffix}-{stamp}@example.com" + }); + Assert.True(c.WasSuccessful(), $"Setup customer falhou: {string.Join(",", c.Errors)}"); + return c.Data.Id; + } + + [IntegrationFact] + public async Task CreateSubscription_Boleto_RoundTrip() + { + var customerId = await CreateSandboxCustomer("sub"); + try + { + var created = await Asaas.Subscription.Create(new CreateSubscriptionRequest + { + CustomerId = customerId, + BillingType = BillingType.BOLETO, + Value = 19.9m, + NextDueDate = DateTime.UtcNow.Date.AddDays(7), + Cycle = Cycle.MONTHLY, + Description = "Contract test subscription" + }); + + Assert.True(created.WasSuccessful(), $"Create sub falhou: {string.Join(",", created.Errors)}"); + Assert.StartsWith("sub_", created.Data.Id); + Assert.Equal(SubscriptionStatus.ACTIVE, created.Data.Status); + + // Cleanup + await Asaas.Subscription.Delete(created.Data.Id); + } + finally + { + await Asaas.Customer.Delete(customerId); + } + } + + [IntegrationFact] + public async Task ListSubscriptions_WithFilterAndStatus_SerializesEnumCorrectly() + { + // B-27a regression sandbox: garantir que filtro com SubscriptionStatus.INACTIVE + // (valor novo do enum) nao retorna 400. + var filter = new SubscriptionListFilter { Status = SubscriptionStatus.INACTIVE }; + var result = await Asaas.Subscription.List(0, 5, filter); + Assert.True(result.WasSuccessful(), $"List com status=INACTIVE falhou: {string.Join(",", result.Errors)}"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Integration/TransferIntegrationTests.cs b/Codout.Apis.Asaas.Tests/Integration/TransferIntegrationTests.cs new file mode 100644 index 0000000..7fc4a5a --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Integration/TransferIntegrationTests.cs @@ -0,0 +1,21 @@ +using System; +using Codout.Apis.Asaas.Models.Transfer; + +namespace Codout.Apis.Asaas.Tests.Integration; + +public class TransferIntegrationTests : IntegrationTestBase +{ + [IntegrationFact] + public async Task ListTransfers_WithDateRangeFilter_SerializesCorrectly() + { + // B-29h regression sandbox: filter usa [ge]/[le] lowercase. + var filter = new TransferListFilter + { + DateCreatedGE = DateTime.UtcNow.Date.AddDays(-30), + DateCreatedLE = DateTime.UtcNow.Date + }; + + var result = await Asaas.Transfer.List(0, 5, filter); + Assert.True(result.WasSuccessful(), $"ListTransfers falhou: {string.Join(",", result.Errors)}"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/AnticipationManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/AnticipationManagerTests.cs index a851b00..7b0a3ad 100644 --- a/Codout.Apis.Asaas.Tests/Managers/AnticipationManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/AnticipationManagerTests.cs @@ -50,7 +50,7 @@ public async Task Create_DeserializesResponse() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("ant_123", result.Data.Id); Assert.Equal("inst_1", result.Data.InstallmentId); @@ -85,7 +85,7 @@ public async Task Simulate_DeserializesResponse() var result = await Manager.Simulate(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inst_1", result.Data.InstallmentId); Assert.Equal("pay_1", result.Data.PaymentId); @@ -115,7 +115,7 @@ public async Task Find_DeserializesResponse() var result = await Manager.Find("ant_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("ant_456", result.Data.Id); Assert.Equal(500.00m, result.Data.TotalValue); @@ -160,7 +160,7 @@ public async Task List_DeserializesResponse() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2, result.Data.Count); Assert.Equal(2, result.TotalCount); @@ -168,33 +168,56 @@ public async Task List_DeserializesResponse() Assert.Equal("ant_2", result.Data[1].Id); } - // ── SignAgreement ─────────────────────────────────────────────── + // ── Cancel / GetLimits / Configurations ───────────────────────── [Fact] - public async Task SignAgreement_SendsPostToCorrectUrl() + public async Task Cancel_SendsPostToCancelRoute() { - SetupOkResponse("{\"id\":\"ant_789\",\"status\":\"CREDITED\"}"); + SetupOkResponse("{\"id\":\"ant_42\",\"status\":\"CANCELLED\"}"); - var request = new SignAnticipationAgreementRequest { Agreed = true }; - - var result = await Manager.SignAgreement(request); + var result = await Manager.Cancel("ant_42"); AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/anticipations/agreement/sign"); + AssertRequestUrl("/v3/anticipations/ant_42/cancel"); } [Fact] - public async Task SignAgreement_DeserializesResponse() + public async Task GetLimits_SendsGetToLimitsRoute() { - SetupOkResponse("{\"id\":\"ant_789\",\"status\":\"CREDITED\",\"totalValue\":300.00}"); + SetupOkResponse("{\"bankSlip\":{\"total\":1000,\"available\":800,\"used\":200},\"creditCard\":{\"total\":2000,\"available\":1500,\"used\":500}}"); - var request = new SignAnticipationAgreementRequest { Agreed = true }; + var result = await Manager.GetLimits(); - var result = await Manager.SignAgreement(request); + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/anticipations/limits"); + Assert.True(result.WasSuccessful()); + Assert.Equal(800m, result.Data.BankSlip.Available); + Assert.Equal(1500m, result.Data.CreditCard.Available); + } - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal("ant_789", result.Data.Id); + [Fact] + public async Task GetAutomaticConfiguration_SendsGetToConfigurationsRoute() + { + SetupOkResponse("{\"bankSlipEnabled\":true,\"creditCardEnabled\":false}"); + + var result = await Manager.GetAutomaticConfiguration(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/anticipations/configurations"); + Assert.True(result.Data.BankSlipEnabled); + Assert.False(result.Data.CreditCardEnabled); + } + + [Fact] + public async Task UpdateAutomaticConfiguration_SendsPutToConfigurationsRoute() + { + SetupOkResponse("{\"bankSlipEnabled\":true,\"creditCardEnabled\":true}"); + var request = new UpdateAutomaticAnticipationConfigRequest { BankSlipEnabled = true, CreditCardEnabled = true }; + + var result = await Manager.UpdateAutomaticConfiguration(request); + + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/anticipations/configurations"); } // ── Error handling ────────────────────────────────────────────── @@ -206,7 +229,7 @@ public async Task Find_OnError_ReturnsErrorResponse() var result = await Manager.Find("ant_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -225,7 +248,7 @@ public async Task Create_OnError_ReturnsErrorResponse() }; var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); diff --git a/Codout.Apis.Asaas.Tests/Managers/AsaasAccountManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/AsaasAccountManagerTests.cs index e0a515e..909a8c7 100644 --- a/Codout.Apis.Asaas.Tests/Managers/AsaasAccountManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/AsaasAccountManagerTests.cs @@ -41,7 +41,7 @@ public async Task Create_DeserializesResponse() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("Test Company", result.Data.Name); Assert.Equal("test@test.com", result.Data.Email); @@ -90,7 +90,7 @@ public async Task List_DeserializesResponse() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2, result.Data.Count); Assert.Equal(2, result.TotalCount); @@ -109,7 +109,7 @@ public async Task Create_OnError_ReturnsErrorResponse() var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -122,8 +122,80 @@ public async Task List_OnError_ReturnsErrorResponse() var result = await Manager.List(0, 10); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode); Assert.NotEmpty(result.Errors); } + + // ── Find / ResendActivationLink ───────────────────────────────── + + [Fact] + public async Task Find_SendsGetToAccountsId() + { + SetupOkResponse("{\"id\":\"acc_1\",\"name\":\"Sub\"}"); + + var result = await Manager.Find("acc_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/accounts/acc_1"); + } + + [Fact] + public async Task ResendActivationLink_SendsPostToResendRoute() + { + SetupOkResponse("{\"id\":\"acc_1\",\"name\":\"Sub\"}"); + + var result = await Manager.ResendActivationLink("acc_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/accounts/acc_1/resendActivationLink"); + } + + // ── AccessTokens ──────────────────────────────────────────────── + + [Fact] + public async Task CreateAccessToken_SendsPostToAccessTokensRoute() + { + SetupOkResponse("{\"id\":\"tok_1\",\"name\":\"key01\"}"); + var request = new CreateAccessTokenRequest { Name = "key01" }; + + var result = await Manager.CreateAccessToken("acc_1", request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/accounts/acc_1/accessTokens"); + } + + [Fact] + public async Task ListAccessTokens_SendsGetToAccessTokensRoute() + { + SetupListResponse("[{\"id\":\"tok_1\",\"name\":\"key01\"}]"); + + var result = await Manager.ListAccessTokens("acc_1", 0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/accounts/acc_1/accessTokens"); + } + + [Fact] + public async Task UpdateAccessToken_SendsPutToAccessTokenIdRoute() + { + SetupOkResponse("{\"id\":\"tok_1\",\"enabled\":false}"); + var request = new UpdateAccessTokenRequest { Enabled = false }; + + var result = await Manager.UpdateAccessToken("acc_1", "tok_1", request); + + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/accounts/acc_1/accessTokens/tok_1"); + } + + [Fact] + public async Task DeleteAccessToken_SendsDeleteToAccessTokenIdRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"tok_1\"}"); + + var result = await Manager.DeleteAccessToken("acc_1", "tok_1"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/accounts/acc_1/accessTokens/tok_1"); + } } diff --git a/Codout.Apis.Asaas.Tests/Managers/BillPaymentManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/BillPaymentManagerTests.cs index 63feaf7..7aadf8e 100644 --- a/Codout.Apis.Asaas.Tests/Managers/BillPaymentManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/BillPaymentManagerTests.cs @@ -46,7 +46,7 @@ public async Task Create_DeserializesResponse() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("bill_123", result.Data.Id); Assert.Equal(100.00m, result.Data.Value); @@ -86,7 +86,7 @@ public async Task Simulate_DeserializesResponse() var result = await Manager.Simulate(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2.50m, result.Data.Fee); Assert.NotNull(result.Data.BankSlipInfo); @@ -112,7 +112,7 @@ public async Task Find_DeserializesResponse() var result = await Manager.Find("bill_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("bill_456", result.Data.Id); Assert.Equal(250.00m, result.Data.Value); @@ -155,7 +155,7 @@ public async Task List_DeserializesResponse() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(3, result.Data.Count); Assert.Equal(3, result.TotalCount); @@ -186,7 +186,7 @@ public async Task Cancel_DeserializesResponse() var result = await Manager.Cancel("bill_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("bill_123", result.Data.Id); Assert.False(result.Data.CanBeCancelled); @@ -203,7 +203,7 @@ public async Task Create_OnError_ReturnsErrorResponse() var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -217,7 +217,7 @@ public async Task Find_OnError_ReturnsErrorResponse() var result = await Manager.Find("bill_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -229,7 +229,7 @@ public async Task Cancel_OnError_ReturnsErrorResponse() var result = await Manager.Cancel("bill_already_paid"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.UnprocessableEntity, result.StatusCode); Assert.NotEmpty(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/ChargebackManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/ChargebackManagerTests.cs new file mode 100644 index 0000000..7338306 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/ChargebackManagerTests.cs @@ -0,0 +1,56 @@ +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.Chargeback; +using Codout.Apis.Asaas.Models.Chargeback.Enums; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class ChargebackManagerTests : ManagerTestBase +{ + protected override ChargebackManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableChargebackManager(settings, handler); + + [Fact] + public async Task List_SendsGetToChargebacksRoute() + { + SetupListResponse("[{\"id\":\"chrg_1\",\"payment\":\"pay_1\",\"status\":\"REQUESTED\",\"reason\":\"FRAUD\",\"value\":100}]", totalCount: 1); + + var result = await Manager.List(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/chargebacks"); + Assert.True(result.WasSuccessful()); + Assert.Equal("chrg_1", result.Data[0].Id); + Assert.Equal(ChargebackStatus.REQUESTED, result.Data[0].Status); + Assert.Equal(ChargebackReason.FRAUD, result.Data[0].Reason); + } + + [Fact] + public async Task FindByPayment_SendsGetToPaymentChargebackRoute() + { + SetupOkResponse("{\"id\":\"chrg_1\",\"payment\":\"pay_1\",\"status\":\"DONE\",\"reason\":\"COMMERCIAL_DISAGREEMENT\",\"value\":50}"); + + var result = await Manager.FindByPayment("pay_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/chargeback"); + Assert.True(result.WasSuccessful()); + Assert.Equal("chrg_1", result.Data.Id); + Assert.Equal(ChargebackStatus.DONE, result.Data.Status); + } + + [Fact] + public async Task CreateDispute_SendsPostToDisputeRoute() + { + SetupOkResponse("{\"id\":\"chrg_1\",\"disputeStatus\":\"REQUESTED\"}"); + + var request = new CreateChargebackDisputeRequest { Description = "Tenho prova de entrega" }; + + var result = await Manager.CreateDispute("chrg_1", request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/chargebacks/chrg_1/dispute"); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/CheckoutManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/CheckoutManagerTests.cs new file mode 100644 index 0000000..c8a6f0c --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/CheckoutManagerTests.cs @@ -0,0 +1,80 @@ +using System.Net; +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.Checkout; +using Codout.Apis.Asaas.Models.Checkout.Enums; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class CheckoutManagerTests : ManagerTestBase +{ + protected override CheckoutManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableCheckoutManager(settings, handler); + + [Fact] + public async Task Create_SendsPostToCheckoutsRoute() + { + SetupOkResponse("{\"id\":\"ck_1\",\"status\":\"ACTIVE\",\"link\":\"https://sandbox.asaas.com/checkoutSession/show/ck_1\",\"billingTypes\":[\"CREDIT_CARD\"],\"chargeTypes\":[\"DETACHED\"]}"); + var request = new CreateCheckoutRequest + { + BillingTypes = [CheckoutBillingType.CREDIT_CARD], + ChargeTypes = [CheckoutChargeType.DETACHED], + Callback = new CheckoutCallback { SuccessUrl = "https://example.com/ok", CancelUrl = "https://example.com/cancel" }, + Items = [new CheckoutItem { Name = "Produto", Quantity = 1, Value = 100m, ImageBase64 = "..." }] + }; + + var result = await Manager.Create(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/checkouts"); + Assert.True(result.WasSuccessful()); + Assert.Equal("ck_1", result.Data.Id); + Assert.Equal(CheckoutStatus.ACTIVE, result.Data.Status); + Assert.Contains(CheckoutBillingType.CREDIT_CARD, result.Data.BillingTypes); + } + + [Fact] + public async Task Create_SerializesItemsAndCallback() + { + SetupOkResponse("{\"id\":\"ck_2\"}"); + var request = new CreateCheckoutRequest + { + BillingTypes = [CheckoutBillingType.PIX], + ChargeTypes = [CheckoutChargeType.DETACHED], + Callback = new CheckoutCallback { SuccessUrl = "https://example.com/ok", CancelUrl = "https://example.com/cancel" }, + Items = [new CheckoutItem { Name = "Servico", Quantity = 2, Value = 50m, ImageBase64 = "X" }] + }; + + await Manager.Create(request); + + Assert.NotNull(Handler.LastRequestContent); + Assert.Contains("\"billingTypes\":[\"PIX\"]", Handler.LastRequestContent); + Assert.Contains("\"successUrl\":\"https://example.com/ok\"", Handler.LastRequestContent); + Assert.Contains("\"items\":[", Handler.LastRequestContent); + } + + [Fact] + public async Task Cancel_SendsPostToCheckoutCancelRoute() + { + SetupOkResponse("{\"id\":\"ck_1\",\"status\":\"CANCELED\"}"); + + var result = await Manager.Cancel("ck_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/checkouts/ck_1/cancel"); + } + + [Fact] + public async Task Create_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(HttpStatusCode.BadRequest); + var request = new CreateCheckoutRequest(); + + var result = await Manager.Create(request); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/CreditBureauReportManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/CreditBureauReportManagerTests.cs index 3ea7353..8b03a89 100644 --- a/Codout.Apis.Asaas.Tests/Managers/CreditBureauReportManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/CreditBureauReportManagerTests.cs @@ -17,26 +17,25 @@ protected override CreditBureauReportManager CreateManager(ApiSettings settings, [Fact] public async Task Create_SendsPostRequest() { - SetupOkResponse("{\"id\":\"cbr_123\",\"customer\":\"cus_abc\",\"cpfCnpj\":\"12345678901\",\"state\":\"SP\",\"status\":\"PENDING\",\"dateCreated\":\"2024-01-15\"}"); + SetupOkResponse("{\"id\":\"cbr_123\",\"customer\":\"cus_abc\",\"cpfCnpj\":\"12345678901\",\"dateCreated\":\"2024-01-15\",\"downloadUrl\":\"https://example/cbr.pdf\",\"reportFile\":\"JVBERi0xLjQK\"}"); var request = new CreateCreditBureauReportRequest { Customer = "cus_abc", - CpfCnpj = "12345678901", - State = "SP" + CpfCnpj = "12345678901" }; var result = await Manager.Create(request); AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/creditBureauReport"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("cbr_123", result.Data.Id); Assert.Equal("cus_abc", result.Data.Customer); Assert.Equal("12345678901", result.Data.CpfCnpj); - Assert.Equal("SP", result.Data.State); - Assert.Equal("PENDING", result.Data.Status); + Assert.Equal("https://example/cbr.pdf", result.Data.DownloadUrl); + Assert.Equal("JVBERi0xLjQK", result.Data.ReportFile); } [Fact] @@ -47,8 +46,7 @@ public async Task Create_SerializesRequestBody() var request = new CreateCreditBureauReportRequest { Customer = "cus_test", - CpfCnpj = "98765432100", - State = "RJ" + CpfCnpj = "98765432100" }; await Manager.Create(request); @@ -56,7 +54,6 @@ public async Task Create_SerializesRequestBody() Assert.NotNull(Handler.LastRequestContent); Assert.Contains("\"customer\":\"cus_test\"", Handler.LastRequestContent); Assert.Contains("\"cpfCnpj\":\"98765432100\"", Handler.LastRequestContent); - Assert.Contains("\"state\":\"RJ\"", Handler.LastRequestContent); } #endregion @@ -66,7 +63,7 @@ public async Task Create_SerializesRequestBody() [Fact] public async Task List_SendsGetRequest() { - SetupListResponse("[{\"id\":\"cbr_1\",\"customer\":\"cus_abc\",\"cpfCnpj\":\"12345678901\",\"state\":\"SP\",\"status\":\"DONE\",\"dateCreated\":\"2024-01-15\"}]", totalCount: 1, limit: 10, offset: 0); + SetupListResponse("[{\"id\":\"cbr_1\",\"customer\":\"cus_abc\",\"cpfCnpj\":\"12345678901\",\"dateCreated\":\"2024-01-15\"}]", totalCount: 1, limit: 10, offset: 0); var result = await Manager.List(0, 10); @@ -74,10 +71,9 @@ public async Task List_SendsGetRequest() AssertRequestUrlContains("/v3/creditBureauReport"); AssertRequestUrlContains("offset=0"); AssertRequestUrlContains("limit=10"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Single(result.Data); Assert.Equal("cbr_1", result.Data[0].Id); - Assert.Equal("DONE", result.Data[0].Status); } [Fact] @@ -100,16 +96,15 @@ public async Task List_RespectsOffsetAndLimit() [Fact] public async Task Find_SendsGetRequestWithId() { - SetupOkResponse("{\"id\":\"cbr_456\",\"customer\":\"cus_def\",\"cpfCnpj\":\"11122233344\",\"state\":\"MG\",\"status\":\"DONE\",\"dateCreated\":\"2024-02-20\"}"); + SetupOkResponse("{\"id\":\"cbr_456\",\"customer\":\"cus_def\",\"cpfCnpj\":\"11122233344\",\"dateCreated\":\"2024-02-20\"}"); var result = await Manager.Find("cbr_456"); AssertRequestMethod(HttpMethod.Get); AssertRequestUrl("/v3/creditBureauReport/cbr_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cbr_456", result.Data.Id); Assert.Equal("cus_def", result.Data.Customer); - Assert.Equal("MG", result.Data.State); } #endregion @@ -124,7 +119,7 @@ public async Task Create_ReturnsErrorOnBadRequest() var request = new CreateCreditBureauReportRequest { Customer = "cus_invalid" }; var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -137,7 +132,7 @@ public async Task Find_ReturnsErrorOnNotFound() var result = await Manager.Find("cbr_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.Single(result.Errors); Assert.Equal("not_found", result.Errors[0].Code); diff --git a/Codout.Apis.Asaas.Tests/Managers/CreditCardManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/CreditCardManagerTests.cs index cc9b29f..d4bb6e9 100644 --- a/Codout.Apis.Asaas.Tests/Managers/CreditCardManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/CreditCardManagerTests.cs @@ -44,10 +44,10 @@ public async Task TokenizeCreditCard_DeserializesResponse() var result = await Manager.TokenizeCreditCard(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("4444", result.Data.Number); - Assert.Equal("MASTERCARD", result.Data.Brand); + Assert.Equal(Codout.Apis.Asaas.Models.Common.Enums.CreditCardBrand.MASTERCARD, result.Data.Brand); Assert.Equal("tok_xyz789", result.Data.Token); } @@ -64,10 +64,10 @@ public async Task TokenizeCreditCard_WithFullRequest_DeserializesResponse() var result = await Manager.TokenizeCreditCard(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("1111", result.Data.Number); - Assert.Equal("VISA", result.Data.Brand); + Assert.Equal(Codout.Apis.Asaas.Models.Common.Enums.CreditCardBrand.VISA, result.Data.Brand); Assert.Equal("tok_full_test", result.Data.Token); } @@ -86,7 +86,7 @@ public async Task TokenizeCreditCard_OnBadRequest_ReturnsErrorResponse() var result = await Manager.TokenizeCreditCard(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -106,7 +106,7 @@ public async Task TokenizeCreditCard_OnUnauthorized_ReturnsErrorResponse() var result = await Manager.TokenizeCreditCard(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -124,8 +124,35 @@ public async Task TokenizeCreditCard_OnInternalServerError_ReturnsErrorResponse( var result = await Manager.TokenizeCreditCard(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); Assert.NotEmpty(result.Errors); } + + // ── PreAuthorization Config ───────────────────────────────────── + + [Fact] + public async Task SavePreAuthorizationConfig_SendsPostToConfigRoute() + { + SetupOkResponse("{\"daysToExpire\":5}"); + var request = new SavePreAuthorizationConfigRequest { DaysToExpire = 5 }; + + var result = await Manager.SavePreAuthorizationConfig(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/creditCard/preAuthorization/config"); + Assert.Equal(5, result.Data.DaysToExpire); + } + + [Fact] + public async Task GetPreAuthorizationConfig_SendsGetToConfigRoute() + { + SetupOkResponse("{\"daysToExpire\":7}"); + + var result = await Manager.GetPreAuthorizationConfig(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/creditCard/preAuthorization/config"); + Assert.Equal(7, result.Data.DaysToExpire); + } } diff --git a/Codout.Apis.Asaas.Tests/Managers/CustomerManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/CustomerManagerTests.cs index fd8edf6..482625e 100644 --- a/Codout.Apis.Asaas.Tests/Managers/CustomerManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/CustomerManagerTests.cs @@ -34,7 +34,7 @@ public async Task Create_DeserializesResponseCorrectly() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cus_123", result.Data.Id); Assert.Equal("Test Customer", result.Data.Name); Assert.Equal("test@example.com", result.Data.Email); @@ -49,7 +49,7 @@ public async Task Create_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -76,7 +76,7 @@ public async Task Find_DeserializesResponseCorrectly() var result = await Manager.Find("cus_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cus_456", result.Data.Id); Assert.Equal("Found Customer", result.Data.Name); Assert.False(result.Data.Deleted); @@ -106,7 +106,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("cus_1", result.Data[0].Id); @@ -141,14 +141,14 @@ public async Task List_WithPagination_IncludesOffsetAndLimit() #region Update [Fact] - public async Task Update_SendsPostToCorrectUrl() + public async Task Update_SendsPutToCorrectUrl() { SetupOkResponse("{\"id\":\"cus_123\",\"name\":\"Updated Name\"}"); var request = new UpdateCustomerRequest { Name = "Updated Name" }; var result = await Manager.Update("cus_123", request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/customers/cus_123"); } @@ -160,7 +160,7 @@ public async Task Update_DeserializesResponseCorrectly() var result = await Manager.Update("cus_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cus_123", result.Data.Id); Assert.Equal("Updated Name", result.Data.Name); Assert.Equal("updated@example.com", result.Data.Email); @@ -188,7 +188,7 @@ public async Task Delete_DeserializesResponseCorrectly() var result = await Manager.Delete("cus_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cus_123", result.Data.Id); Assert.True(result.Data.Deleted); } @@ -215,7 +215,7 @@ public async Task Restore_DeserializesResponseCorrectly() var result = await Manager.Restore("cus_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("cus_123", result.Data.Id); Assert.False(result.Data.Deleted); } diff --git a/Codout.Apis.Asaas.Tests/Managers/EscrowManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/EscrowManagerTests.cs new file mode 100644 index 0000000..1580b64 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/EscrowManagerTests.cs @@ -0,0 +1,110 @@ +using System.Net; +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.Escrow; +using Codout.Apis.Asaas.Models.Escrow.Enums; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class EscrowManagerTests : ManagerTestBase +{ + protected override EscrowManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableEscrowManager(settings, handler); + + [Fact] + public async Task SaveSubaccountConfig_SendsPostToAccountsEscrowRoute() + { + SetupOkResponse("{\"daysToExpire\":30,\"enabled\":true,\"isFeePayer\":false}"); + var request = new SaveEscrowConfigRequest { Enabled = true, DaysToExpire = 30, IsFeePayer = false }; + + var result = await Manager.SaveSubaccountConfig("acc_1", request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/accounts/acc_1/escrow"); + Assert.Equal(30, result.Data.DaysToExpire); + Assert.True(result.Data.Enabled); + } + + [Fact] + public async Task GetSubaccountConfig_SendsGetToAccountsEscrowRoute() + { + SetupOkResponse("{\"enabled\":true}"); + + var result = await Manager.GetSubaccountConfig("acc_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/accounts/acc_1/escrow"); + } + + [Fact] + public async Task SaveDefaultConfig_SendsPostToAccountsEscrowRoot() + { + SetupOkResponse("{\"daysToExpire\":30,\"enabled\":true}"); + + var result = await Manager.SaveDefaultConfig(new SaveEscrowConfigRequest { DaysToExpire = 30, Enabled = true }); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/accounts/escrow"); + } + + [Fact] + public async Task GetDefaultConfig_SendsGetToAccountsEscrowRoot() + { + SetupOkResponse("{\"enabled\":false}"); + + var result = await Manager.GetDefaultConfig(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/accounts/escrow"); + } + + [Fact] + public async Task FinishPaymentEscrow_SendsPostToFinishRouteAndDeserializesPayment() + { + // API real retorna PaymentGetResponseDTO (Payment), nao Escrow + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"RECEIVED\",\"value\":100}"); + + var result = await Manager.FinishPaymentEscrow("esc_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/escrow/esc_1/finish"); + Assert.True(result.WasSuccessful()); + Assert.Equal("pay_1", result.Data.Id); + } + + [Fact] + public async Task GetPaymentEscrow_SendsGetToPaymentsEscrowRouteAndDeserializesEnums() + { + SetupOkResponse("{\"id\":\"esc_1\",\"status\":\"DONE\",\"finishReason\":\"EXPIRED\"}"); + + var result = await Manager.GetPaymentEscrow("pay_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/escrow"); + Assert.Equal(EscrowStatus.DONE, result.Data.Status); + Assert.Equal(EscrowFinishReason.EXPIRED, result.Data.FinishReason); + } + + [Fact] + public async Task SaveSubaccountConfig_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(HttpStatusCode.BadRequest); + + var result = await Manager.SaveSubaccountConfig("acc_1", new SaveEscrowConfigRequest()); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } + + [Fact] + public async Task GetPaymentEscrow_OnNotFound_ReturnsError() + { + SetupErrorResponse(HttpStatusCode.NotFound); + + var result = await Manager.GetPaymentEscrow("pay_unknown"); + + Assert.False(result.WasSuccessful()); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/FinanceManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/FinanceManagerTests.cs index 683e8ab..79563ae 100644 --- a/Codout.Apis.Asaas.Tests/Managers/FinanceManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/FinanceManagerTests.cs @@ -12,38 +12,39 @@ public class FinanceManagerTests : ManagerTestBase protected override FinanceManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) => new TestableFinanceManager(settings, handler); - #region Balance + #region GetBalance [Fact] - public async Task Balance_SendsGetToCorrectUrl() + public async Task GetBalance_SendsGetToCorrectUrl() { - SetupOkResponse("12345.67"); + SetupOkResponse("{\"balance\":12345.67}"); - var result = await Manager.Balance(); + var result = await Manager.GetBalance(); AssertRequestMethod(HttpMethod.Get); AssertRequestUrl("/v3/finance/balance"); } [Fact] - public async Task Balance_DeserializesResponseCorrectly() + public async Task GetBalance_DeserializesObjectResponseCorrectly() { - SetupOkResponse("12345.67"); + SetupOkResponse("{\"balance\":5210.96}"); - var result = await Manager.Balance(); + var result = await Manager.GetBalance(); - Assert.True(result.WasSucessfull()); - Assert.Equal(12345.67m, result.Data); + Assert.True(result.WasSuccessful()); + Assert.NotNull(result.Data); + Assert.Equal(5210.96m, result.Data.Value); } [Fact] - public async Task Balance_WhenApiReturnsError_ReturnsErrorResponse() + public async Task GetBalance_WhenApiReturnsError_ReturnsErrorResponse() { SetupErrorResponse(HttpStatusCode.Unauthorized); - var result = await Manager.Balance(); + var result = await Manager.GetBalance(); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -72,7 +73,7 @@ public async Task ListTransactions_DeserializesListResponseCorrectly() var result = await Manager.ListTransactions(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("txn_1", result.Data[0].Id); @@ -131,7 +132,7 @@ public async Task GetPaymentStatistics_DeserializesResponseCorrectly() var result = await Manager.GetPaymentStatistics(); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(10, result.Data.Quantity); Assert.Equal(1500.00m, result.Data.Value); Assert.Equal(1400.00m, result.Data.NetValue); @@ -155,13 +156,14 @@ public async Task GetSplitStatistics_SendsGetToCorrectUrl() [Fact] public async Task GetSplitStatistics_DeserializesResponseCorrectly() { - SetupOkResponse("{\"totalPendingValue\":500.00,\"totalReceivedValue\":3000.00}"); + // B-37a: schema usa {income, value}, nao {totalPendingValue, totalReceivedValue} + SetupOkResponse("{\"income\":500.00,\"value\":3000.00}"); var result = await Manager.GetSplitStatistics(); - Assert.True(result.WasSucessfull()); - Assert.Equal(500.00m, result.Data.TotalPendingValue); - Assert.Equal(3000.00m, result.Data.TotalReceivedValue); + Assert.True(result.WasSuccessful()); + Assert.Equal(500.00m, result.Data.Income); + Assert.Equal(3000.00m, result.Data.Value); } #endregion diff --git a/Codout.Apis.Asaas.Tests/Managers/CustomerFiscalInfoManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/FiscalInfoManagerTests.cs similarity index 56% rename from Codout.Apis.Asaas.Tests/Managers/CustomerFiscalInfoManagerTests.cs rename to Codout.Apis.Asaas.Tests/Managers/FiscalInfoManagerTests.cs index b1095b0..9180dfc 100644 --- a/Codout.Apis.Asaas.Tests/Managers/CustomerFiscalInfoManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/FiscalInfoManagerTests.cs @@ -2,42 +2,40 @@ using System.Net.Http; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Managers; -using Codout.Apis.Asaas.Models.CustomerFiscalInfo; +using Codout.Apis.Asaas.Models.FiscalInfo; using Codout.Apis.Asaas.Tests.Helpers; namespace Codout.Apis.Asaas.Tests.Managers; -public class CustomerFiscalInfoManagerTests : ManagerTestBase +public class FiscalInfoManagerTests : ManagerTestBase { - protected override CustomerFiscalInfoManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) - => new TestableCustomerFiscalInfoManager(settings, handler); + protected override FiscalInfoManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableFiscalInfoManager(settings, handler); #region Find [Fact] - public async Task Find_SendsGetRequest() + public async Task Find_SendsGetRequestToFiscalInfoRoute() { - SetupOkResponse("{\"email\":\"test@example.com\",\"municipalInscription\":\"12345\",\"stateInscription\":\"67890\",\"simplesNacional\":true,\"culturalProjectsPromoter\":false,\"cnae\":\"6201-5/00\",\"specialTaxRegime\":\"MICROEMPRESA\",\"serviceListItem\":\"14.01\",\"rpsSerie\":\"A\",\"rpsNumber\":\"100\",\"loteNumber\":\"1\",\"username\":\"testuser\",\"accessToken\":\"token123\"}"); + SetupOkResponse("{\"email\":\"test@example.com\",\"municipalInscription\":\"12345\",\"simplesNacional\":true,\"culturalProjectsPromoter\":false,\"cnae\":\"6201-5/00\",\"specialTaxRegime\":\"MICROEMPRESA\",\"serviceListItem\":\"14.01\",\"rpsSerie\":\"A\",\"rpsNumber\":100,\"loteNumber\":1,\"username\":\"testuser\"}"); var result = await Manager.Find(); AssertRequestMethod(HttpMethod.Get); - AssertRequestUrl("/v3/customerFiscalInfo"); - Assert.True(result.WasSucessfull()); + AssertRequestUrl("/v3/fiscalInfo"); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("test@example.com", result.Data.Email); Assert.Equal("12345", result.Data.MunicipalInscription); - Assert.Equal("67890", result.Data.StateInscription); Assert.True(result.Data.SimplesNacional); Assert.False(result.Data.CulturalProjectsPromoter); Assert.Equal("6201-5/00", result.Data.Cnae); Assert.Equal("MICROEMPRESA", result.Data.SpecialTaxRegime); Assert.Equal("14.01", result.Data.ServiceListItem); Assert.Equal("A", result.Data.RpsSerie); - Assert.Equal("100", result.Data.RpsNumber); - Assert.Equal("1", result.Data.LoteNumber); + Assert.Equal(100, result.Data.RpsNumber); + Assert.Equal(1, result.Data.LoteNumber); Assert.Equal("testuser", result.Data.Username); - Assert.Equal("token123", result.Data.AccessToken); } #endregion @@ -45,17 +43,17 @@ public async Task Find_SendsGetRequest() #region ListMunicipalOptions [Fact] - public async Task ListMunicipalOptions_SendsGetRequest() + public async Task ListMunicipalOptions_SendsGetToFiscalInfoMunicipalOptions() { SetupListResponse("[{\"id\":\"mo_1\",\"label\":\"Sao Paulo\"},{\"id\":\"mo_2\",\"label\":\"Rio de Janeiro\"}]", totalCount: 2, limit: 100, offset: 0); var result = await Manager.ListMunicipalOptions(); AssertRequestMethod(HttpMethod.Get); - AssertRequestUrlContains("/v3/customerFiscalInfo/municipalOptions"); + AssertRequestUrlContains("/v3/fiscalInfo/municipalOptions"); AssertRequestUrlContains("offset=0"); AssertRequestUrlContains("limit=100"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.Data.Count); Assert.Equal("mo_1", result.Data[0].Id); Assert.Equal("Sao Paulo", result.Data[0].Label); @@ -78,6 +76,38 @@ public async Task ListMunicipalOptions_ParsesListMetadata() #endregion + #region ListServices + + [Fact] + public async Task ListServices_SendsGetToFiscalInfoServices() + { + SetupListResponse("[{\"id\":\"3544\",\"description\":\"1.01 - Analise e desenvolvimento de sistemas\",\"issTax\":5}]", totalCount: 1); + + var result = await Manager.ListServices("1.01"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/fiscalInfo/services"); + AssertRequestUrlContains("description=1.01"); + Assert.True(result.WasSuccessful()); + Assert.Single(result.Data); + Assert.Equal("3544", result.Data[0].Id); + Assert.Equal("1.01 - Analise e desenvolvimento de sistemas", result.Data[0].Description); + Assert.Equal(5m, result.Data[0].IssTax); + } + + [Fact] + public async Task ListServices_IncludesPaginationParameters() + { + SetupListResponse("[]", totalCount: 0, offset: 20, limit: 5); + + var result = await Manager.ListServices("test", offset: 20, limit: 5); + + AssertRequestUrlContains("offset=20"); + AssertRequestUrlContains("limit=5"); + } + + #endregion + #region Error Handling [Fact] @@ -87,7 +117,7 @@ public async Task Find_ReturnsErrorOnUnauthorized() var result = await Manager.Find(); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("unauthorized", result.Errors[0].Code); diff --git a/Codout.Apis.Asaas.Tests/Managers/InstallmentManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/InstallmentManagerTests.cs index 4c4cd96..e13c84c 100644 --- a/Codout.Apis.Asaas.Tests/Managers/InstallmentManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/InstallmentManagerTests.cs @@ -1,7 +1,9 @@ +using System; using System.Net; using System.Net.Http; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.Common.Enums; using Codout.Apis.Asaas.Models.Installment; using Codout.Apis.Asaas.Models.Payment; using Codout.Apis.Asaas.Tests.Helpers; @@ -13,6 +15,86 @@ public class InstallmentManagerTests : ManagerTestBase protected override InstallmentManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) => new TestableInstallmentManager(settings, handler); + #region Create + + [Fact] + public async Task Create_SendsPostToInstallmentsRoot() + { + SetupOkResponse("{\"id\":\"inst_new\",\"installmentCount\":3,\"value\":100.00}"); + var request = new CreateInstallmentRequest + { + CustomerId = "cus_1", + BillingType = BillingType.BOLETO, + Value = 100.00m, + InstallmentCount = 3, + DueDate = new DateTime(2026, 6, 10) + }; + + var result = await Manager.Create(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/installments"); + Assert.True(result.WasSuccessful()); + Assert.Equal("inst_new", result.Data.Id); + } + + [Fact] + public async Task Create_SerializesRequestBody() + { + SetupOkResponse("{\"id\":\"inst_new\"}"); + var request = new CreateInstallmentRequest + { + CustomerId = "cus_1", + BillingType = BillingType.PIX, + Value = 200.00m, + InstallmentCount = 5, + DueDate = new DateTime(2026, 7, 15), + Description = "Curso parcelado" + }; + + await Manager.Create(request); + + Assert.NotNull(Handler.LastRequestContent); + Assert.Contains("\"customer\":\"cus_1\"", Handler.LastRequestContent); + Assert.Contains("\"installmentCount\":5", Handler.LastRequestContent); + Assert.Contains("\"billingType\":\"PIX\"", Handler.LastRequestContent); + } + + [Fact] + public async Task CreateWithCreditCard_SendsPostToInstallmentsRootWithTrailingSlash() + { + SetupOkResponse("{\"id\":\"inst_cc\",\"installmentCount\":4}"); + var request = new CreateInstallmentWithCreditCardRequest + { + CustomerId = "cus_1", + BillingType = BillingType.CREDIT_CARD, + Value = 100.00m, + InstallmentCount = 4, + DueDate = new DateTime(2026, 6, 10), + CreditCardToken = "tok_abc" + }; + + var result = await Manager.CreateWithCreditCard(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/installments/"); + Assert.True(result.WasSuccessful()); + } + + [Fact] + public async Task Create_WhenApiReturnsError_ReturnsErrorResponse() + { + SetupErrorResponse(HttpStatusCode.BadRequest); + var request = new CreateInstallmentRequest { CustomerId = "invalid" }; + + var result = await Manager.Create(request); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } + + #endregion + #region Find [Fact] @@ -33,7 +115,7 @@ public async Task Find_DeserializesResponseCorrectly() var result = await Manager.Find("inst_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("inst_123", result.Data.Id); Assert.Equal(500.00m, result.Data.Value); Assert.Equal(480.00m, result.Data.NetValue); @@ -51,7 +133,7 @@ public async Task Find_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.Find("inst_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -80,7 +162,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("inst_1", result.Data[0].Id); @@ -120,7 +202,7 @@ public async Task Delete_DeserializesResponseCorrectly() var result = await Manager.Delete("inst_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("inst_123", result.Data.Id); Assert.True(result.Data.Deleted); } @@ -147,7 +229,7 @@ public async Task Refund_DeserializesResponseCorrectly() var result = await Manager.Refund("inst_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("inst_123", result.Data.Id); Assert.Equal(500.00m, result.Data.Value); } @@ -176,7 +258,7 @@ public async Task ListPaymentBook_DeserializesResponseCorrectly() var result = await Manager.ListPaymentBook("inst_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(3, result.TotalCount); Assert.Equal(3, result.Data.Count); Assert.Equal("pay_1", result.Data[0].Id); @@ -185,4 +267,83 @@ public async Task ListPaymentBook_DeserializesResponseCorrectly() } #endregion + + #region ListPayments + + [Fact] + public async Task ListPayments_SendsGetToInstallmentsPaymentsRoute() + { + SetupListResponse("[{\"id\":\"pay_1\",\"value\":100.00}]"); + + var result = await Manager.ListPayments("inst_123", 0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/installments/inst_123/payments"); + AssertRequestUrlContains("offset=0"); + AssertRequestUrlContains("limit=10"); + } + + [Fact] + public async Task ListPayments_DeserializesPayments() + { + SetupListResponse("[{\"id\":\"pay_1\",\"value\":100.00},{\"id\":\"pay_2\",\"value\":100.00}]", totalCount: 2); + + var result = await Manager.ListPayments("inst_123", 0, 10); + + Assert.True(result.WasSuccessful()); + Assert.Equal(2, result.Data.Count); + Assert.Equal("pay_1", result.Data[0].Id); + } + + #endregion + + #region CancelPendingPayments + + [Fact] + public async Task CancelPendingPayments_SendsDeleteToInstallmentsPaymentsRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"inst_123\"}"); + + var result = await Manager.CancelPendingPayments("inst_123"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/installments/inst_123/payments"); + } + + #endregion + + #region UpdateSplits + + [Fact] + public async Task UpdateSplits_SendsPutToCorrectUrl() + { + SetupOkResponse("{\"id\":\"inst_123\"}"); + var request = new UpdateInstallmentSplitsRequest + { + Splits = [new InstallmentSplitRequest { WalletId = "wallet_1", FixedValue = 10m }] + }; + + var result = await Manager.UpdateSplits("inst_123", request); + + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/installments/inst_123/splits"); + } + + [Fact] + public async Task UpdateSplits_SerializesRequestBody() + { + SetupOkResponse("{\"id\":\"inst_123\"}"); + var request = new UpdateInstallmentSplitsRequest + { + Splits = [new InstallmentSplitRequest { WalletId = "wallet_1", PercentualValue = 5m }] + }; + + await Manager.UpdateSplits("inst_123", request); + + Assert.NotNull(Handler.LastRequestContent); + Assert.Contains("\"walletId\":\"wallet_1\"", Handler.LastRequestContent); + Assert.Contains("\"percentualValue\":5", Handler.LastRequestContent); + } + + #endregion } diff --git a/Codout.Apis.Asaas.Tests/Managers/InvoiceManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/InvoiceManagerTests.cs index 33548eb..d74c564 100644 --- a/Codout.Apis.Asaas.Tests/Managers/InvoiceManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/InvoiceManagerTests.cs @@ -41,7 +41,7 @@ public async Task Schedule_DeserializesResponse() var result = await Manager.Schedule(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inv_123", result.Data.Id); Assert.Equal("cust_1", result.Data.CustomerId); @@ -87,7 +87,7 @@ public async Task Update_DeserializesResponse() var result = await Manager.Update("inv_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inv_123", result.Data.Id); Assert.Equal("Updated consulting", result.Data.ServiceDescription); @@ -114,7 +114,7 @@ public async Task Find_DeserializesResponse() var result = await Manager.Find("inv_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inv_456", result.Data.Id); Assert.Equal(2000.00m, result.Data.Value); @@ -158,7 +158,7 @@ public async Task List_DeserializesResponse() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2, result.Data.Count); Assert.Equal(2, result.TotalCount); @@ -199,7 +199,7 @@ public async Task Authorize_DeserializesResponse() var result = await Manager.Authorize("inv_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inv_123", result.Data.Id); } @@ -224,41 +224,11 @@ public async Task Cancel_DeserializesResponse() var result = await Manager.Cancel("inv_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("inv_123", result.Data.Id); } - // ── ListMunicipalServices ─────────────────────────────────────── - - [Fact] - public async Task ListMunicipalServices_SendsGetToCorrectUrl() - { - SetupListResponse("[]", totalCount: 0); - - var result = await Manager.ListMunicipalServices("IT"); - - AssertRequestMethod(HttpMethod.Get); - AssertRequestUrlContains("/v3/invoices/municipalServices"); - AssertRequestUrlContains("description=IT"); - } - - [Fact] - public async Task ListMunicipalServices_DeserializesResponse() - { - SetupListResponse("[{\"id\":\"ms_1\",\"description\":\"IT Service\",\"iss\":5.0},{\"id\":\"ms_2\",\"description\":\"IT Consulting\",\"iss\":3.0}]", totalCount: 2); - - var result = await Manager.ListMunicipalServices("IT"); - - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal(2, result.Data.Count); - Assert.Equal("ms_1", result.Data[0].Id); - Assert.Equal("IT Service", result.Data[0].Description); - Assert.Equal(5.0m, result.Data[0].Iss); - Assert.Equal("ms_2", result.Data[1].Id); - } - // ── Error handling ────────────────────────────────────────────── [Fact] @@ -269,7 +239,7 @@ public async Task Schedule_OnError_ReturnsErrorResponse() var request = new CreateInvoiceRequest { PaymentId = "pay_invalid" }; var result = await Manager.Schedule(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -282,7 +252,7 @@ public async Task Find_OnError_ReturnsErrorResponse() var result = await Manager.Find("inv_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.NotEmpty(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/MobilePhoneRechargeManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/MobilePhoneRechargeManagerTests.cs new file mode 100644 index 0000000..e267476 --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/MobilePhoneRechargeManagerTests.cs @@ -0,0 +1,106 @@ +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.MobilePhoneRecharge; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class MobilePhoneRechargeManagerTests : ManagerTestBase +{ + protected override MobilePhoneRechargeManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableMobilePhoneRechargeManager(settings, handler); + + [Fact] + public async Task Create_SendsPostToRechargesRoute() + { + SetupOkResponse("{\"id\":\"rec_1\",\"status\":\"PENDING\"}"); + var request = new CreateMobilePhoneRechargeRequest { PhoneNumber = "11999998888", Value = 20m }; + + var result = await Manager.Create(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/mobilePhoneRecharges"); + } + + [Fact] + public async Task List_SendsGetToRechargesRoute() + { + SetupListResponse("[]"); + + var result = await Manager.List(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/mobilePhoneRecharges"); + } + + [Fact] + public async Task Find_SendsGetToRechargeIdRoute() + { + SetupOkResponse("{\"id\":\"rec_1\",\"status\":\"CONFIRMED\"}"); + + var result = await Manager.Find("rec_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/mobilePhoneRecharges/rec_1"); + } + + [Fact] + public async Task Cancel_SendsPostToCancelRoute() + { + SetupOkResponse("{\"id\":\"rec_1\",\"status\":\"CANCELLED\"}"); + + var result = await Manager.Cancel("rec_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/mobilePhoneRecharges/rec_1/cancel"); + } + + [Fact] + public async Task GetProvider_SendsGetToProviderRoute() + { + SetupOkResponse("{\"name\":\"Vivo\",\"values\":[{\"name\":\"R$ 12,00\",\"bonus\":\"5.0\",\"minValue\":1,\"maxValue\":5}]}"); + + var result = await Manager.GetProvider("11999998888"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/mobilePhoneRecharges/11999998888/provider"); + Assert.Equal("Vivo", result.Data.Name); + Assert.Single(result.Data.Values); + } + + [Fact] + public async Task Find_DeserializesOperatorNameAndCanBeCancelled() + { + SetupOkResponse("{\"id\":\"rec_1\",\"value\":20.00,\"phoneNumber\":\"63997365512\",\"status\":\"CONFIRMED\",\"canBeCancelled\":true,\"operatorName\":\"Vivo\"}"); + + var result = await Manager.Find("rec_1"); + + Assert.True(result.WasSuccessful()); + Assert.Equal("Vivo", result.Data.OperatorName); + Assert.True(result.Data.CanBeCancelled); + Assert.Equal(Codout.Apis.Asaas.Models.MobilePhoneRecharge.Enums.MobilePhoneRechargeStatus.CONFIRMED, result.Data.Status); + } + + [Fact] + public async Task Create_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(System.Net.HttpStatusCode.BadRequest); + var request = new CreateMobilePhoneRechargeRequest { PhoneNumber = "invalid", Value = 0m }; + + var result = await Manager.Create(request); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } + + [Fact] + public async Task Find_OnNotFound_ReturnsError() + { + SetupErrorResponse(System.Net.HttpStatusCode.NotFound); + + var result = await Manager.Find("rec_unknown"); + + Assert.False(result.WasSuccessful()); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/MyAccountManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/MyAccountManagerTests.cs index 1e6fe7d..ac31533 100644 --- a/Codout.Apis.Asaas.Tests/Managers/MyAccountManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/MyAccountManagerTests.cs @@ -4,6 +4,7 @@ using Codout.Apis.Asaas.Managers; using Codout.Apis.Asaas.Models.Common; using Codout.Apis.Asaas.Models.MyAccount; +using Codout.Apis.Asaas.Models.MyAccount.Enums; using Codout.Apis.Asaas.Tests.Helpers; namespace Codout.Apis.Asaas.Tests.Managers; @@ -13,40 +14,109 @@ public class MyAccountManagerTests : ManagerTestBase protected override MyAccountManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) => new TestableMyAccountManager(settings, handler); - // ── Find ──────────────────────────────────────────────────────── + // ── GetCommercialInfo ─────────────────────────────────────────── [Fact] - public async Task Find_SendsGetToCorrectUrl() + public async Task GetCommercialInfo_SendsGetToCommercialInfoRoute() { SetupOkResponse("{\"name\":\"My Company\",\"email\":\"company@test.com\"}"); - var result = await Manager.Find(); + var result = await Manager.GetCommercialInfo(); AssertRequestMethod(HttpMethod.Get); - AssertRequestUrl("/v3/myAccount"); + AssertRequestUrl("/v3/myAccount/commercialInfo"); } [Fact] - public async Task Find_DeserializesResponse() + public async Task GetCommercialInfo_DeserializesResponse() { - SetupOkResponse("{\"name\":\"My Company\",\"email\":\"company@test.com\",\"cpfCnpj\":\"12345678901234\",\"phone\":\"1199998888\",\"mobilePhone\":\"11999887766\",\"address\":\"Rua Principal\",\"addressNumber\":\"500\",\"complement\":\"Sala 10\",\"province\":\"Centro\",\"postalCode\":\"01001000\",\"inscricaoEstadual\":\"123456789\",\"status\":\"ACTIVE\"}"); + SetupOkResponse("{\"name\":\"My Company\",\"email\":\"company@test.com\",\"cpfCnpj\":\"12345678901234\",\"phone\":\"1199998888\",\"mobilePhone\":\"11999887766\",\"address\":\"Rua Principal\",\"addressNumber\":\"500\",\"complement\":\"Sala 10\",\"province\":\"Centro\",\"postalCode\":\"01001000\",\"status\":\"APPROVED\"}"); - var result = await Manager.Find(); + var result = await Manager.GetCommercialInfo(); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("My Company", result.Data.Name); Assert.Equal("company@test.com", result.Data.Email); Assert.Equal("12345678901234", result.Data.CpfCnpj); - Assert.Equal("1199998888", result.Data.Phone); - Assert.Equal("11999887766", result.Data.MobilePhone); - Assert.Equal("Rua Principal", result.Data.Address); - Assert.Equal("500", result.Data.AddressNumber); - Assert.Equal("Sala 10", result.Data.Complement); - Assert.Equal("Centro", result.Data.Province); - Assert.Equal("01001000", result.Data.PostalCode); - Assert.Equal("123456789", result.Data.InscricaoEstadual); - Assert.Equal("ACTIVE", result.Data.Status); + Assert.Equal(Codout.Apis.Asaas.Models.MyAccount.Enums.AccountInfoStatus.APPROVED, result.Data.Status); + } + + // ── UpdateCommercialInfo / GetStatus / DeleteWhiteLabelAccount ── + + [Fact] + public async Task UpdateCommercialInfo_SendsPostToCommercialInfoRoute() + { + SetupOkResponse("{\"name\":\"My Company\",\"email\":\"new@test.com\"}"); + var request = new UpdateCommercialInfoRequest { Name = "My Company", Email = "new@test.com" }; + + var result = await Manager.UpdateCommercialInfo(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/myAccount/commercialInfo"); + } + + [Fact] + public async Task GetStatus_SendsGetToStatusRoute() + { + SetupOkResponse("{\"general\":\"APPROVED\",\"commercialInfo\":\"APPROVED\",\"bankAccountInfo\":\"PENDING\",\"documentation\":\"AWAITING_APPROVAL\"}"); + + var result = await Manager.GetStatus(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/myAccount/status"); + Assert.Equal(AccountApprovalStatus.APPROVED, result.Data.General); + Assert.Equal(AccountApprovalStatus.PENDING, result.Data.BankAccountInfo); + Assert.Equal(AccountApprovalStatus.AWAITING_APPROVAL, result.Data.Documentation); + } + + [Fact] + public async Task DeleteWhiteLabelAccount_SendsDeleteToMyAccountRoot() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"acc_123\"}"); + + var result = await Manager.DeleteWhiteLabelAccount(); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/myAccount"); + } + + // ── Documents ─────────────────────────────────────────────────── + + [Fact] + public async Task ListPendingDocuments_SendsGetToDocumentsRoute() + { + SetupOkResponse("{\"rejectReasons\":null,\"data\":[{\"id\":\"sec_1\",\"title\":\"Identificacao\",\"status\":\"PENDING\",\"type\":\"IDENTIFICATION\",\"documents\":[]}]}"); + + var result = await Manager.ListPendingDocuments(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/myAccount/documents"); + Assert.True(result.WasSuccessful()); + Assert.Single(result.Data.Data); + Assert.Equal("sec_1", result.Data.Data[0].Id); + } + + [Fact] + public async Task ViewDocumentFile_SendsGetToFilesRoute() + { + SetupOkResponse("{\"id\":\"file_1\",\"status\":\"APPROVED\"}"); + + var result = await Manager.ViewDocumentFile("file_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/myAccount/documents/files/file_1"); + } + + [Fact] + public async Task DeleteDocumentFile_SendsDeleteToFilesRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"file_1\"}"); + + var result = await Manager.DeleteDocumentFile("file_1"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/myAccount/documents/files/file_1"); } // ── CreatePaymentCheckoutConfig ───────────────────────────────── @@ -87,7 +157,7 @@ public async Task CreatePaymentCheckoutConfig_DeserializesResponse() var result = await Manager.CreatePaymentCheckoutConfig(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("#FFFFFF", result.Data.LogoBackgroundColor); Assert.Equal("#000000", result.Data.InfoBackgroundColor); @@ -118,7 +188,7 @@ public async Task FindPaymentCheckoutConfig_DeserializesResponse() var result = await Manager.FindPaymentCheckoutConfig(); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("#FF0000", result.Data.LogoBackgroundColor); Assert.False(result.Data.Enabled); @@ -145,7 +215,7 @@ public async Task FindFees_DeserializesResponse() var result = await Manager.FindFees(); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.NotNull(result.Data.Payment); Assert.NotNull(result.Data.Transfer); @@ -175,7 +245,7 @@ public async Task FindAccountNumber_DeserializesResponse() var result = await Manager.FindAccountNumber(); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("0001", result.Data.Agency); Assert.Equal("123456", result.Data.Account); @@ -185,13 +255,13 @@ public async Task FindAccountNumber_DeserializesResponse() // ── Error handling ────────────────────────────────────────────── [Fact] - public async Task Find_OnError_ReturnsErrorResponse() + public async Task GetCommercialInfo_OnError_ReturnsErrorResponse() { SetupErrorResponse(HttpStatusCode.Unauthorized); - var result = await Manager.Find(); + var result = await Manager.GetCommercialInfo(); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -204,7 +274,7 @@ public async Task FindFees_OnError_ReturnsErrorResponse() var result = await Manager.FindFees(); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode); Assert.NotEmpty(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/NotificationManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/NotificationManagerTests.cs index 7be84fe..bbbeaa8 100644 --- a/Codout.Apis.Asaas.Tests/Managers/NotificationManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/NotificationManagerTests.cs @@ -16,7 +16,7 @@ protected override NotificationManager CreateManager(ApiSettings settings, MockH #region Update [Fact] - public async Task Update_SendsPostRequest() + public async Task Update_SendsPutRequest() { SetupOkResponse("{\"id\":\"not_123\",\"customer\":\"cus_abc\",\"enabled\":true,\"emailEnabledForProvider\":true,\"smsEnabledForProvider\":false,\"emailEnabledForCustomer\":true,\"smsEnabledForCustomer\":false,\"phoneCallEnabledForCustomer\":false,\"whatsappEnabledForCustomer\":true,\"scheduleOffset\":5}"); @@ -33,9 +33,9 @@ public async Task Update_SendsPostRequest() var result = await Manager.Update("not_123", request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/notifications/not_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("not_123", result.Data.Id); Assert.Equal("cus_abc", result.Data.Customer); @@ -74,7 +74,7 @@ public async Task Update_SerializesRequestBody() #region BatchUpdate [Fact] - public async Task BatchUpdate_SendsPostRequest() + public async Task BatchUpdate_SendsPutRequest() { SetupOkResponse("{\"notifications\":[{\"id\":\"not_1\",\"customer\":\"cus_abc\",\"enabled\":true,\"emailEnabledForProvider\":true,\"smsEnabledForProvider\":false,\"emailEnabledForCustomer\":false,\"smsEnabledForCustomer\":false,\"phoneCallEnabledForCustomer\":false,\"whatsappEnabledForCustomer\":false},{\"id\":\"not_2\",\"customer\":\"cus_abc\",\"enabled\":false,\"emailEnabledForProvider\":false,\"smsEnabledForProvider\":false,\"emailEnabledForCustomer\":false,\"smsEnabledForCustomer\":false,\"phoneCallEnabledForCustomer\":false,\"whatsappEnabledForCustomer\":false}]}"); @@ -99,9 +99,9 @@ public async Task BatchUpdate_SendsPostRequest() var result = await Manager.BatchUpdate(request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/notifications/batch"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.NotNull(result.Data.Notifications); Assert.Equal(2, result.Data.Notifications.Count); @@ -149,7 +149,7 @@ public async Task Update_ReturnsErrorOnBadRequest() var request = new UpdateNotificationRequest { Enabled = true }; var result = await Manager.Update("invalid_id", request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -167,7 +167,7 @@ public async Task BatchUpdate_ReturnsErrorOnInternalServerError() var result = await Manager.BatchUpdate(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); } diff --git a/Codout.Apis.Asaas.Tests/Managers/PaymentDunningManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PaymentDunningManagerTests.cs index 6e3ecfa..7da0235 100644 --- a/Codout.Apis.Asaas.Tests/Managers/PaymentDunningManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/PaymentDunningManagerTests.cs @@ -48,7 +48,7 @@ public async Task Create_SendsPostToCorrectUrl() [Fact] public async Task Create_DeserializesResponse() { - SetupOkResponse("{\"id\":\"dun_123\",\"dunningNumber\":\"DUN001\",\"status\":\"PENDING\",\"type\":\"CREDIT_BUREAU\",\"payment\":\"pay_1\",\"requestDate\":\"2024-01-10\",\"description\":\"Dunning for overdue payment\",\"value\":500.00,\"feeValue\":25.00,\"netValue\":475.00,\"receivedInCashFeeValue\":10.00,\"canBeCancelled\":true,\"isNecessaryResendDocumentation\":false}"); + SetupOkResponse("{\"id\":\"dun_123\",\"dunningNumber\":15,\"status\":\"PENDING\",\"type\":\"CREDIT_BUREAU\",\"payment\":\"pay_1\",\"requestDate\":\"2024-01-10\",\"description\":\"Dunning for overdue payment\",\"value\":500.00,\"feeValue\":25.00,\"netValue\":475.00,\"receivedInCashFeeValue\":10.00,\"canBeCancelled\":true,\"isNecessaryResendDocumentation\":false}"); var request = new CreatePaymentDunningRequest { @@ -69,16 +69,15 @@ public async Task Create_DeserializesResponse() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("dun_123", result.Data.Id); - Assert.Equal("DUN001", result.Data.DunningNumber); + Assert.Equal(15, result.Data.DunningNumber); Assert.Equal("pay_1", result.Data.PaymentId); Assert.Equal("Dunning for overdue payment", result.Data.Description); Assert.Equal(500.00m, result.Data.Value); Assert.Equal(25.00m, result.Data.FeeValue); Assert.Equal(475.00m, result.Data.NetValue); - Assert.Equal(10.00m, result.Data.ReceivedInCashFeeValue); Assert.True(result.Data.CanBeCancelled); Assert.False(result.Data.IsNecessaryResendDocumentation); } @@ -95,7 +94,8 @@ public async Task Simulate_SendsPostToCorrectUrl() var result = await Manager.Simulate(request); AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/paymentDunnings/simulate"); + AssertRequestUrlContains("/v3/paymentDunnings/simulate"); + AssertRequestUrlContains("payment=pay_1"); } [Fact] @@ -107,7 +107,7 @@ public async Task Simulate_DeserializesResponse() var result = await Manager.Simulate(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("pay_1", result.Data.PaymentId); Assert.Equal(500.00m, result.Data.Value); @@ -133,7 +133,7 @@ public async Task Find_DeserializesResponse() var result = await Manager.Find("dun_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("dun_456", result.Data.Id); Assert.Equal(300.00m, result.Data.Value); @@ -178,7 +178,7 @@ public async Task List_DeserializesResponse() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2, result.Data.Count); Assert.Equal(2, result.TotalCount); @@ -193,7 +193,7 @@ public async Task List_WithoutFilter_DoesNotThrow() var result = await Manager.List(0, 10, null); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); } // ── ListEventHistory ──────────────────────────────────────────── @@ -214,15 +214,15 @@ public async Task ListEventHistory_SendsGetToCorrectUrl() [Fact] public async Task ListEventHistory_DeserializesResponse() { - SetupListResponse("[{\"status\":\"CREATED\",\"description\":\"Dunning created\",\"eventDate\":\"2024-01-10\"},{\"status\":\"SENT\",\"description\":\"Dunning sent\",\"eventDate\":\"2024-01-11\"}]", totalCount: 2); + SetupListResponse("[{\"status\":\"NEGOTIATED\",\"description\":\"Dunning negotiated\",\"eventDate\":\"2024-01-10\"},{\"status\":\"PAID\",\"description\":\"Dunning paid\",\"eventDate\":\"2024-01-11\"}]", totalCount: 2); var result = await Manager.ListEventHistory("dun_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal(2, result.Data.Count); - Assert.Equal("CREATED", result.Data[0].Status); - Assert.Equal("Dunning created", result.Data[0].Description); + Assert.Equal(Codout.Apis.Asaas.Models.PaymentDunning.Enums.PaymentDunningHistoryStatus.NEGOTIATED, result.Data[0].Status); + Assert.Equal("Dunning negotiated", result.Data[0].Description); } // ── ListPartialPaymentsReceived ───────────────────────────────── @@ -247,7 +247,7 @@ public async Task ListPartialPaymentsReceived_DeserializesResponse() var result = await Manager.ListPartialPaymentsReceived("dun_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Single(result.Data); Assert.Equal(150.00m, result.Data[0].Value); @@ -276,7 +276,7 @@ public async Task ListPaymentsAvailableForDunning_DeserializesResponse() var result = await Manager.ListPaymentsAvailableForDunning(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Single(result.Data); Assert.Equal("pay_1", result.Data[0].PaymentId); @@ -317,10 +317,9 @@ public async Task Cancel_DeserializesResponse() var result = await Manager.Cancel("dun_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("dun_123", result.Data.Id); - Assert.Equal(10.00m, result.Data.CancellationFeeValue); } // ── Error handling ────────────────────────────────────────────── @@ -332,7 +331,7 @@ public async Task Find_OnError_ReturnsErrorResponse() var result = await Manager.Find("dun_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -345,7 +344,7 @@ public async Task Cancel_OnError_ReturnsErrorResponse() var result = await Manager.Cancel("dun_invalid"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/PaymentLinkManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PaymentLinkManagerTests.cs index 246d8cc..551e4f2 100644 --- a/Codout.Apis.Asaas.Tests/Managers/PaymentLinkManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/PaymentLinkManagerTests.cs @@ -35,7 +35,7 @@ public async Task Create_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/paymentLinks"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.NotNull(result.Data); Assert.Equal("pl_123", result.Data.Id); Assert.Equal("Test Link", result.Data.Name); @@ -83,7 +83,7 @@ public async Task List_SendsGetRequest() AssertRequestUrlContains("/v3/paymentLinks"); AssertRequestUrlContains("offset=0"); AssertRequestUrlContains("limit=10"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Single(result.Data); Assert.Equal("pl_1", result.Data[0].Id); } @@ -114,7 +114,7 @@ public async Task Find_SendsGetRequestWithId() AssertRequestMethod(HttpMethod.Get); AssertRequestUrl("/v3/paymentLinks/pl_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pl_456", result.Data.Id); Assert.Equal("Found Link", result.Data.Name); Assert.True(result.Data.Active); @@ -139,7 +139,7 @@ public async Task Update_SendsPutRequest() AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/paymentLinks/pl_789"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pl_789", result.Data.Id); Assert.Equal("Updated Link", result.Data.Name); } @@ -174,7 +174,7 @@ public async Task Restore_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/paymentLinks/pl_restored/restore"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.False(result.Data.Deleted); } @@ -191,7 +191,7 @@ public async Task ListImages_SendsGetRequest() AssertRequestMethod(HttpMethod.Get); AssertRequestUrlContains("/v3/paymentLinks/pl_123/images"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Single(result.Data); Assert.Equal("img_1", result.Data[0].Id); Assert.True(result.Data[0].Main); @@ -227,7 +227,7 @@ public async Task FindImage_SendsGetRequest() AssertRequestMethod(HttpMethod.Get); AssertRequestUrl("/v3/paymentLinks/pl_123/images/img_find"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("img_find", result.Data.Id); Assert.False(result.Data.Main); } @@ -245,7 +245,7 @@ public async Task SetMainImage_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/paymentLinks/pl_123/images/img_main/setAsMain"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.True(result.Data.Main); } @@ -260,7 +260,7 @@ public async Task Find_ReturnsErrorOnBadRequest() var result = await Manager.Find("invalid_id"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); Assert.Equal("invalid", result.Errors[0].Code); @@ -275,7 +275,7 @@ public async Task Create_ReturnsErrorOnNotFound() var request = new CreatePaymentLinkRequest { Name = "Test" }; var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); Assert.Single(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/PaymentManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PaymentManagerTests.cs index de16b5b..88e3524 100644 --- a/Codout.Apis.Asaas.Tests/Managers/PaymentManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/PaymentManagerTests.cs @@ -13,6 +13,30 @@ public class PaymentManagerTests : ManagerTestBase protected override PaymentManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) => new TestablePaymentManager(settings, handler); + #region CreateWithCreditCard + + [Fact] + public async Task CreateWithCreditCard_SendsPostToPaymentsRootWithTrailingSlash() + { + SetupOkResponse("{\"id\":\"pay_cc\",\"value\":100.00}"); + var request = new CreatePaymentRequest + { + CustomerId = "cus_1", + BillingType = BillingType.CREDIT_CARD, + Value = 100.00m, + DueDate = new DateTime(2026, 3, 15), + CreditCardToken = "tok_abc" + }; + + var result = await Manager.CreateWithCreditCard(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/payments/"); + Assert.True(result.WasSuccessful()); + } + + #endregion + #region Create [Fact] @@ -46,7 +70,7 @@ public async Task Create_DeserializesResponseCorrectly() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); Assert.Equal("cus_1", result.Data.CustomerId); Assert.Equal(100.00m, result.Data.Value); @@ -60,7 +84,7 @@ public async Task Create_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -87,7 +111,7 @@ public async Task Find_DeserializesResponseCorrectly() var result = await Manager.Find("pay_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_456", result.Data.Id); Assert.Equal(50.00m, result.Data.Value); Assert.False(result.Data.Deleted); @@ -117,7 +141,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("pay_1", result.Data[0].Id); @@ -141,14 +165,14 @@ public async Task List_WithFilter_IncludesFilterParametersInUrl() #region Update [Fact] - public async Task Update_SendsPostToCorrectUrl() + public async Task Update_SendsPutToCorrectUrl() { SetupOkResponse("{\"id\":\"pay_123\",\"value\":150.00}"); var request = new UpdatePaymentRequest { Value = 150.00m }; var result = await Manager.Update("pay_123", request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/payments/pay_123"); } @@ -160,7 +184,7 @@ public async Task Update_DeserializesResponseCorrectly() var result = await Manager.Update("pay_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); Assert.Equal(150.00m, result.Data.Value); Assert.Equal("Updated payment", result.Data.Description); @@ -188,7 +212,7 @@ public async Task Delete_DeserializesResponseCorrectly() var result = await Manager.Delete("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); Assert.True(result.Data.Deleted); } @@ -215,7 +239,7 @@ public async Task Restore_DeserializesResponseCorrectly() var result = await Manager.Restore("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); Assert.False(result.Data.Deleted); } @@ -242,7 +266,7 @@ public async Task Refund_DeserializesResponseCorrectly() var result = await Manager.Refund("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); } @@ -268,7 +292,7 @@ public async Task ReceiveInCash_DeserializesResponseCorrectly() var result = await Manager.ReceiveInCash("pay_123", new DateTime(2026, 3, 15), 100.00m, false); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); } @@ -294,7 +318,7 @@ public async Task GetBankSlipBarCode_DeserializesResponseCorrectly() var result = await Manager.GetBankSlipBarCode("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("12345.67890", result.Data.IdentificationField); Assert.Equal("1234567", result.Data.NossoNumero); Assert.Equal("12345678901234567890123456789012345678901234", result.Data.BarCode); @@ -322,7 +346,7 @@ public async Task GetPixQrCode_DeserializesResponseCorrectly() var result = await Manager.GetPixQrCode("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("base64data", result.Data.EncodedImage); Assert.Equal("pixpayload", result.Data.Payload); } @@ -349,9 +373,234 @@ public async Task UndoReceivedInCash_DeserializesResponseCorrectly() var result = await Manager.UndoReceivedInCash("pay_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pay_123", result.Data.Id); } #endregion + + #region CaptureAuthorizedPayment / PayWithCreditCard + + [Fact] + public async Task CaptureAuthorizedPayment_SendsPostToCaptureRoute() + { + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"CONFIRMED\"}"); + + var result = await Manager.CaptureAuthorizedPayment("pay_1", new CapturePaymentRequest { Value = 50.00m }); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/payments/pay_1/captureAuthorizedPayment"); + } + + [Fact] + public async Task PayWithCreditCard_SendsPostToPayWithCreditCardRoute() + { + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"CONFIRMED\"}"); + + var result = await Manager.PayWithCreditCard("pay_1", new PayWithCreditCardRequest { CreditCardToken = "tok_abc" }); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/payments/pay_1/payWithCreditCard"); + } + + #endregion + + #region BillingInfo / ViewingInfo / Status + + [Fact] + public async Task GetBillingInfo_SendsGetToBillingInfoRoute() + { + SetupOkResponse("{\"creditCard\":{\"creditCardNumber\":\"1234\"},\"pix\":null,\"bankSlip\":null}"); + + var result = await Manager.GetBillingInfo("pay_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/billingInfo"); + Assert.True(result.WasSuccessful()); + Assert.Equal("1234", result.Data.CreditCard.CreditCardNumber); + } + + [Fact] + public async Task GetViewingInfo_SendsGetToViewingInfoRoute() + { + SetupOkResponse("{\"bankSlipViewedDate\":\"2026-05-01\"}"); + + var result = await Manager.GetViewingInfo("pay_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/viewingInfo"); + } + + [Fact] + public async Task GetStatus_SendsGetToStatusRoute() + { + SetupOkResponse("{\"status\":\"CONFIRMED\"}"); + + var result = await Manager.GetStatus("pay_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/status"); + Assert.True(result.WasSuccessful()); + Assert.Equal(Codout.Apis.Asaas.Models.Payment.Enums.PaymentStatus.CONFIRMED, result.Data.Status); + } + + #endregion + + #region Simulate / GetLimits + + [Fact] + public async Task Simulate_SendsPostToSimulateRoute() + { + SetupOkResponse("{\"value\":100,\"creditCard\":{\"netValue\":100,\"feePercentage\":2.49,\"operationFee\":0.49}}"); + var request = new SimulatePaymentRequest { Value = 100m, BillingTypes = [BillingType.BOLETO, BillingType.PIX] }; + + var result = await Manager.Simulate(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/payments/simulate"); + Assert.True(result.WasSuccessful()); + Assert.Equal(100m, result.Data.Value); + Assert.Equal(100m, result.Data.CreditCard.NetValue); + Assert.Equal(2.49m, result.Data.CreditCard.FeePercentage); + } + + [Fact] + public async Task GetLimits_SendsGetToLimitsRoute() + { + SetupOkResponse("{\"creation\":{\"daily\":{\"limit\":10,\"used\":5,\"wasReached\":false}}}"); + + var result = await Manager.GetLimits(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/limits"); + Assert.True(result.WasSuccessful()); + Assert.Equal(10, result.Data.Creation.Daily.Limit); + Assert.Equal(5, result.Data.Creation.Daily.Used); + Assert.False(result.Data.Creation.Daily.WasReached); + } + + #endregion + + #region Refunds / BankSlip refund + + [Fact] + public async Task ListRefunds_SendsGetToRefundsRoute() + { + SetupListResponse("[{\"value\":10.00,\"status\":\"DONE\"}]"); + + var result = await Manager.ListRefunds("pay_1", 0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/payments/pay_1/refunds"); + } + + [Fact] + public async Task RefundBankSlip_SendsPostToBankSlipRefundRoute() + { + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"REFUNDED\"}"); + + var result = await Manager.RefundBankSlip("pay_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/payments/pay_1/bankSlip/refund"); + } + + #endregion + + #region Documents + + [Fact] + public async Task ListDocuments_SendsGetToDocumentsRoute() + { + SetupListResponse("[{\"id\":\"doc_1\",\"name\":\"contrato.pdf\"}]"); + + var result = await Manager.ListDocuments("pay_1", 0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/payments/pay_1/documents"); + } + + [Fact] + public async Task FindDocument_SendsGetToSpecificDocumentRoute() + { + SetupOkResponse("{\"id\":\"doc_1\",\"name\":\"contrato.pdf\"}"); + + var result = await Manager.FindDocument("pay_1", "doc_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/pay_1/documents/doc_1"); + } + + [Fact] + public async Task UpdateDocument_SendsPutToSpecificDocumentRoute() + { + SetupOkResponse("{\"id\":\"doc_1\",\"available\":true}"); + var request = new UpdatePaymentDocumentRequest { Available = true }; + + var result = await Manager.UpdateDocument("pay_1", "doc_1", request); + + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/payments/pay_1/documents/doc_1"); + } + + [Fact] + public async Task DeleteDocument_SendsDeleteToSpecificDocumentRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"doc_1\"}"); + + var result = await Manager.DeleteDocument("pay_1", "doc_1"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/payments/pay_1/documents/doc_1"); + } + + #endregion + + #region Splits Queries + + [Fact] + public async Task ListPaidSplits_SendsGetToPaidSplitsRoute() + { + SetupListResponse("[{\"id\":\"sp_1\",\"walletId\":\"w_1\"}]"); + + var result = await Manager.ListPaidSplits(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/payments/splits/paid"); + } + + [Fact] + public async Task FindPaidSplit_SendsGetToPaidSplitIdRoute() + { + SetupOkResponse("{\"id\":\"sp_1\"}"); + + var result = await Manager.FindPaidSplit("sp_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/splits/paid/sp_1"); + } + + [Fact] + public async Task ListReceivedSplits_SendsGetToReceivedSplitsRoute() + { + SetupListResponse("[{\"id\":\"sp_1\"}]"); + + var result = await Manager.ListReceivedSplits(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/payments/splits/received"); + } + + [Fact] + public async Task FindReceivedSplit_SendsGetToReceivedSplitIdRoute() + { + SetupOkResponse("{\"id\":\"sp_1\"}"); + + var result = await Manager.FindReceivedSplit("sp_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/payments/splits/received/sp_1"); + } + + #endregion } diff --git a/Codout.Apis.Asaas.Tests/Managers/PixAutomaticManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PixAutomaticManagerTests.cs new file mode 100644 index 0000000..2987ddb --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/PixAutomaticManagerTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.PixAutomatic; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class PixAutomaticManagerTests : ManagerTestBase +{ + protected override PixAutomaticManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestablePixAutomaticManager(settings, handler); + + [Fact] + public async Task CreateAuthorization_SendsPostToAuthorizationsRoute() + { + SetupOkResponse("{\"id\":\"auth_1\",\"status\":\"CREATED\",\"customerId\":\"cus_1\",\"frequency\":\"MONTHLY\"}"); + var request = new CreatePixAutomaticAuthorizationRequest + { + Frequency = PixAutomaticRecurringFrequency.MONTHLY, + ContractId = "CONTRACT-123", + StartDate = new DateTime(2026, 1, 1), + CustomerId = "cus_1", + Value = 100m, + ImmediateQrCode = new CreatePixAutomaticImmediateQrCodeRequest + { + ExpirationSeconds = 3600, + OriginalValue = 100m + } + }; + + var result = await Manager.CreateAuthorization(request); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/pix/automatic/authorizations"); + Assert.Equal(PixAutomaticAuthorizationStatus.CREATED, result.Data.Status); + Assert.Equal(PixAutomaticRecurringFrequency.MONTHLY, result.Data.Frequency); + } + + [Fact] + public async Task CreateAuthorization_SerializesImmediateQrCodeAndFrequency() + { + SetupOkResponse("{\"id\":\"auth_1\"}"); + var request = new CreatePixAutomaticAuthorizationRequest + { + Frequency = PixAutomaticRecurringFrequency.QUARTERLY, + ContractId = "C-9", + StartDate = new DateTime(2026, 2, 1), + CustomerId = "cus_9", + ImmediateQrCode = new CreatePixAutomaticImmediateQrCodeRequest { ExpirationSeconds = 600, OriginalValue = 50m } + }; + + await Manager.CreateAuthorization(request); + + Assert.NotNull(Handler.LastRequestContent); + Assert.Contains("\"frequency\":\"QUARTERLY\"", Handler.LastRequestContent); + Assert.Contains("\"immediateQrCode\":{", Handler.LastRequestContent); + Assert.Contains("\"expirationSeconds\":600", Handler.LastRequestContent); + } + + [Fact] + public async Task ListAuthorizations_SendsGetToAuthorizationsRoute() + { + SetupListResponse("[]"); + + var result = await Manager.ListAuthorizations(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/pix/automatic/authorizations"); + } + + [Fact] + public async Task FindAuthorization_SendsGetToAuthorizationIdRoute() + { + SetupOkResponse("{\"id\":\"auth_1\"}"); + + var result = await Manager.FindAuthorization("auth_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/pix/automatic/authorizations/auth_1"); + } + + [Fact] + public async Task CancelAuthorization_SendsDeleteToAuthorizationIdRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"auth_1\"}"); + + var result = await Manager.CancelAuthorization("auth_1"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/pix/automatic/authorizations/auth_1"); + } + + [Fact] + public async Task FindPaymentInstruction_SendsGetToPaymentInstructionIdRoute() + { + SetupOkResponse("{\"id\":\"pi_1\"}"); + + var result = await Manager.FindPaymentInstruction("pi_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/pix/automatic/paymentInstructions/pi_1"); + } + + [Fact] + public async Task ListPaymentInstructions_SendsGetToPaymentInstructionsRoute() + { + SetupListResponse("[]"); + + var result = await Manager.ListPaymentInstructions(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/pix/automatic/paymentInstructions"); + } + + [Fact] + public async Task CreateAuthorization_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(System.Net.HttpStatusCode.BadRequest); + var request = new CreatePixAutomaticAuthorizationRequest + { + Frequency = PixAutomaticRecurringFrequency.MONTHLY, + ContractId = "C-1", + StartDate = new DateTime(2026, 1, 1), + CustomerId = "cus_x" + }; + + var result = await Manager.CreateAuthorization(request); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } + + [Fact] + public async Task FindAuthorization_OnNotFound_ReturnsError() + { + SetupErrorResponse(System.Net.HttpStatusCode.NotFound); + + var result = await Manager.FindAuthorization("auth_unknown"); + + Assert.False(result.WasSuccessful()); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/PixManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PixManagerTests.cs index f1943c3..07c95c8 100644 --- a/Codout.Apis.Asaas.Tests/Managers/PixManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/PixManagerTests.cs @@ -26,7 +26,7 @@ public async Task ListTransactions_SendsGetRequest() AssertRequestUrlContains("/v3/pix/transactions"); AssertRequestUrlContains("offset=0"); AssertRequestUrlContains("limit=10"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Single(result.Data); Assert.Equal("pix_tx_1", result.Data[0].Id); Assert.Equal("pay_123", result.Data[0].Payment); @@ -61,7 +61,7 @@ public async Task CancelTransaction_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/pix/transactions/pix_tx_cancel/cancel"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pix_tx_cancel", result.Data.Id); Assert.Equal(PixTransactionStatus.CANCELLED, result.Data.Status); } @@ -86,7 +86,7 @@ public async Task CreateStaticQrCode_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/pix/qrCodes/static"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("qr_123", result.Data.Id); Assert.Equal("base64data", result.Data.EncodedImage); Assert.Equal("00020126...", result.Data.Payload); @@ -131,7 +131,7 @@ public async Task DecodeQrCode_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/pix/qrCodes/decode"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("00020126...", result.Data.Payload); Assert.Equal("STATIC", result.Data.Type); Assert.Equal("E123", result.Data.EndToEndIdentifier); @@ -159,7 +159,7 @@ public async Task PayQrCode_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/pix/qrCodes/pay"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("pix_pay_123", result.Data.Id); Assert.Equal(75.00m, result.Data.Value); Assert.Equal("QR payment", result.Data.Description); @@ -203,11 +203,11 @@ public async Task CreateAddressKey_SendsPostRequest() AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/pix/addressKeys"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("key_123", result.Data.Id); Assert.Equal("12345678901", result.Data.Key); Assert.Equal(PixAddressKeyType.CPF, result.Data.Type); - Assert.Equal("ACTIVE", result.Data.Status); + Assert.Equal(Codout.Apis.Asaas.Models.Pix.Enums.PixAddressKeyStatus.ACTIVE, result.Data.Status); } [Fact] @@ -254,7 +254,7 @@ public async Task ListAddressKeys_SendsGetRequest() AssertRequestUrlContains("/v3/pix/addressKeys"); AssertRequestUrlContains("offset=0"); AssertRequestUrlContains("limit=10"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Single(result.Data); Assert.Equal("key_1", result.Data[0].Id); Assert.Equal(PixAddressKeyType.EMAIL, result.Data[0].Type); @@ -273,7 +273,7 @@ public async Task FindAddressKey_SendsGetRequest() AssertRequestMethod(HttpMethod.Get); AssertRequestUrl("/v3/pix/addressKeys/key_find"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("key_find", result.Data.Id); } @@ -305,7 +305,7 @@ public async Task CancelTransaction_ReturnsErrorOnBadRequest() var result = await Manager.CancelTransaction("invalid_tx"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -321,4 +321,42 @@ public async Task FindAddressKey_ReturnsNullDataOnError() } #endregion + + #region FindTransaction / DeleteStaticQrCode / GetAddressKeyTokenBucket + + [Fact] + public async Task FindTransaction_SendsGetToTransactionRoute() + { + SetupOkResponse("{\"id\":\"pix_tx_42\",\"status\":\"DONE\"}"); + + var result = await Manager.FindTransaction("pix_tx_42"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/pix/transactions/pix_tx_42"); + } + + [Fact] + public async Task DeleteStaticQrCode_SendsDeleteToStaticQrCodeRoute() + { + SetupOkResponse("{\"deleted\":true,\"id\":\"qr_1\"}"); + + var result = await Manager.DeleteStaticQrCode("qr_1"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/pix/qrCodes/static/qr_1"); + } + + [Fact] + public async Task GetAddressKeyTokenBucket_SendsGetToTokenBucketRoute() + { + SetupOkResponse("{\"remainingTokens\":5,\"maxTokens\":10}"); + + var result = await Manager.GetAddressKeyTokenBucket(); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/pix/tokenBucket/addressKey"); + Assert.Equal(5, result.Data.RemainingTokens); + } + + #endregion } diff --git a/Codout.Apis.Asaas.Tests/Managers/PixRecurringManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/PixRecurringManagerTests.cs new file mode 100644 index 0000000..331f94c --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/PixRecurringManagerTests.cs @@ -0,0 +1,94 @@ +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Models.PixRecurring; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class PixRecurringManagerTests : ManagerTestBase +{ + protected override PixRecurringManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestablePixRecurringManager(settings, handler); + + [Fact] + public async Task List_SendsGetToRecurringsRoute() + { + SetupListResponse("[]"); + + var result = await Manager.List(0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/pix/transactions/recurrings"); + } + + [Fact] + public async Task Find_SendsGetToRecurringIdRoute() + { + SetupOkResponse("{\"id\":\"rec_1\"}"); + + var result = await Manager.Find("rec_1"); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/pix/transactions/recurrings/rec_1"); + } + + [Fact] + public async Task Cancel_SendsPostToCancelRoute() + { + SetupOkResponse("{\"id\":\"rec_1\",\"status\":\"CANCELLED\"}"); + + var result = await Manager.Cancel("rec_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/pix/transactions/recurrings/rec_1/cancel"); + } + + [Fact] + public async Task ListItems_SendsGetToItemsRouteAndParsesEnvelope() + { + // API retorna { data: [...] }, nao o envelope padrao de lista + SetupOkResponse("{\"data\":[{\"id\":\"item_1\",\"status\":\"PENDING\",\"value\":0.02,\"recurrenceNumber\":1,\"quantity\":2,\"canBeCancelled\":true}]}"); + + var result = await Manager.ListItems("rec_1", 0, 10); + + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrlContains("/v3/pix/transactions/recurrings/rec_1/items"); + Assert.True(result.WasSuccessful()); + Assert.Single(result.Data.Data); + Assert.Equal("item_1", result.Data.Data[0].Id); + Assert.True(result.Data.Data[0].CanBeCancelled); + } + + [Fact] + public async Task CancelItem_SendsPostToItemCancelRoute() + { + SetupOkResponse("{\"id\":\"item_1\",\"status\":\"CANCELLED\"}"); + + var result = await Manager.CancelItem("item_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/pix/transactions/recurrings/items/item_1/cancel"); + } + + [Fact] + public async Task Find_OnNotFound_ReturnsError() + { + SetupErrorResponse(System.Net.HttpStatusCode.NotFound); + + var result = await Manager.Find("rec_unknown"); + + Assert.False(result.WasSuccessful()); + Assert.NotEmpty(result.Errors); + } + + [Fact] + public async Task Cancel_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(System.Net.HttpStatusCode.BadRequest); + + var result = await Manager.Cancel("rec_1"); + + Assert.False(result.WasSuccessful()); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/SandboxManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/SandboxManagerTests.cs new file mode 100644 index 0000000..dda96ba --- /dev/null +++ b/Codout.Apis.Asaas.Tests/Managers/SandboxManagerTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Net.Http; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Managers; +using Codout.Apis.Asaas.Tests.Helpers; + +namespace Codout.Apis.Asaas.Tests.Managers; + +public class SandboxManagerTests : ManagerTestBase +{ + protected override SandboxManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) + => new TestableSandboxManager(settings, handler); + + [Fact] + public async Task ApproveAccount_SendsPostToApproveRoute() + { + SetupOkResponse("{}"); + + var result = await Manager.ApproveAccount(); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/sandbox/myAccount/approve"); + } + + [Fact] + public async Task ConfirmPayment_SendsPostToConfirmRoute() + { + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"CONFIRMED\"}"); + + var result = await Manager.ConfirmPayment("pay_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/sandbox/payment/pay_1/confirm"); + } + + [Fact] + public async Task ForceOverdue_SendsPostToOverdueRoute() + { + SetupOkResponse("{\"id\":\"pay_1\",\"status\":\"OVERDUE\"}"); + + var result = await Manager.ForceOverdue("pay_1"); + + AssertRequestMethod(HttpMethod.Post); + AssertRequestUrl("/v3/sandbox/payment/pay_1/overdue"); + } + + [Fact] + public async Task ApproveAccount_InProduction_ThrowsInvalidOperationException() + { + var prodSettings = new ApiSettings("token", "TestApp", AsaasEnvironment.PRODUCTION); + var prodManager = new TestableSandboxManager(prodSettings, Handler); + + await Assert.ThrowsAsync(() => prodManager.ApproveAccount()); + } + + [Fact] + public async Task ConfirmPayment_InProduction_ThrowsInvalidOperationException() + { + var prodSettings = new ApiSettings("token", "TestApp", AsaasEnvironment.PRODUCTION); + var prodManager = new TestableSandboxManager(prodSettings, Handler); + + await Assert.ThrowsAsync(() => prodManager.ConfirmPayment("pay_1")); + } + + [Fact] + public async Task ForceOverdue_InProduction_ThrowsInvalidOperationException() + { + var prodSettings = new ApiSettings("token", "TestApp", AsaasEnvironment.PRODUCTION); + var prodManager = new TestableSandboxManager(prodSettings, Handler); + + await Assert.ThrowsAsync(() => prodManager.ForceOverdue("pay_1")); + } +} diff --git a/Codout.Apis.Asaas.Tests/Managers/SubscriptionManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/SubscriptionManagerTests.cs index c1fd22f..3dde59d 100644 --- a/Codout.Apis.Asaas.Tests/Managers/SubscriptionManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/SubscriptionManagerTests.cs @@ -49,7 +49,7 @@ public async Task Create_DeserializesResponseCorrectly() var result = await Manager.Create(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("sub_123", result.Data.Id); Assert.Equal("cus_1", result.Data.CustomerId); Assert.Equal(99.90m, result.Data.Value); @@ -64,7 +64,7 @@ public async Task Create_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.NotEmpty(result.Errors); } @@ -91,7 +91,7 @@ public async Task Find_DeserializesResponseCorrectly() var result = await Manager.Find("sub_456"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("sub_456", result.Data.Id); Assert.Equal("cus_1", result.Data.CustomerId); Assert.Equal(49.90m, result.Data.Value); @@ -122,7 +122,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("sub_1", result.Data[0].Id); @@ -138,7 +138,7 @@ public async Task List_WithFilter_IncludesFilterParametersInUrl() var result = await Manager.List(0, 10, filter); AssertRequestUrlContains("customer=cus_1"); - AssertRequestUrlContains("includeDeleted=True"); + AssertRequestUrlContains("includeDeleted=true"); } #endregion @@ -146,14 +146,14 @@ public async Task List_WithFilter_IncludesFilterParametersInUrl() #region Update [Fact] - public async Task Update_SendsPostToCorrectUrl() + public async Task Update_SendsPutToCorrectUrl() { SetupOkResponse("{\"id\":\"sub_123\",\"value\":129.90}"); var request = new UpdateSubscriptionRequest { Value = 129.90m }; var result = await Manager.Update("sub_123", request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/subscriptions/sub_123"); } @@ -165,7 +165,7 @@ public async Task Update_DeserializesResponseCorrectly() var result = await Manager.Update("sub_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("sub_123", result.Data.Id); Assert.Equal(129.90m, result.Data.Value); Assert.Equal("Updated plan", result.Data.Description); @@ -173,6 +173,33 @@ public async Task Update_DeserializesResponseCorrectly() #endregion + #region UpdateCreditCard + + [Fact] + public async Task UpdateCreditCard_SendsPutToCreditCardRoute() + { + SetupOkResponse("{\"id\":\"sub_123\",\"value\":129.90}"); + var request = new UpdateSubscriptionCreditCardRequest { CreditCardToken = "tok_abc" }; + + var result = await Manager.UpdateCreditCard("sub_123", request); + + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/subscriptions/sub_123/creditCard"); + } + + [Fact] + public async Task UpdateCreditCard_OnError_ReturnsErrorResponse() + { + SetupErrorResponse(HttpStatusCode.BadRequest); + var request = new UpdateSubscriptionCreditCardRequest { CreditCardToken = "tok_bad" }; + + var result = await Manager.UpdateCreditCard("sub_123", request); + + Assert.False(result.WasSuccessful()); + } + + #endregion + #region Delete [Fact] @@ -193,7 +220,7 @@ public async Task Delete_DeserializesResponseCorrectly() var result = await Manager.Delete("sub_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("sub_123", result.Data.Id); Assert.True(result.Data.Deleted); } @@ -222,7 +249,7 @@ public async Task ListPayments_DeserializesResponseCorrectly() var result = await Manager.ListPayments("sub_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(2, result.TotalCount); Assert.Equal(2, result.Data.Count); Assert.Equal("pay_1", result.Data[0].Id); @@ -252,7 +279,7 @@ public async Task ListPaymentBook_DeserializesResponseCorrectly() var result = await Manager.ListPaymentBook("sub_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(3, result.TotalCount); Assert.Equal(3, result.Data.Count); } @@ -281,7 +308,7 @@ public async Task ListInvoice_DeserializesResponseCorrectly() var result = await Manager.ListInvoice("sub_123", 0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(1, result.TotalCount); Assert.Single(result.Data); Assert.Equal("inv_1", result.Data[0].Id); @@ -332,7 +359,7 @@ public async Task CreateInvoiceSettings_DeserializesResponseCorrectly() var result = await Manager.CreateInvoiceSettings("sub_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("svc_1", result.Data.MunicipalServiceId); Assert.Equal("1234", result.Data.MunicipalServiceCode); Assert.Equal("Service", result.Data.MunicipalServiceName); @@ -345,14 +372,14 @@ public async Task CreateInvoiceSettings_DeserializesResponseCorrectly() #region UpdateInvoiceSettings [Fact] - public async Task UpdateInvoiceSettings_SendsPostToCorrectUrl() + public async Task UpdateInvoiceSettings_SendsPutToCorrectUrl() { SetupOkResponse("{\"municipalServiceId\":\"svc_1\",\"daysBeforeDueDate\":10,\"receivedOnly\":true}"); var request = new UpdateInvoiceSettingsRequest { DaysBeforeDueDate = 10, ReceivedOnly = true }; var result = await Manager.UpdateInvoiceSettings("sub_123", request); - AssertRequestMethod(HttpMethod.Post); + AssertRequestMethod(HttpMethod.Put); AssertRequestUrl("/v3/subscriptions/sub_123/invoiceSettings"); } @@ -364,7 +391,7 @@ public async Task UpdateInvoiceSettings_DeserializesResponseCorrectly() var result = await Manager.UpdateInvoiceSettings("sub_123", request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(10, result.Data.DaysBeforeDueDate); Assert.True(result.Data.ReceivedOnly); } @@ -391,7 +418,7 @@ public async Task FindInvoiceSettings_DeserializesResponseCorrectly() var result = await Manager.FindInvoiceSettings("sub_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("svc_1", result.Data.MunicipalServiceId); Assert.Equal(5, result.Data.DaysBeforeDueDate); } @@ -418,7 +445,7 @@ public async Task DeleteInvoiceSettings_DeserializesResponseCorrectly() var result = await Manager.DeleteInvoiceSettings("sub_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("sub_123", result.Data.Id); Assert.True(result.Data.Deleted); } diff --git a/Codout.Apis.Asaas.Tests/Managers/TransferManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/TransferManagerTests.cs index f0b9be7..cff9995 100644 --- a/Codout.Apis.Asaas.Tests/Managers/TransferManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/TransferManagerTests.cs @@ -36,7 +36,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(0, result.TotalCount); Assert.Empty(result.Data); } @@ -73,17 +73,17 @@ public async Task List_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.List(0, 10); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode); Assert.NotEmpty(result.Errors); } #endregion - #region Execute (AsaasAccountTransfer) + #region TransferToAsaasAccount [Fact] - public async Task ExecuteAsaasAccountTransfer_SendsPostToCorrectUrl() + public async Task TransferToAsaasAccount_SendsPostToTransfersRootWithTrailingSlash() { SetupOkResponse("{\"id\":\"trans_123\",\"walletId\":\"wal_456\",\"value\":500.00,\"type\":\"ASAAS_ACCOUNT\",\"status\":\"PENDING\"}"); var request = new AsaasAccountTransferRequest @@ -92,14 +92,14 @@ public async Task ExecuteAsaasAccountTransfer_SendsPostToCorrectUrl() Value = 500.00m }; - var result = await Manager.Execute(request); + var result = await Manager.TransferToAsaasAccount(request); AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/transfers"); + AssertRequestUrl("/v3/transfers/"); } [Fact] - public async Task ExecuteAsaasAccountTransfer_DeserializesResponseCorrectly() + public async Task TransferToAsaasAccount_DeserializesResponseCorrectly() { SetupOkResponse("{\"id\":\"trans_123\",\"walletId\":\"wal_456\",\"value\":500.00,\"type\":\"ASAAS_ACCOUNT\",\"status\":\"PENDING\",\"authorized\":true}"); var request = new AsaasAccountTransferRequest @@ -108,9 +108,9 @@ public async Task ExecuteAsaasAccountTransfer_DeserializesResponseCorrectly() Value = 500.00m }; - var result = await Manager.Execute(request); + var result = await Manager.TransferToAsaasAccount(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("trans_123", result.Data.Id); Assert.Equal("wal_456", result.Data.WalletId); Assert.Equal(500.00m, result.Data.Value); @@ -118,10 +118,10 @@ public async Task ExecuteAsaasAccountTransfer_DeserializesResponseCorrectly() #endregion - #region Execute (BankAccountTransfer) + #region TransferToBankAccount [Fact] - public async Task ExecuteBankAccountTransfer_SendsPostToCorrectUrl() + public async Task TransferToBankAccount_SendsPostToTransfersRoot() { SetupOkResponse("{\"id\":\"trans_789\",\"value\":1000.00,\"type\":\"BANK_ACCOUNT\",\"status\":\"PENDING\"}"); var request = new BankAccountTransferRequest @@ -130,14 +130,14 @@ public async Task ExecuteBankAccountTransfer_SendsPostToCorrectUrl() BankAccount = new BankAccount() }; - var result = await Manager.Execute(request); + var result = await Manager.TransferToBankAccount(request); AssertRequestMethod(HttpMethod.Post); AssertRequestUrl("/v3/transfers"); } [Fact] - public async Task ExecuteBankAccountTransfer_DeserializesResponseCorrectly() + public async Task TransferToBankAccount_DeserializesResponseCorrectly() { SetupOkResponse("{\"id\":\"trans_789\",\"value\":1000.00,\"netValue\":995.00,\"type\":\"BANK_ACCOUNT\",\"status\":\"PENDING\"}"); var request = new BankAccountTransferRequest @@ -146,15 +146,30 @@ public async Task ExecuteBankAccountTransfer_DeserializesResponseCorrectly() BankAccount = new BankAccount() }; - var result = await Manager.Execute(request); + var result = await Manager.TransferToBankAccount(request); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("trans_789", result.Data.Id); Assert.Equal(1000.00m, result.Data.Value); } #endregion + #region Cancel + + [Fact] + public async Task Cancel_SendsDeleteToCancelRoute() + { + SetupErrorResponse(HttpStatusCode.NotFound); + + var result = await Manager.Cancel("trans_123"); + + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/transfers/trans_123/cancel"); + } + + #endregion + #region Find [Fact] @@ -175,7 +190,7 @@ public async Task Find_DeserializesResponseCorrectly() var result = await Manager.Find("trans_123"); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal("trans_123", result.Data.Id); Assert.Equal(1000.00m, result.Data.Value); Assert.Equal(TransferType.BANK_ACCOUNT, result.Data.Type); @@ -190,7 +205,7 @@ public async Task Find_WhenNotFound_ReturnsErrorResponse() var result = await Manager.Find("trans_nonexistent"); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); } diff --git a/Codout.Apis.Asaas.Tests/Managers/WalletManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/WalletManagerTests.cs index a437a78..9dd1512 100644 --- a/Codout.Apis.Asaas.Tests/Managers/WalletManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/WalletManagerTests.cs @@ -34,7 +34,7 @@ public async Task List_DeserializesListResponseCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(3, result.TotalCount); Assert.Equal(3, result.Data.Count); Assert.Equal("wal_1", result.Data[0].Id); @@ -60,7 +60,7 @@ public async Task List_ReturnsEmptyListWhenNoWallets() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.Equal(0, result.TotalCount); Assert.Empty(result.Data); } @@ -72,7 +72,7 @@ public async Task List_HasMoreFlagIsParsedCorrectly() var result = await Manager.List(0, 10); - Assert.True(result.WasSucessfull()); + Assert.True(result.WasSuccessful()); Assert.True(result.HasMore); Assert.Equal(50, result.TotalCount); Assert.Equal(10, result.Limit); @@ -86,7 +86,7 @@ public async Task List_WhenApiReturnsError_ReturnsErrorResponse() var result = await Manager.List(0, 10); - Assert.False(result.WasSucessfull()); + Assert.False(result.WasSuccessful()); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); Assert.NotEmpty(result.Errors); } diff --git a/Codout.Apis.Asaas.Tests/Managers/WebhookManagerTests.cs b/Codout.Apis.Asaas.Tests/Managers/WebhookManagerTests.cs index 26399a5..9b23421 100644 --- a/Codout.Apis.Asaas.Tests/Managers/WebhookManagerTests.cs +++ b/Codout.Apis.Asaas.Tests/Managers/WebhookManagerTests.cs @@ -3,6 +3,7 @@ using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Managers; using Codout.Apis.Asaas.Models.Webhook; +using Codout.Apis.Asaas.Models.Webhook.Enums; using Codout.Apis.Asaas.Tests.Helpers; namespace Codout.Apis.Asaas.Tests.Managers; @@ -12,179 +13,185 @@ public class WebhookManagerTests : ManagerTestBase protected override WebhookManager CreateManager(ApiSettings settings, MockHttpMessageHandler handler) => new TestableWebhookManager(settings, handler); - // ── FindPaymentWebhook ────────────────────────────────────────── + #region Create [Fact] - public async Task FindPaymentWebhook_SendsGetRequest() + public async Task Create_SendsPostToWebhooksRoute() { - SetupOkResponse("{\"url\":\"https://example.com\",\"enabled\":true}"); - - var result = await Manager.FindPaymentWebhook(); - - AssertRequestMethod(HttpMethod.Get); - AssertRequestUrl("/v3/webhook"); - } - - [Fact] - public async Task FindPaymentWebhook_DeserializesResponse() - { - SetupOkResponse("{\"url\":\"https://example.com\",\"email\":\"test@test.com\",\"apiVersion\":3,\"enabled\":true,\"interrupted\":false,\"authToken\":\"tok123\"}"); - - var result = await Manager.FindPaymentWebhook(); - - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal("https://example.com", result.Data.Url); - Assert.Equal("test@test.com", result.Data.Email); - Assert.Equal(3, result.Data.ApiVersion); - Assert.True(result.Data.Enabled); - Assert.False(result.Data.Interrupted); - Assert.Equal("tok123", result.Data.AuthToken); - } - - // ── CreateOrUpdatePaymentWebhook ──────────────────────────────── - - [Fact] - public async Task CreateOrUpdatePaymentWebhook_SendsPostToCorrectUrl() - { - SetupOkResponse("{\"url\":\"https://example.com\",\"enabled\":true}"); - - var request = new WebhookRequest + SetupOkResponse("{\"id\":\"wh_new\",\"name\":\"My webhook\",\"url\":\"https://example.com\",\"enabled\":true}"); + var request = new CreateWebhookRequest { + Name = "My webhook", Url = "https://example.com", - Email = "test@test.com", + Email = "ops@example.com", + Enabled = true, + Interrupted = false, ApiVersion = 3, - Enabled = true + AuthToken = "whsec_Pxeh17yy3LQbLVpnzz6I1chB7mtzYk5F7pg8bRR80pE", + SendType = WebhookSendType.SEQUENTIALLY, + Events = [WebhookEvent.PAYMENT_CONFIRMED, WebhookEvent.PAYMENT_RECEIVED] }; - var result = await Manager.CreateOrUpdatePaymentWebhook(request); + var result = await Manager.Create(request); AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/webhook"); + AssertRequestUrl("/v3/webhooks"); + Assert.True(result.WasSuccessful()); + Assert.Equal("wh_new", result.Data.Id); } [Fact] - public async Task CreateOrUpdatePaymentWebhook_DeserializesResponse() + public async Task Create_SerializesAllFields() { - SetupOkResponse("{\"url\":\"https://example.com\",\"enabled\":true,\"apiVersion\":3}"); - - var request = new WebhookRequest { Url = "https://example.com", Enabled = true, ApiVersion = 3 }; + SetupOkResponse("{\"id\":\"wh_new\"}"); + var request = new CreateWebhookRequest + { + Name = "Webhook A", + Url = "https://example.com/hook", + Email = "ops@example.com", + Enabled = true, + Interrupted = false, + ApiVersion = 3, + AuthToken = "whsec_abcdefghijklmnopqrstuvwxyz123456", + SendType = WebhookSendType.NON_SEQUENTIALLY, + Events = [WebhookEvent.PAYMENT_CREATED, WebhookEvent.PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CREATED] + }; - var result = await Manager.CreateOrUpdatePaymentWebhook(request); + await Manager.Create(request); - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal("https://example.com", result.Data.Url); - Assert.True(result.Data.Enabled); + Assert.NotNull(Handler.LastRequestContent); + Assert.Contains("\"name\":\"Webhook A\"", Handler.LastRequestContent); + Assert.Contains("\"url\":\"https://example.com/hook\"", Handler.LastRequestContent); + Assert.Contains("\"sendType\":\"NON_SEQUENTIALLY\"", Handler.LastRequestContent); + Assert.Contains("\"PAYMENT_CREATED\"", Handler.LastRequestContent); + Assert.Contains("\"PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CREATED\"", Handler.LastRequestContent); } - // ── FindInvoiceWebhook ────────────────────────────────────────── + #endregion + + #region List [Fact] - public async Task FindInvoiceWebhook_SendsGetToCorrectUrl() + public async Task List_SendsGetToWebhooksRoute() { - SetupOkResponse("{\"url\":\"https://invoice.example.com\",\"enabled\":true}"); + SetupListResponse("[{\"id\":\"wh_1\",\"name\":\"A\"}]"); - var result = await Manager.FindInvoiceWebhook(); + var result = await Manager.List(0, 10); AssertRequestMethod(HttpMethod.Get); - AssertRequestUrl("/v3/webhook/invoice"); + AssertRequestUrlContains("/v3/webhooks"); + AssertRequestUrlContains("offset=0"); + AssertRequestUrlContains("limit=10"); } [Fact] - public async Task FindInvoiceWebhook_DeserializesResponse() + public async Task List_WithFilter_IncludesFilterParameters() { - SetupOkResponse("{\"url\":\"https://invoice.example.com\",\"enabled\":false}"); + SetupListResponse("[]"); + var filter = new WebhookListFilter { Name = "Payments", Enabled = true }; - var result = await Manager.FindInvoiceWebhook(); + await Manager.List(0, 10, filter); - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal("https://invoice.example.com", result.Data.Url); - Assert.False(result.Data.Enabled); + AssertRequestUrlContains("name=Payments"); + AssertRequestUrlContains("enabled=true"); } - // ── CreateOrUpdateInvoiceWebhook ──────────────────────────────── + #endregion + + #region Find [Fact] - public async Task CreateOrUpdateInvoiceWebhook_SendsPostToCorrectUrl() + public async Task Find_SendsGetToWebhooksId() { - SetupOkResponse("{\"url\":\"https://invoice.example.com\",\"enabled\":true}"); - - var request = new WebhookRequest { Url = "https://invoice.example.com", Enabled = true }; + SetupOkResponse("{\"id\":\"wh_42\",\"name\":\"Found\",\"url\":\"https://example.com\",\"events\":[\"PAYMENT_RECEIVED\"]}"); - var result = await Manager.CreateOrUpdateInvoiceWebhook(request); + var result = await Manager.Find("wh_42"); - AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/webhook/invoice"); + AssertRequestMethod(HttpMethod.Get); + AssertRequestUrl("/v3/webhooks/wh_42"); + Assert.True(result.WasSuccessful()); + Assert.Equal("wh_42", result.Data.Id); + Assert.Equal("Found", result.Data.Name); + Assert.Single(result.Data.Events); + Assert.Equal(WebhookEvent.PAYMENT_RECEIVED, result.Data.Events[0]); } - // ── FindMobilePhoneRechargeWebhook ────────────────────────────── + #endregion + + #region Update [Fact] - public async Task FindMobilePhoneRechargeWebhook_SendsGetToCorrectUrl() + public async Task Update_SendsPutToWebhooksId() { - SetupOkResponse("{\"url\":\"https://mobile.example.com\",\"enabled\":true}"); + SetupOkResponse("{\"id\":\"wh_42\",\"enabled\":false}"); + var request = new UpdateWebhookRequest { Enabled = false }; - var result = await Manager.FindMobilePhoneRechargeWebhook(); + var result = await Manager.Update("wh_42", request); - AssertRequestMethod(HttpMethod.Get); - AssertRequestUrl("/v3/webhook/mobilePhoneRecharge"); + AssertRequestMethod(HttpMethod.Put); + AssertRequestUrl("/v3/webhooks/wh_42"); + Assert.True(result.WasSuccessful()); + Assert.False(result.Data.Enabled); } + #endregion + + #region Delete + [Fact] - public async Task FindMobilePhoneRechargeWebhook_DeserializesResponse() + public async Task Delete_SendsDeleteToWebhooksId() { - SetupOkResponse("{\"url\":\"https://mobile.example.com\",\"enabled\":true,\"apiVersion\":3}"); + SetupOkResponse("{\"id\":\"wh_42\",\"deleted\":true}"); - var result = await Manager.FindMobilePhoneRechargeWebhook(); + var result = await Manager.Delete("wh_42"); - Assert.True(result.WasSucessfull()); - Assert.NotNull(result.Data); - Assert.Equal("https://mobile.example.com", result.Data.Url); + AssertRequestMethod(HttpMethod.Delete); + AssertRequestUrl("/v3/webhooks/wh_42"); + Assert.True(result.WasSuccessful()); + Assert.True(result.Data.Deleted); } - // ── CreateOrUpdateMobilePhoneRechargeWebhook ──────────────────── + #endregion + + #region RemoveBackoff [Fact] - public async Task CreateOrUpdateMobilePhoneRechargeWebhook_SendsPostToCorrectUrl() + public async Task RemoveBackoff_SendsPostToRemoveBackoffRoute() { - SetupOkResponse("{\"url\":\"https://mobile.example.com\",\"enabled\":true}"); + SetupOkResponse("{\"id\":\"wh_42\",\"interrupted\":false}"); - var request = new WebhookRequest { Url = "https://mobile.example.com", Enabled = true }; - - var result = await Manager.CreateOrUpdateMobilePhoneRechargeWebhook(request); + var result = await Manager.RemoveBackoff("wh_42"); AssertRequestMethod(HttpMethod.Post); - AssertRequestUrl("/v3/webhook/mobilePhoneRecharge"); + AssertRequestUrl("/v3/webhooks/wh_42/removeBackoff"); + Assert.True(result.WasSuccessful()); } - // ── Error handling ────────────────────────────────────────────── + #endregion + + #region Error Handling [Fact] - public async Task FindPaymentWebhook_OnError_ReturnsErrorResponse() + public async Task Create_OnError_ReturnsErrorResponse() { SetupErrorResponse(HttpStatusCode.BadRequest); + var request = new CreateWebhookRequest { Url = "https://example.com" }; - var result = await Manager.FindPaymentWebhook(); + var result = await Manager.Create(request); - Assert.False(result.WasSucessfull()); - Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + Assert.False(result.WasSuccessful()); Assert.NotEmpty(result.Errors); - Assert.Equal("invalid", result.Errors[0].Code); - Assert.Equal("Test error", result.Errors[0].Description); } [Fact] - public async Task CreateOrUpdatePaymentWebhook_OnError_ReturnsErrorResponse() + public async Task Find_OnNotFound_ReturnsError() { - SetupErrorResponse(HttpStatusCode.InternalServerError); + SetupErrorResponse(HttpStatusCode.NotFound); - var request = new WebhookRequest { Url = "https://example.com" }; - var result = await Manager.CreateOrUpdatePaymentWebhook(request); + var result = await Manager.Find("wh_unknown"); - Assert.False(result.WasSucessfull()); - Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); - Assert.NotEmpty(result.Errors); + Assert.False(result.WasSuccessful()); } + + #endregion } diff --git a/Codout.Apis.Asaas.Tests/Models/SerializationTests.cs b/Codout.Apis.Asaas.Tests/Serialization/SerializationTests.cs similarity index 91% rename from Codout.Apis.Asaas.Tests/Models/SerializationTests.cs rename to Codout.Apis.Asaas.Tests/Serialization/SerializationTests.cs index 91fdcbb..98b4cf7 100644 --- a/Codout.Apis.Asaas.Tests/Models/SerializationTests.cs +++ b/Codout.Apis.Asaas.Tests/Serialization/SerializationTests.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using System.Text.Json; -using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Models.PaymentLink; using Codout.Apis.Asaas.Models.PaymentLink.Enums; using Codout.Apis.Asaas.Models.Common; using Codout.Apis.Asaas.Models.Common.Enums; using Codout.Apis.Asaas.Models.Notification; using Codout.Apis.Asaas.Models.CreditBureauReport; -using Codout.Apis.Asaas.Models.CustomerFiscalInfo; +using Codout.Apis.Asaas.Models.FiscalInfo; using Codout.Apis.Asaas.Models.Pix; using Codout.Apis.Asaas.Models.Pix.Enums; @@ -16,20 +16,9 @@ namespace Codout.Apis.Asaas.Tests.Serialization; public class SerializationTests { - // Replicate the SDK's JSON configuration for testing - private static readonly JsonSerializerOptions Options = CreateOptions(); - - private static JsonSerializerOptions CreateOptions() - { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - options.Converters.Add(new JsonStringEnumConverter()); - return options; - } + // Usa as opcoes reais do SDK (com SafeEnumConverterFactory + FlexibleDateTimeConverter) + // para os testes exercitarem o mesmo comportamento que produccao. + private static readonly JsonSerializerOptions Options = JsonSerializerConfiguration.Options; #region PaymentLink Models @@ -67,7 +56,7 @@ public void PaymentLink_Deserialize_FromApiJson() Assert.Equal(BillingType.CREDIT_CARD, result.BillingType); Assert.Equal(ChargeType.RECURRENT, result.ChargeType); Assert.Equal(10, result.DueDateLimitDays); - Assert.Equal("MONTHLY", result.SubscriptionCycle); + Assert.Equal(Codout.Apis.Asaas.Models.Subscription.Enums.Cycle.MONTHLY, result.SubscriptionCycle); Assert.Equal(1, result.MaxInstallmentCount); Assert.True(result.NotificationEnabled); Assert.NotNull(result.EndDate); @@ -260,9 +249,9 @@ public void CreditBureauReport_Deserialize() "id": "cbr_123", "customer": "cus_abc", "cpfCnpj": "12345678901", - "state": "SP", - "status": "DONE", - "dateCreated": "2024-06-15T10:30:00" + "downloadUrl": "https://example/report.pdf", + "reportFile": null, + "dateCreated": "2024-06-15" } """; @@ -272,9 +261,9 @@ public void CreditBureauReport_Deserialize() Assert.Equal("cbr_123", result.Id); Assert.Equal("cus_abc", result.Customer); Assert.Equal("12345678901", result.CpfCnpj); - Assert.Equal("SP", result.State); - Assert.Equal("DONE", result.Status); - Assert.Equal(new DateTime(2024, 6, 15, 10, 30, 0), result.DateCreated); + Assert.Equal("https://example/report.pdf", result.DownloadUrl); + Assert.Null(result.ReportFile); + Assert.Equal(new DateTime(2024, 6, 15), result.DateCreated); } [Fact] @@ -283,23 +272,21 @@ public void CreateCreditBureauReportRequest_Serialize() var request = new CreateCreditBureauReportRequest { Customer = "cus_test", - CpfCnpj = "98765432100", - State = "RJ" + CpfCnpj = "98765432100" }; var json = JsonSerializer.Serialize(request, Options); Assert.Contains("\"customer\":\"cus_test\"", json); Assert.Contains("\"cpfCnpj\":\"98765432100\"", json); - Assert.Contains("\"state\":\"RJ\"", json); } #endregion - #region CustomerFiscalInfo Models + #region FiscalInfo Models [Fact] - public void CustomerFiscalInfo_Deserialize() + public void FiscalInfo_Deserialize() { var json = """ { @@ -312,14 +299,14 @@ public void CustomerFiscalInfo_Deserialize() "specialTaxRegime": "MICROEMPRESA", "serviceListItem": "14.01", "rpsSerie": "A", - "rpsNumber": "100", - "loteNumber": "1", + "rpsNumber": 100, + "loteNumber": 1, "username": "testuser", "accessToken": "token123" } """; - var result = JsonSerializer.Deserialize(json, Options); + var result = JsonSerializer.Deserialize(json, Options); Assert.NotNull(result); Assert.Equal("fiscal@company.com", result.Email); @@ -331,8 +318,8 @@ public void CustomerFiscalInfo_Deserialize() Assert.Equal("MICROEMPRESA", result.SpecialTaxRegime); Assert.Equal("14.01", result.ServiceListItem); Assert.Equal("A", result.RpsSerie); - Assert.Equal("100", result.RpsNumber); - Assert.Equal("1", result.LoteNumber); + Assert.Equal(100, result.RpsNumber); + Assert.Equal(1, result.LoteNumber); Assert.Equal("testuser", result.Username); Assert.Equal("token123", result.AccessToken); } @@ -380,8 +367,8 @@ public void PixTransaction_Deserialize_WithEnum() "status": "DONE", "value": 250.75, "description": "Pix payment received", - "transactionDate": "2024-07-01T14:30:00", - "scheduleDate": null + "effectiveDate": "2024-07-01T14:30:00", + "scheduledDate": null } """; @@ -393,18 +380,18 @@ public void PixTransaction_Deserialize_WithEnum() Assert.Equal(PixTransactionStatus.DONE, result.Status); Assert.Equal(250.75m, result.Value); Assert.Equal("Pix payment received", result.Description); - Assert.NotNull(result.TransactionDate); - Assert.Null(result.ScheduleDate); + Assert.NotNull(result.EffectiveDate); + Assert.Null(result.ScheduledDate); } [Fact] public void PixTransaction_AllStatuses() { - var statuses = new[] { "PENDING", "DONE", "CANCELLED", "SCHEDULED", "FAILED" }; + var statuses = new[] { "AWAITING_REQUEST", "DONE", "CANCELLED", "SCHEDULED", "REFUSED" }; var expectedEnums = new[] { - PixTransactionStatus.PENDING, PixTransactionStatus.DONE, PixTransactionStatus.CANCELLED, - PixTransactionStatus.SCHEDULED, PixTransactionStatus.FAILED + PixTransactionStatus.AWAITING_REQUEST, PixTransactionStatus.DONE, PixTransactionStatus.CANCELLED, + PixTransactionStatus.SCHEDULED, PixTransactionStatus.REFUSED }; for (int i = 0; i < statuses.Length; i++) @@ -542,7 +529,7 @@ public void PixAddressKey_Deserialize_WithEnum() Assert.Equal("key_001", result.Id); Assert.Equal("12345678901", result.Key); Assert.Equal(PixAddressKeyType.CPF, result.Type); - Assert.Equal("ACTIVE", result.Status); + Assert.Equal(Codout.Apis.Asaas.Models.Pix.Enums.PixAddressKeyStatus.ACTIVE, result.Status); Assert.Equal(new DateTime(2024, 3, 1, 10, 0, 0), result.DateCreated); } diff --git a/Codout.Apis.Asaas/AsaasApi.cs b/Codout.Apis.Asaas/AsaasApi.cs index cff69b4..cd79530 100644 --- a/Codout.Apis.Asaas/AsaasApi.cs +++ b/Codout.Apis.Asaas/AsaasApi.cs @@ -16,7 +16,7 @@ public class AsaasApi(ApiSettings apiSettings) private Lazy LazyWallet { get; } = new(() => new WalletManager(apiSettings), true); private Lazy LazyWebhook { get; } = new(() => new WebhookManager(apiSettings), true); private Lazy LazyAsaasAccount { get; } = new(() => new AsaasAccountManager(apiSettings), true); - private Lazy LazyReceivableAnticipation { get; } = new(() => new AnticipationManager(apiSettings), true); + private Lazy LazyAnticipation { get; } = new(() => new AnticipationManager(apiSettings), true); private Lazy LazyMyAccount { get; } = new(() => new MyAccountManager(apiSettings), true); private Lazy LazyInvoice { get; } = new(() => new InvoiceManager(apiSettings), true); private Lazy LazyPaymentDunning { get; } = new(() => new PaymentDunningManager(apiSettings), true); @@ -25,8 +25,15 @@ public class AsaasApi(ApiSettings apiSettings) private Lazy LazyPaymentLink { get; } = new(() => new PaymentLinkManager(apiSettings), true); private Lazy LazyNotification { get; } = new(() => new NotificationManager(apiSettings), true); private Lazy LazyCreditBureauReport { get; } = new(() => new CreditBureauReportManager(apiSettings), true); - private Lazy LazyCustomerFiscalInfo { get; } = new(() => new CustomerFiscalInfoManager(apiSettings), true); + private Lazy LazyFiscalInfo { get; } = new(() => new FiscalInfoManager(apiSettings), true); private Lazy LazyPix { get; } = new(() => new PixManager(apiSettings), true); + private Lazy LazyChargeback { get; } = new(() => new ChargebackManager(apiSettings), true); + private Lazy LazyEscrow { get; } = new(() => new EscrowManager(apiSettings), true); + private Lazy LazyCheckout { get; } = new(() => new CheckoutManager(apiSettings), true); + private Lazy LazyMobilePhoneRecharge { get; } = new(() => new MobilePhoneRechargeManager(apiSettings), true); + private Lazy LazySandbox { get; } = new(() => new SandboxManager(apiSettings), true); + private Lazy LazyPixAutomatic { get; } = new(() => new PixAutomaticManager(apiSettings), true); + private Lazy LazyPixRecurring { get; } = new(() => new PixRecurringManager(apiSettings), true); #endregion @@ -40,7 +47,7 @@ public class AsaasApi(ApiSettings apiSettings) public WalletManager Wallet => LazyWallet.Value; public WebhookManager Webhook => LazyWebhook.Value; public AsaasAccountManager AsaasAccount => LazyAsaasAccount.Value; - public AnticipationManager ReceivableAnticipation => LazyReceivableAnticipation.Value; + public AnticipationManager Anticipation => LazyAnticipation.Value; public MyAccountManager MyAccount => LazyMyAccount.Value; public InvoiceManager Invoice => LazyInvoice.Value; public PaymentDunningManager PaymentDunning => LazyPaymentDunning.Value; @@ -49,7 +56,14 @@ public class AsaasApi(ApiSettings apiSettings) public PaymentLinkManager PaymentLink => LazyPaymentLink.Value; public NotificationManager Notification => LazyNotification.Value; public CreditBureauReportManager CreditBureauReport => LazyCreditBureauReport.Value; - public CustomerFiscalInfoManager CustomerFiscalInfo => LazyCustomerFiscalInfo.Value; + public FiscalInfoManager FiscalInfo => LazyFiscalInfo.Value; public PixManager Pix => LazyPix.Value; + public ChargebackManager Chargeback => LazyChargeback.Value; + public EscrowManager Escrow => LazyEscrow.Value; + public CheckoutManager Checkout => LazyCheckout.Value; + public MobilePhoneRechargeManager MobilePhoneRecharge => LazyMobilePhoneRecharge.Value; + public SandboxManager Sandbox => LazySandbox.Value; + public PixAutomaticManager PixAutomatic => LazyPixAutomatic.Value; + public PixRecurringManager PixRecurring => LazyPixRecurring.Value; #endregion } diff --git a/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj b/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj index 3cf676f..696d250 100644 --- a/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj +++ b/Codout.Apis.Asaas/Codout.Apis.Asaas.csproj @@ -6,7 +6,7 @@ Asaas.Api - 2.0.2 + 3.0.0 Clovis Coli Jr Codout SDK .NET no-oficial para integracao com a API v3 do Asaas (asaas.com). Suporta cobrancas (Boleto, Pix, Cartao), assinaturas, transferencias, links de pagamento, notas fiscais, antecipacoes, negativacoes e muito mais. Zero dependencias externas. @@ -18,9 +18,13 @@ https://github.com/codout/Codout.Apis.Asaas git -v2.0.2: -- Add: CreditCardToken em CreateSubscriptionRequest — paridade com CreatePaymentRequest, permite criar assinaturas com cartao tokenizado sem reutilizar dados sensiveis -- Veja CHANGELOG.md para historico completo +v3.0.0 (major release, breaking changes): +- Auditoria de conformidade contra a documentacao oficial do Asaas (via MCP) +- 7 bugs bloqueantes corrigidos: PUT em Updates, rota /fiscalInfo, Balance shape, etc. +- WebhookManager reescrito para CRUD em /v3/webhooks/{id} +- 7 novos managers: Chargeback, Escrow, Checkout, MobilePhoneRecharge, Sandbox, PixAutomatic, PixRecurring +- ~50 endpoints novos em managers existentes (Payment, MyAccount, AsaasAccount, Pix, ...) +- Veja CHANGELOG.md para a lista completa de breaking changes e guia de migracao Copyright (c) Clovis Coli Jr @@ -46,5 +50,9 @@ v2.0.2: + + + + diff --git a/Codout.Apis.Asaas/Core/BaseManager.cs b/Codout.Apis.Asaas/Core/BaseManager.cs index 95f0eb3..ecc5ad3 100644 --- a/Codout.Apis.Asaas/Core/BaseManager.cs +++ b/Codout.Apis.Asaas/Core/BaseManager.cs @@ -20,11 +20,11 @@ public class BaseManager private const string ProductionUrl = "https://api.asaas.com"; private const string SandboxUrl = "https://api-sandbox.asaas.com"; - private readonly ApiSettings _settings; + protected readonly ApiSettings Settings; protected BaseManager(ApiSettings settings) { - _settings = settings; + Settings = settings; } protected async Task> PostMultipartFormDataContentAsync(string resource, object payload) @@ -54,11 +54,14 @@ protected async Task> PostMultipartFormDataContentAsync(str if (prop.PropertyType == typeof(IAsaasFile)) { IAsaasFile asaasFile = prop.GetValue(payload) as IAsaasFile; + if (asaasFile is null) continue; multipartContent.Add(BuildByteArrayContent(asaasFile), jsonPropertyName, asaasFile.FileName); continue; } - multipartContent.Add(new StringContent(prop.GetValue(payload).ToString()), jsonPropertyName); + var value = prop.GetValue(payload); + if (value is null) continue; + multipartContent.Add(new StringContent(value.ToString()), jsonPropertyName); } var response = await httpClient.PostAsync(BuildApiRoute(resource), multipartContent); @@ -143,13 +146,25 @@ protected async Task> DeleteAsync(string resource, string i return await BuildResponseObject(response); } + private static readonly SocketsHttpHandler SharedHandler = new() + { + // Reusa conexoes TCP entre requisicoes (evita esgotamento de portas). + // Recicla a conexao a cada 10 min para captar mudancas de DNS. + PooledConnectionLifetime = TimeSpan.FromMinutes(10), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), + MaxConnectionsPerServer = 32 + }; + protected virtual HttpClient BuildHttpClient() { - HttpClient httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.TryAddWithoutValidation("access_token", _settings.AccessToken); - httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", _settings.ApplicationName); + // Compartilha um unico SocketsHttpHandler estatico para nao esgotar + // sockets nem fazer DNS lookup a cada request (boa pratica .NET). + // DisposeHandler = false porque o handler eh shared. + HttpClient httpClient = new HttpClient(SharedHandler, disposeHandler: false); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("access_token", Settings.AccessToken); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Settings.ApplicationName); httpClient.BaseAddress = BuildBaseAddress(); - httpClient.Timeout = _settings.TimeOut; + httpClient.Timeout = Settings.TimeOut; return httpClient; } @@ -161,12 +176,12 @@ private string BuildApiRoute(string resource) private Uri BuildBaseAddress() { - if (_settings.AsaasEnvironment.IsProduction()) + if (Settings.AsaasEnvironment.IsProduction()) { return new Uri(ProductionUrl); } - if (_settings.AsaasEnvironment.IsSandbox()) + if (Settings.AsaasEnvironment.IsSandbox()) { return new Uri(SandboxUrl); } diff --git a/Codout.Apis.Asaas/Core/Extension/DateTimeExtensions.cs b/Codout.Apis.Asaas/Core/Extension/DateTimeExtensions.cs index 5e378ff..640e65c 100644 --- a/Codout.Apis.Asaas/Core/Extension/DateTimeExtensions.cs +++ b/Codout.Apis.Asaas/Core/Extension/DateTimeExtensions.cs @@ -1,11 +1,18 @@ using System; +using System.Globalization; namespace Codout.Apis.Asaas.Core.Extension; internal static class DateTimeExtensions { + /// + /// Formata uma data no formato ISO YYYY-MM-DD usado pela API Asaas em + /// query params (filtros como paymentDate, dueDate, startDate). + /// Usa InvariantCulture explicitamente para garantir formato consistente + /// mesmo em ambientes pt-BR, es-ES, etc. + /// public static string ToApiRequest(this DateTime dateTime) { - return dateTime.ToString("yyyy-MM-dd"); + return dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } } \ No newline at end of file diff --git a/Codout.Apis.Asaas/Core/RequestParameters.cs b/Codout.Apis.Asaas/Core/RequestParameters.cs index 9e825ef..e6a502c 100644 --- a/Codout.Apis.Asaas/Core/RequestParameters.cs +++ b/Codout.Apis.Asaas/Core/RequestParameters.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Codout.Apis.Asaas.Core.Extension; using Codout.Apis.Asaas.Core.Utils; @@ -58,7 +59,10 @@ public void Add(string key, bool? value) { if (value != null) { - Add(key, value.ToString()); + // bool.ToString() retorna "True"/"False" (PascalCase), mas a API Asaas + // espera "true"/"false" lowercase (padrao JSON/HTTP). Sem o ToLower + // o filtro e silenciosamente ignorado pela API. + Add(key, value.Value ? "true" : "false"); return; } @@ -69,7 +73,9 @@ public void Add(string key, decimal? value) { if (value != null) { - Add(key, value.ToString()); + // decimal.ToString() usa cultura corrente: em pt-BR vira "12,5" + // (virgula) ao inves de "12.5" (ponto). API Asaas (JSON) exige ponto. + Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); return; } diff --git a/Codout.Apis.Asaas/Core/Response/Base/BaseResponse.cs b/Codout.Apis.Asaas/Core/Response/Base/BaseResponse.cs index 594d156..8709673 100644 --- a/Codout.Apis.Asaas/Core/Response/Base/BaseResponse.cs +++ b/Codout.Apis.Asaas/Core/Response/Base/BaseResponse.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net; using System.Text.Json; using Codout.Apis.Asaas.Core.Extension; @@ -22,7 +23,7 @@ protected BaseResponse(HttpStatusCode httpStatusCode, string content) private void BuildErrors() { - if (WasSucessfull() || string.IsNullOrEmpty(AsaasResponse)) + if (WasSuccessful() || string.IsNullOrEmpty(AsaasResponse)) return; try @@ -51,5 +52,8 @@ private void BuildErrors() } } - public bool WasSucessfull() => StatusCode.IsSuccessStatusCode(); + public bool WasSuccessful() => StatusCode.IsSuccessStatusCode(); + + [Obsolete("Typo na grafia original. Use WasSuccessful() em vez disso. Sera removido em versao futura.")] + public bool WasSucessfull() => WasSuccessful(); } diff --git a/Codout.Apis.Asaas/Managers/AnticipationManager.cs b/Codout.Apis.Asaas/Managers/AnticipationManager.cs index 544a3f3..0df36e3 100644 --- a/Codout.Apis.Asaas/Managers/AnticipationManager.cs +++ b/Codout.Apis.Asaas/Managers/AnticipationManager.cs @@ -36,9 +36,27 @@ public async Task> List(int offset, int limit, Antici return responseList; } - public async Task> SignAgreement(SignAnticipationAgreementRequest requestObj) + public async Task> Cancel(string anticipationId) { - var route = $"{AnticipationsRoute}/agreement/sign"; - return await PostAsync(route, requestObj); + var route = $"{AnticipationsRoute}/{anticipationId}/cancel"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> GetLimits() + { + var route = $"{AnticipationsRoute}/limits"; + return await GetAsync(route); + } + + public async Task> GetAutomaticConfiguration() + { + var route = $"{AnticipationsRoute}/configurations"; + return await GetAsync(route); + } + + public async Task> UpdateAutomaticConfiguration(UpdateAutomaticAnticipationConfigRequest requestObj) + { + var route = $"{AnticipationsRoute}/configurations"; + return await PutAsync(route, requestObj); } } diff --git a/Codout.Apis.Asaas/Managers/AsaasAccountManager.cs b/Codout.Apis.Asaas/Managers/AsaasAccountManager.cs index 7bdc1b7..a97374b 100644 --- a/Codout.Apis.Asaas/Managers/AsaasAccountManager.cs +++ b/Codout.Apis.Asaas/Managers/AsaasAccountManager.cs @@ -2,6 +2,7 @@ using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; using Codout.Apis.Asaas.Models.AsaasAccount; +using Codout.Apis.Asaas.Models.Common.Base; namespace Codout.Apis.Asaas.Managers; @@ -18,4 +19,39 @@ public async Task> List(int offset, int limit) { return await GetListAsync(AsaasAccountRoute, offset, limit); } + + public async Task> Find(string accountId) + { + return await GetAsync(AsaasAccountRoute, accountId); + } + + public async Task> ResendActivationLink(string accountId) + { + var route = $"{AsaasAccountRoute}/{accountId}/resendActivationLink"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> CreateAccessToken(string accountId, CreateAccessTokenRequest requestObj) + { + var route = $"{AsaasAccountRoute}/{accountId}/accessTokens"; + return await PostAsync(route, requestObj); + } + + public async Task> ListAccessTokens(string accountId, int offset, int limit) + { + var route = $"{AsaasAccountRoute}/{accountId}/accessTokens"; + return await GetListAsync(route, offset, limit); + } + + public async Task> UpdateAccessToken(string accountId, string accessTokenId, UpdateAccessTokenRequest requestObj) + { + var route = $"{AsaasAccountRoute}/{accountId}/accessTokens/{accessTokenId}"; + return await PutAsync(route, requestObj); + } + + public async Task> DeleteAccessToken(string accountId, string accessTokenId) + { + var route = $"{AsaasAccountRoute}/{accountId}/accessTokens/{accessTokenId}"; + return await DeleteAsync(route); + } } diff --git a/Codout.Apis.Asaas/Managers/ChargebackManager.cs b/Codout.Apis.Asaas/Managers/ChargebackManager.cs new file mode 100644 index 0000000..48f28e2 --- /dev/null +++ b/Codout.Apis.Asaas/Managers/ChargebackManager.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Chargeback; + +namespace Codout.Apis.Asaas.Managers; + +public class ChargebackManager(ApiSettings settings) : BaseManager(settings) +{ + private const string ChargebacksRoute = "/chargebacks"; + private const string PaymentsRoute = "/payments"; + + public async Task> List(int offset, int limit) + { + return await GetListAsync(ChargebacksRoute, offset, limit); + } + + public async Task> FindByPayment(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/chargeback"; + return await GetAsync(route); + } + + public async Task> CreateDispute(string chargebackId, CreateChargebackDisputeRequest requestObj) + { + var route = $"{ChargebacksRoute}/{chargebackId}/dispute"; + return await PostMultipartFormDataContentAsync(route, requestObj); + } +} diff --git a/Codout.Apis.Asaas/Managers/CheckoutManager.cs b/Codout.Apis.Asaas/Managers/CheckoutManager.cs new file mode 100644 index 0000000..aa1968b --- /dev/null +++ b/Codout.Apis.Asaas/Managers/CheckoutManager.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Checkout; + +namespace Codout.Apis.Asaas.Managers; + +public class CheckoutManager(ApiSettings settings) : BaseManager(settings) +{ + private const string CheckoutsRoute = "/checkouts"; + + public async Task> Create(CreateCheckoutRequest requestObj) + { + return await PostAsync(CheckoutsRoute, requestObj); + } + + public async Task> Cancel(string checkoutId) + { + var route = $"{CheckoutsRoute}/{checkoutId}/cancel"; + return await PostAsync(route, new RequestParameters()); + } +} diff --git a/Codout.Apis.Asaas/Managers/CreditBureauReportManager.cs b/Codout.Apis.Asaas/Managers/CreditBureauReportManager.cs index f304e8d..cb00721 100644 --- a/Codout.Apis.Asaas/Managers/CreditBureauReportManager.cs +++ b/Codout.Apis.Asaas/Managers/CreditBureauReportManager.cs @@ -14,9 +14,9 @@ public async Task> Create(CreateCreditBureauR return await PostAsync(CreditBureauReportRoute, requestObj); } - public async Task> List(int offset, int limit) + public async Task> List(int offset, int limit, CreditBureauReportListFilter filter = null) { - return await GetListAsync(CreditBureauReportRoute, offset, limit); + return await GetListAsync(CreditBureauReportRoute, offset, limit, filter); } public async Task> Find(string creditBureauReportId) diff --git a/Codout.Apis.Asaas/Managers/CreditCardManager.cs b/Codout.Apis.Asaas/Managers/CreditCardManager.cs index d2d9990..04b52b3 100644 --- a/Codout.Apis.Asaas/Managers/CreditCardManager.cs +++ b/Codout.Apis.Asaas/Managers/CreditCardManager.cs @@ -14,4 +14,14 @@ public async Task> TokenizeCreditCard(TokenizeCreditC { return await PostAsync($"{PaymentsRoute}/tokenizeCreditCard", requestObj); } + + public async Task> SavePreAuthorizationConfig(SavePreAuthorizationConfigRequest requestObj) + { + return await PostAsync($"{PaymentsRoute}/preAuthorization/config", requestObj); + } + + public async Task> GetPreAuthorizationConfig() + { + return await GetAsync($"{PaymentsRoute}/preAuthorization/config"); + } } \ No newline at end of file diff --git a/Codout.Apis.Asaas/Managers/CustomerFiscalInfoManager.cs b/Codout.Apis.Asaas/Managers/CustomerFiscalInfoManager.cs deleted file mode 100644 index a82dd66..0000000 --- a/Codout.Apis.Asaas/Managers/CustomerFiscalInfoManager.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; -using Codout.Apis.Asaas.Core; -using Codout.Apis.Asaas.Core.Response; -using Codout.Apis.Asaas.Models.CustomerFiscalInfo; - -namespace Codout.Apis.Asaas.Managers; - -public class CustomerFiscalInfoManager(ApiSettings settings) : BaseManager(settings) -{ - private const string CustomerFiscalInfoRoute = "/customerFiscalInfo"; - - public async Task> CreateOrUpdate(CreateCustomerFiscalInfoRequest requestObj) - { - return await PostMultipartFormDataContentAsync(CustomerFiscalInfoRoute, requestObj); - } - - public async Task> Find() - { - return await GetAsync(CustomerFiscalInfoRoute); - } - - public async Task> ListMunicipalOptions() - { - var route = $"{CustomerFiscalInfoRoute}/municipalOptions"; - return await GetListAsync(route, 0, 100); - } -} diff --git a/Codout.Apis.Asaas/Managers/CustomerManager.cs b/Codout.Apis.Asaas/Managers/CustomerManager.cs index b93fedc..e8defe4 100644 --- a/Codout.Apis.Asaas/Managers/CustomerManager.cs +++ b/Codout.Apis.Asaas/Managers/CustomerManager.cs @@ -2,6 +2,7 @@ using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; using Codout.Apis.Asaas.Models.Customer; +using Codout.Apis.Asaas.Models.Notification; namespace Codout.Apis.Asaas.Managers; @@ -33,7 +34,7 @@ public async Task> Update(string customerId, UpdateCust { var route = $"{CustomersRoute}/{customerId}"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } public async Task> Delete(string customerId) @@ -47,4 +48,15 @@ public async Task> Restore(string customerId) return await PostAsync(route, new RequestParameters()); } + + /// + /// GET /v3/customers/{id}/notifications — recupera notificacoes do cliente. + /// Envelope padrao com paginacao. + /// + public async Task> GetNotifications(string customerId) + { + var route = $"{CustomersRoute}/{customerId}/notifications"; + + return await GetListAsync(route, 0, 100); + } } \ No newline at end of file diff --git a/Codout.Apis.Asaas/Managers/EscrowManager.cs b/Codout.Apis.Asaas/Managers/EscrowManager.cs new file mode 100644 index 0000000..fc91d9b --- /dev/null +++ b/Codout.Apis.Asaas/Managers/EscrowManager.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Escrow; +using Codout.Apis.Asaas.Models.Payment; + +namespace Codout.Apis.Asaas.Managers; + +public class EscrowManager(ApiSettings settings) : BaseManager(settings) +{ + private const string AccountsRoute = "/accounts"; + private const string EscrowRoute = "/escrow"; + private const string PaymentsRoute = "/payments"; + + public async Task> SaveSubaccountConfig(string accountId, SaveEscrowConfigRequest requestObj) + { + var route = $"{AccountsRoute}/{accountId}/escrow"; + return await PostAsync(route, requestObj); + } + + public async Task> GetSubaccountConfig(string accountId) + { + var route = $"{AccountsRoute}/{accountId}/escrow"; + return await GetAsync(route); + } + + public async Task> SaveDefaultConfig(SaveEscrowConfigRequest requestObj) + { + var route = $"{AccountsRoute}/escrow"; + return await PostAsync(route, requestObj); + } + + public async Task> GetDefaultConfig() + { + var route = $"{AccountsRoute}/escrow"; + return await GetAsync(route); + } + + public async Task> FinishPaymentEscrow(string escrowId) + { + // A API documenta retorno PaymentGetResponseDTO (Payment) e request body + // PaymentEscrowPathIdRequestDTO sem propriedades, entao mandamos {} vazio. + var route = $"{EscrowRoute}/{escrowId}/finish"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> GetPaymentEscrow(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/escrow"; + return await GetAsync(route); + } +} diff --git a/Codout.Apis.Asaas/Managers/FinanceManager.cs b/Codout.Apis.Asaas/Managers/FinanceManager.cs index 1d7e496..4955b0d 100644 --- a/Codout.Apis.Asaas/Managers/FinanceManager.cs +++ b/Codout.Apis.Asaas/Managers/FinanceManager.cs @@ -10,11 +10,11 @@ public class FinanceManager(ApiSettings settings) : BaseManager(settings) private const string FinanceRoute = "/finance"; private const string FinanceTransactionsRoute = "/financialTransactions"; - public async Task> Balance() + public async Task> GetBalance() { var route = $"{FinanceRoute}/balance"; - return await GetAsync(route); + return await GetAsync(route); } public async Task> ListTransactions(int offset, int limit, FinancialTransactionListFilter filter = null) @@ -25,9 +25,10 @@ public async Task> ListTransactions(int offse return await GetListAsync(FinanceTransactionsRoute, offset, limit, queryMap); } - public async Task> GetPaymentStatistics() + public async Task> GetPaymentStatistics(PaymentStatisticsFilter filter = null) { - var route = $"{FinanceRoute}/payment/statistics"; + var query = filter?.Build() ?? string.Empty; + var route = $"{FinanceRoute}/payment/statistics{query}"; return await GetAsync(route); } diff --git a/Codout.Apis.Asaas/Managers/FiscalInfoManager.cs b/Codout.Apis.Asaas/Managers/FiscalInfoManager.cs new file mode 100644 index 0000000..b3bcdc5 --- /dev/null +++ b/Codout.Apis.Asaas/Managers/FiscalInfoManager.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.FiscalInfo; + +namespace Codout.Apis.Asaas.Managers; + +public class FiscalInfoManager(ApiSettings settings) : BaseManager(settings) +{ + private const string FiscalInfoRoute = "/fiscalInfo"; + + public async Task> CreateOrUpdate(CreateFiscalInfoRequest requestObj) + { + return await PostMultipartFormDataContentAsync(FiscalInfoRoute, requestObj); + } + + public async Task> Find() + { + return await GetAsync(FiscalInfoRoute); + } + + public async Task> ListMunicipalOptions() + { + var route = $"{FiscalInfoRoute}/municipalOptions"; + return await GetListAsync(route, 0, 100); + } + + public async Task> ListServices(string description, int offset = 0, int limit = 10) + { + var queryMap = new RequestParameters + { + { "description", description } + }; + + var route = $"{FiscalInfoRoute}/services"; + + return await GetListAsync(route, offset, limit, queryMap); + } +} diff --git a/Codout.Apis.Asaas/Managers/InstallmentManager.cs b/Codout.Apis.Asaas/Managers/InstallmentManager.cs index 2ece6e7..e79ea85 100644 --- a/Codout.Apis.Asaas/Managers/InstallmentManager.cs +++ b/Codout.Apis.Asaas/Managers/InstallmentManager.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; using Codout.Apis.Asaas.Models.Installment; @@ -10,6 +10,16 @@ public class InstallmentManager(ApiSettings settings) : BaseManager(settings) { private const string InstallmentsRoute = "/installments"; + public async Task> Create(CreateInstallmentRequest requestObj) + { + return await PostAsync(InstallmentsRoute, requestObj); + } + + public async Task> CreateWithCreditCard(CreateInstallmentWithCreditCardRequest requestObj) + { + return await PostAsync($"{InstallmentsRoute}/", requestObj); + } + public async Task> Find(string installmentId) { var route = $"{InstallmentsRoute}/{installmentId}"; @@ -40,4 +50,24 @@ public async Task> ListPaymentBook(string installmentId, i var route = $"{InstallmentsRoute}/{installmentId}/paymentBook"; return await GetListAsync(route, offset, limit); } + + public async Task> ListPayments(string installmentId, int offset, int limit) + { + var route = $"{InstallmentsRoute}/{installmentId}/payments"; + return await GetListAsync(route, offset, limit); + } + + public async Task> CancelPendingPayments(string installmentId) + { + var route = $"{InstallmentsRoute}/{installmentId}/payments"; + + return await DeleteAsync(route); + } + + public async Task> UpdateSplits(string installmentId, UpdateInstallmentSplitsRequest requestObj) + { + var route = $"{InstallmentsRoute}/{installmentId}/splits"; + + return await PutAsync(route, requestObj); + } } diff --git a/Codout.Apis.Asaas/Managers/InvoiceManager.cs b/Codout.Apis.Asaas/Managers/InvoiceManager.cs index 1866c8e..13d397a 100644 --- a/Codout.Apis.Asaas/Managers/InvoiceManager.cs +++ b/Codout.Apis.Asaas/Managers/InvoiceManager.cs @@ -46,16 +46,4 @@ public async Task> Cancel(string invoiceId) var route = $"{InvoicesRoute}/{invoiceId}/cancel"; return await PostAsync(route, new RequestParameters()); } - - public async Task> ListMunicipalServices(string serviceDescription) - { - var queryMap = new RequestParameters - { - { "description", serviceDescription } - }; - - var route = $"{InvoicesRoute}/municipalServices"; - - return await GetListAsync(route, 0, 0, queryMap); - } } diff --git a/Codout.Apis.Asaas/Managers/MobilePhoneRechargeManager.cs b/Codout.Apis.Asaas/Managers/MobilePhoneRechargeManager.cs new file mode 100644 index 0000000..6aa8879 --- /dev/null +++ b/Codout.Apis.Asaas/Managers/MobilePhoneRechargeManager.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.MobilePhoneRecharge; + +namespace Codout.Apis.Asaas.Managers; + +public class MobilePhoneRechargeManager(ApiSettings settings) : BaseManager(settings) +{ + private const string MobilePhoneRechargesRoute = "/mobilePhoneRecharges"; + + public async Task> Create(CreateMobilePhoneRechargeRequest requestObj) + { + return await PostAsync(MobilePhoneRechargesRoute, requestObj); + } + + public async Task> List(int offset, int limit) + { + return await GetListAsync(MobilePhoneRechargesRoute, offset, limit); + } + + public async Task> Find(string rechargeId) + { + return await GetAsync(MobilePhoneRechargesRoute, rechargeId); + } + + public async Task> Cancel(string rechargeId) + { + var route = $"{MobilePhoneRechargesRoute}/{rechargeId}/cancel"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> GetProvider(string phoneNumber) + { + var route = $"{MobilePhoneRechargesRoute}/{phoneNumber}/provider"; + return await GetAsync(route); + } +} diff --git a/Codout.Apis.Asaas/Managers/MyAccountManager.cs b/Codout.Apis.Asaas/Managers/MyAccountManager.cs index 21cf692..3e67fea 100644 --- a/Codout.Apis.Asaas/Managers/MyAccountManager.cs +++ b/Codout.Apis.Asaas/Managers/MyAccountManager.cs @@ -1,6 +1,7 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Base; using Codout.Apis.Asaas.Models.MyAccount; namespace Codout.Apis.Asaas.Managers; @@ -8,13 +9,31 @@ namespace Codout.Apis.Asaas.Managers; public class MyAccountManager(ApiSettings settings) : BaseManager(settings) { private const string MyAccountRoute = "/myAccount"; + private const string CommercialInfoRoute = MyAccountRoute + "/commercialInfo"; private const string PaymentCheckoutConfigRoute = MyAccountRoute + "/paymentCheckoutConfig"; private const string FeesRoute = MyAccountRoute + "/fees"; private const string AccountNumberRoute = MyAccountRoute + "/accountNumber"; + private const string StatusRoute = MyAccountRoute + "/status"; + private const string DocumentsRoute = MyAccountRoute + "/documents"; - public async Task> Find() + public async Task> GetCommercialInfo() { - return await GetAsync(MyAccountRoute); + return await GetAsync(CommercialInfoRoute); + } + + public async Task> UpdateCommercialInfo(UpdateCommercialInfoRequest requestObj) + { + return await PostAsync(CommercialInfoRoute, requestObj); + } + + public async Task> GetStatus() + { + return await GetAsync(StatusRoute); + } + + public async Task> DeleteWhiteLabelAccount() + { + return await DeleteAsync(MyAccountRoute); } public async Task> CreatePaymentCheckoutConfig(CreatePaymentCheckoutConfigRequest requestObj) @@ -36,4 +55,33 @@ public async Task> FindAccountNumber() { return await GetAsync(AccountNumberRoute); } -} \ No newline at end of file + + public async Task> ListPendingDocuments() + { + return await GetAsync(DocumentsRoute); + } + + public async Task> SubmitDocument(string documentId, UploadAccountDocumentRequest requestObj) + { + var route = $"{DocumentsRoute}/{documentId}"; + return await PostMultipartFormDataContentAsync(route, requestObj); + } + + public async Task> ViewDocumentFile(string fileId) + { + var route = $"{DocumentsRoute}/files/{fileId}"; + return await GetAsync(route); + } + + public async Task> UpdateDocumentFile(string fileId, UploadAccountDocumentRequest requestObj) + { + var route = $"{DocumentsRoute}/files/{fileId}"; + return await PostMultipartFormDataContentAsync(route, requestObj); + } + + public async Task> DeleteDocumentFile(string fileId) + { + var route = $"{DocumentsRoute}/files/{fileId}"; + return await DeleteAsync(route); + } +} diff --git a/Codout.Apis.Asaas/Managers/NotificationManager.cs b/Codout.Apis.Asaas/Managers/NotificationManager.cs index 39b2481..8b75fde 100644 --- a/Codout.Apis.Asaas/Managers/NotificationManager.cs +++ b/Codout.Apis.Asaas/Managers/NotificationManager.cs @@ -12,12 +12,12 @@ public class NotificationManager(ApiSettings settings) : BaseManager(settings) public async Task> Update(string notificationId, UpdateNotificationRequest requestObj) { var route = $"{NotificationsRoute}/{notificationId}"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } public async Task> BatchUpdate(BatchUpdateNotificationRequest requestObj) { var route = $"{NotificationsRoute}/batch"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } } diff --git a/Codout.Apis.Asaas/Managers/PaymentDunningManager.cs b/Codout.Apis.Asaas/Managers/PaymentDunningManager.cs index f36581f..2dc7037 100644 --- a/Codout.Apis.Asaas/Managers/PaymentDunningManager.cs +++ b/Codout.Apis.Asaas/Managers/PaymentDunningManager.cs @@ -18,9 +18,14 @@ public async Task> Create(CreatePaymentDunningReq public async Task> Simulate(SimulatePaymentDunningRequest requestObj) { - var route = $"{PaymentDunningRoute}/simulate"; - - return await PostAsync(route, requestObj); + // Schema oficial expoe "payment" como QUERY param, body vazio. + // Mandar PaymentId no body funciona em alguns endpoints por leniencia, + // mas o contrato documentado exige query string. + var query = new RequestParameters(); + query.Add("payment", requestObj?.PaymentId); + var route = $"{PaymentDunningRoute}/simulate{query.Build()}"; + + return await PostAsync(route, new RequestParameters()); } public async Task> Find(string paymentDunningId) diff --git a/Codout.Apis.Asaas/Managers/PaymentManager.cs b/Codout.Apis.Asaas/Managers/PaymentManager.cs index 67057fa..2101cb2 100644 --- a/Codout.Apis.Asaas/Managers/PaymentManager.cs +++ b/Codout.Apis.Asaas/Managers/PaymentManager.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Threading.Tasks; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Base; using Codout.Apis.Asaas.Models.Payment; namespace Codout.Apis.Asaas.Managers; @@ -15,6 +16,11 @@ public async Task> Create(CreatePaymentRequest requestOb return await PostAsync(PaymentsRoute, requestObj); } + public async Task> CreateWithCreditCard(CreatePaymentRequest requestObj) + { + return await PostAsync($"{PaymentsRoute}/", requestObj); + } + public async Task> Find(string id) { var route = $"{PaymentsRoute}/{id}"; @@ -33,7 +39,7 @@ public async Task> Update(string paymentId, UpdatePaymen { var route = $"{PaymentsRoute}/{paymentId}"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } public async Task> Delete(string paymentId) @@ -55,25 +61,45 @@ public async Task> Refund(string paymentId) return await PostAsync(route, new RequestParameters()); } + public async Task> ListRefunds(string paymentId, int offset, int limit) + { + var route = $"{PaymentsRoute}/{paymentId}/refunds"; + return await GetListAsync(route, offset, limit); + } + + public async Task> RefundBankSlip(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/bankSlip/refund"; + return await PostAsync(route, new RequestParameters()); + } + public async Task> ReceiveInCash(string paymentId, DateTime paymentDate, decimal value, bool notifyCustomer) { var route = $"{PaymentsRoute}/{paymentId}/receiveInCash"; - RequestParameters parameters = new RequestParameters + var parameters = new ReceiveInCashRequest { - { "paymentDate", paymentDate }, - { "value", value }, - { "notifyCustomer", notifyCustomer } + PaymentDate = paymentDate, + Value = value, + NotifyCustomer = notifyCustomer }; return await PostAsync(route, parameters); } + + public async Task> UndoReceivedInCash(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/undoReceivedInCash"; + return await PostAsync(route, new RequestParameters()); + } + public async Task> GetBankSlipBarCode(string paymentId) { var route = $"{PaymentsRoute}/{paymentId}/identificationField"; return await GetAsync(route); } + public async Task> GetPixQrCode(string paymentId) { var route = $"{PaymentsRoute}/{paymentId}/pixQrCode"; @@ -81,9 +107,99 @@ public async Task> GetPixQrCode(string paymentId) return await GetAsync(route); } - public async Task> UndoReceivedInCash(string paymentId) + public async Task> CaptureAuthorizedPayment(string paymentId, CapturePaymentRequest requestObj) { - var route = $"{PaymentsRoute}/{paymentId}/undoReceivedInCash"; - return await PostAsync(route, new RequestParameters()); + var route = $"{PaymentsRoute}/{paymentId}/captureAuthorizedPayment"; + return await PostAsync(route, requestObj); + } + + public async Task> PayWithCreditCard(string paymentId, PayWithCreditCardRequest requestObj) + { + var route = $"{PaymentsRoute}/{paymentId}/payWithCreditCard"; + return await PostAsync(route, requestObj); + } + + public async Task> GetBillingInfo(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/billingInfo"; + return await GetAsync(route); + } + + public async Task> GetViewingInfo(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/viewingInfo"; + return await GetAsync(route); + } + + public async Task> GetStatus(string paymentId) + { + var route = $"{PaymentsRoute}/{paymentId}/status"; + return await GetAsync(route); + } + + public async Task> Simulate(SimulatePaymentRequest requestObj) + { + var route = $"{PaymentsRoute}/simulate"; + return await PostAsync(route, requestObj); + } + + public async Task> GetLimits() + { + var route = $"{PaymentsRoute}/limits"; + return await GetAsync(route); + } + + public async Task> UploadDocument(string paymentId, UploadPaymentDocumentRequest requestObj) + { + var route = $"{PaymentsRoute}/{paymentId}/documents"; + return await PostMultipartFormDataContentAsync(route, requestObj); + } + + public async Task> ListDocuments(string paymentId, int offset, int limit) + { + var route = $"{PaymentsRoute}/{paymentId}/documents"; + return await GetListAsync(route, offset, limit); + } + + public async Task> FindDocument(string paymentId, string documentId) + { + var route = $"{PaymentsRoute}/{paymentId}/documents/{documentId}"; + return await GetAsync(route); + } + + public async Task> UpdateDocument(string paymentId, string documentId, UpdatePaymentDocumentRequest requestObj) + { + var route = $"{PaymentsRoute}/{paymentId}/documents/{documentId}"; + return await PutAsync(route, requestObj); + } + + public async Task> DeleteDocument(string paymentId, string documentId) + { + var route = $"{PaymentsRoute}/{paymentId}/documents/{documentId}"; + return await DeleteAsync(route); + } + + public async Task> ListPaidSplits(int offset, int limit) + { + var route = $"{PaymentsRoute}/splits/paid"; + return await GetListAsync(route, offset, limit); + } + + public async Task> FindPaidSplit(string splitId) + { + var route = $"{PaymentsRoute}/splits/paid/{splitId}"; + return await GetAsync(route); + } + + public async Task> ListReceivedSplits(int offset, int limit) + { + var route = $"{PaymentsRoute}/splits/received"; + return await GetListAsync(route, offset, limit); + } + + public async Task> FindReceivedSplit(string splitId) + { + var route = $"{PaymentsRoute}/splits/received/{splitId}"; + return await GetAsync(route); } -} \ No newline at end of file +} diff --git a/Codout.Apis.Asaas/Managers/PixAutomaticManager.cs b/Codout.Apis.Asaas/Managers/PixAutomaticManager.cs new file mode 100644 index 0000000..8b877a0 --- /dev/null +++ b/Codout.Apis.Asaas/Managers/PixAutomaticManager.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Base; +using Codout.Apis.Asaas.Models.PixAutomatic; + +namespace Codout.Apis.Asaas.Managers; + +public class PixAutomaticManager(ApiSettings settings) : BaseManager(settings) +{ + private const string AuthorizationsRoute = "/pix/automatic/authorizations"; + private const string PaymentInstructionsRoute = "/pix/automatic/paymentInstructions"; + + public async Task> CreateAuthorization(CreatePixAutomaticAuthorizationRequest requestObj) + { + return await PostAsync(AuthorizationsRoute, requestObj); + } + + public async Task> ListAuthorizations(int offset, int limit, PixAutomaticAuthorizationListFilter filter = null) + { + var queryMap = new RequestParameters(); + if (filter != null) queryMap.AddRange(filter); + + return await GetListAsync(AuthorizationsRoute, offset, limit, queryMap); + } + + public async Task> FindAuthorization(string authorizationId) + { + var route = $"{AuthorizationsRoute}/{authorizationId}"; + return await GetAsync(route); + } + + public async Task> CancelAuthorization(string authorizationId) + { + var route = $"{AuthorizationsRoute}/{authorizationId}"; + return await DeleteAsync(route); + } + + public async Task> FindPaymentInstruction(string paymentInstructionId) + { + var route = $"{PaymentInstructionsRoute}/{paymentInstructionId}"; + return await GetAsync(route); + } + + public async Task> ListPaymentInstructions(int offset, int limit, PixAutomaticPaymentInstructionListFilter filter = null) + { + var queryMap = new RequestParameters(); + if (filter != null) queryMap.AddRange(filter); + + return await GetListAsync(PaymentInstructionsRoute, offset, limit, queryMap); + } +} diff --git a/Codout.Apis.Asaas/Managers/PixManager.cs b/Codout.Apis.Asaas/Managers/PixManager.cs index 088f740..518ddf5 100644 --- a/Codout.Apis.Asaas/Managers/PixManager.cs +++ b/Codout.Apis.Asaas/Managers/PixManager.cs @@ -10,10 +10,16 @@ public class PixManager(ApiSettings settings) : BaseManager(settings) { private const string PixRoute = "/pix"; - public async Task> ListTransactions(int offset, int limit) + public async Task> ListTransactions(int offset, int limit, PixTransactionListFilter filter = null) { var route = $"{PixRoute}/transactions"; - return await GetListAsync(route, offset, limit); + return await GetListAsync(route, offset, limit, filter); + } + + public async Task> FindTransaction(string transactionId) + { + var route = $"{PixRoute}/transactions/{transactionId}"; + return await GetAsync(route); } public async Task> CancelTransaction(string transactionId) @@ -28,6 +34,18 @@ public async Task> CreateStaticQrCode(CreatePixS return await PostAsync(route, requestObj); } + public async Task> DeleteStaticQrCode(string qrCodeId) + { + var route = $"{PixRoute}/qrCodes/static/{qrCodeId}"; + return await DeleteAsync(route); + } + + public async Task> GetAddressKeyTokenBucket() + { + var route = $"{PixRoute}/tokenBucket/addressKey"; + return await GetAsync(route); + } + public async Task> DecodeQrCode(DecodePixQrCodeRequest requestObj) { var route = $"{PixRoute}/qrCodes/decode"; diff --git a/Codout.Apis.Asaas/Managers/PixRecurringManager.cs b/Codout.Apis.Asaas/Managers/PixRecurringManager.cs new file mode 100644 index 0000000..924eb6f --- /dev/null +++ b/Codout.Apis.Asaas/Managers/PixRecurringManager.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.PixRecurring; + +namespace Codout.Apis.Asaas.Managers; + +public class PixRecurringManager(ApiSettings settings) : BaseManager(settings) +{ + private const string RecurringsRoute = "/pix/transactions/recurrings"; + + public async Task> List(int offset, int limit, PixRecurringTransactionListFilter filter = null) + { + return await GetListAsync(RecurringsRoute, offset, limit, filter); + } + + public async Task> Find(string recurringId) + { + var route = $"{RecurringsRoute}/{recurringId}"; + return await GetAsync(route); + } + + public async Task> Cancel(string recurringId) + { + var route = $"{RecurringsRoute}/{recurringId}/cancel"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> ListItems(string recurringId, int offset = 0, int limit = 10) + { + // API retorna envelope { data: [...] } sem hasMore/totalCount/limit/offset, + // entao usamos GetAsync com query string manual e tipamos com wrapper proprio. + var query = new RequestParameters + { + { "offset", offset }, + { "limit", limit } + }; + var route = $"{RecurringsRoute}/{recurringId}/items{query.Build()}"; + return await GetAsync(route); + } + + public async Task> CancelItem(string itemId) + { + var route = $"{RecurringsRoute}/items/{itemId}/cancel"; + return await PostAsync(route, new RequestParameters()); + } +} diff --git a/Codout.Apis.Asaas/Managers/SandboxManager.cs b/Codout.Apis.Asaas/Managers/SandboxManager.cs new file mode 100644 index 0000000..6670fb2 --- /dev/null +++ b/Codout.Apis.Asaas/Managers/SandboxManager.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Payment; + +namespace Codout.Apis.Asaas.Managers; + +public class SandboxManager(ApiSettings settings) : BaseManager(settings) +{ + private const string SandboxRoute = "/sandbox"; + + private void EnsureSandbox() + { + if (Settings.AsaasEnvironment.IsProduction()) + { + throw new InvalidOperationException( + "SandboxManager so pode ser usado em AsaasEnvironment.SANDBOX. " + + "Os endpoints /v3/sandbox/* nao existem em producao."); + } + } + + public async Task> ApproveAccount() + { + EnsureSandbox(); + var route = $"{SandboxRoute}/myAccount/approve"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> ConfirmPayment(string paymentId) + { + EnsureSandbox(); + var route = $"{SandboxRoute}/payment/{paymentId}/confirm"; + return await PostAsync(route, new RequestParameters()); + } + + public async Task> ForceOverdue(string paymentId) + { + EnsureSandbox(); + var route = $"{SandboxRoute}/payment/{paymentId}/overdue"; + return await PostAsync(route, new RequestParameters()); + } +} diff --git a/Codout.Apis.Asaas/Managers/SubscriptionManager.cs b/Codout.Apis.Asaas/Managers/SubscriptionManager.cs index 421800f..b2f6f0e 100644 --- a/Codout.Apis.Asaas/Managers/SubscriptionManager.cs +++ b/Codout.Apis.Asaas/Managers/SubscriptionManager.cs @@ -35,7 +35,7 @@ public async Task> List(int offset, int limit, Subscr public async Task> Update(string subscriptionId, UpdateSubscriptionRequest requestObj) { var route = $"{SubscriptionsRoute}/{subscriptionId}"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } public async Task> Delete(string subscriptionId) @@ -45,6 +45,12 @@ public async Task> Delete(string subscriptio return await DeleteAsync(route); } + public async Task> UpdateCreditCard(string subscriptionId, UpdateSubscriptionCreditCardRequest requestObj) + { + var route = $"{SubscriptionsRoute}/{subscriptionId}/creditCard"; + return await PutAsync(route, requestObj); + } + public async Task> ListPayments(string subscriptionId, int offset, int limit) { var route = $"{SubscriptionsRoute}/{subscriptionId}/payments"; @@ -79,7 +85,7 @@ public async Task> CreateInvoiceSett public async Task> UpdateInvoiceSettings(string subscriptionId, UpdateInvoiceSettingsRequest requestObj) { var route = $"{SubscriptionsRoute}/{subscriptionId}/invoiceSettings"; - return await PostAsync(route, requestObj); + return await PutAsync(route, requestObj); } public async Task> FindInvoiceSettings(string subscriptionId) diff --git a/Codout.Apis.Asaas/Managers/TransferManager.cs b/Codout.Apis.Asaas/Managers/TransferManager.cs index 12d138f..0a95781 100644 --- a/Codout.Apis.Asaas/Managers/TransferManager.cs +++ b/Codout.Apis.Asaas/Managers/TransferManager.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; using Codout.Apis.Asaas.Models.Transfer; @@ -18,12 +18,12 @@ public async Task> List(int offset, int limit, Transf return await GetListAsync(TransfersRoute, offset, limit, queryMap); } - public async Task> Execute(AsaasAccountTransferRequest requestObj) + public async Task> TransferToAsaasAccount(AsaasAccountTransferRequest requestObj) { - return await PostAsync(TransfersRoute, requestObj); + return await PostAsync($"{TransfersRoute}/", requestObj); } - public async Task> Execute(BankAccountTransferRequest requestObj) + public async Task> TransferToBankAccount(BankAccountTransferRequest requestObj) { return await PostAsync(TransfersRoute, requestObj); } @@ -32,4 +32,10 @@ public async Task> Find(string transferId) { return await GetAsync(TransfersRoute, transferId); } + + public async Task> Cancel(string transferId) + { + var route = $"{TransfersRoute}/{transferId}/cancel"; + return await DeleteAsync(route); + } } diff --git a/Codout.Apis.Asaas/Managers/WebhookManager.cs b/Codout.Apis.Asaas/Managers/WebhookManager.cs index 5c24972..afa6249 100644 --- a/Codout.Apis.Asaas/Managers/WebhookManager.cs +++ b/Codout.Apis.Asaas/Managers/WebhookManager.cs @@ -1,47 +1,49 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Core.Response; +using Codout.Apis.Asaas.Models.Common.Base; using Codout.Apis.Asaas.Models.Webhook; namespace Codout.Apis.Asaas.Managers; public class WebhookManager(ApiSettings settings) : BaseManager(settings) { - private const string WebhookRoute = "/webhook"; + private const string WebhooksRoute = "/webhooks"; - public async Task> CreateOrUpdatePaymentWebhook(WebhookRequest requestObj) + public async Task> Create(CreateWebhookRequest requestObj) { - return await PostAsync(WebhookRoute, requestObj); + return await PostAsync(WebhooksRoute, requestObj); } - public async Task> FindPaymentWebhook() + public async Task> List(int offset, int limit, WebhookListFilter filter = null) { - return await GetAsync(WebhookRoute); + var queryMap = new RequestParameters(); + if (filter != null) queryMap.AddRange(filter); + + return await GetListAsync(WebhooksRoute, offset, limit, queryMap); } - public async Task> CreateOrUpdateInvoiceWebhook(WebhookRequest requestObj) + public async Task> Find(string webhookId) { - var route = $"{WebhookRoute}/invoice"; - - return await PostAsync(route, requestObj); + var route = $"{WebhooksRoute}/{webhookId}"; + return await GetAsync(route); } - public async Task> FindInvoiceWebhook() + public async Task> Update(string webhookId, UpdateWebhookRequest requestObj) { - var route = $"{WebhookRoute}/invoice"; - - return await GetAsync(route); + var route = $"{WebhooksRoute}/{webhookId}"; + return await PutAsync(route, requestObj); } - public async Task> CreateOrUpdateMobilePhoneRechargeWebhook(WebhookRequest requestObj) + public async Task> Delete(string webhookId) { - var route = $"{WebhookRoute}/mobilePhoneRecharge"; - return await PostAsync(route, requestObj); + var route = $"{WebhooksRoute}/{webhookId}"; + return await DeleteAsync(route); } - public async Task> FindMobilePhoneRechargeWebhook() + public async Task> RemoveBackoff(string webhookId) { - var route = $"{WebhookRoute}/mobilePhoneRecharge"; - return await GetAsync(route); + var route = $"{WebhooksRoute}/{webhookId}/removeBackoff"; + return await PostAsync(route, new RequestParameters()); } } diff --git a/Codout.Apis.Asaas/Models/Anticipation/Anticipation.cs b/Codout.Apis.Asaas/Models/Anticipation/Anticipation.cs index 645e86e..4b188b2 100644 --- a/Codout.Apis.Asaas/Models/Anticipation/Anticipation.cs +++ b/Codout.Apis.Asaas/Models/Anticipation/Anticipation.cs @@ -1,37 +1,38 @@ -using System; +using System; using System.Text.Json.Serialization; using Codout.Apis.Asaas.Models.Anticipation.Enums; -namespace Codout.Apis.Asaas.Models.Anticipation +namespace Codout.Apis.Asaas.Models.Anticipation; + +public class Anticipation { - public class Anticipation - { - public string Id { get; set; } + public string Object { get; set; } + + public string Id { get; set; } - [JsonPropertyName("installment")] - public string InstallmentId { get; set; } + [JsonPropertyName("installment")] + public string InstallmentId { get; set; } - [JsonPropertyName("payment")] - public string PaymentId { get; set; } + [JsonPropertyName("payment")] + public string PaymentId { get; set; } - public AnticipationStatus Status { get; set; } + public AnticipationStatus Status { get; set; } - public DateTime AnticipationDate { get; set; } + public DateTime? AnticipationDate { get; set; } - public DateTime DueDate { get; set; } + public DateTime? DueDate { get; set; } - public DateTime RequestDate { get; set; } + public DateTime? RequestDate { get; set; } - public int AnticipationDays { get; set; } + public int AnticipationDays { get; set; } - public decimal TotalValue { get; set; } + public decimal TotalValue { get; set; } - public decimal Fee { get; set; } + public decimal Fee { get; set; } - public decimal NetValue { get; set; } + public decimal NetValue { get; set; } - public decimal Value { get; set; } + public decimal Value { get; set; } - public string DenialObservation { get; set; } - } + public string DenialObservation { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Anticipation/AnticipationLimits.cs b/Codout.Apis.Asaas/Models/Anticipation/AnticipationLimits.cs new file mode 100644 index 0000000..296dac7 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Anticipation/AnticipationLimits.cs @@ -0,0 +1,14 @@ +namespace Codout.Apis.Asaas.Models.Anticipation; + +public class AnticipationLimits +{ + public AnticipationLimitsItem BankSlip { get; set; } + public AnticipationLimitsItem CreditCard { get; set; } +} + +public class AnticipationLimitsItem +{ + public decimal Total { get; set; } + public decimal Available { get; set; } + public decimal Used { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Anticipation/AutomaticAnticipationConfig.cs b/Codout.Apis.Asaas/Models/Anticipation/AutomaticAnticipationConfig.cs new file mode 100644 index 0000000..88e442c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Anticipation/AutomaticAnticipationConfig.cs @@ -0,0 +1,13 @@ +namespace Codout.Apis.Asaas.Models.Anticipation; + +public class AutomaticAnticipationConfig +{ + public bool BankSlipEnabled { get; set; } + public bool CreditCardEnabled { get; set; } +} + +public class UpdateAutomaticAnticipationConfigRequest +{ + public bool? BankSlipEnabled { get; set; } + public bool? CreditCardEnabled { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Anticipation/Enums/AnticipationStatus.cs b/Codout.Apis.Asaas/Models/Anticipation/Enums/AnticipationStatus.cs index eb6b1c7..3c13151 100644 --- a/Codout.Apis.Asaas/Models/Anticipation/Enums/AnticipationStatus.cs +++ b/Codout.Apis.Asaas/Models/Anticipation/Enums/AnticipationStatus.cs @@ -11,7 +11,7 @@ public enum AnticipationStatus SCHEDULED } - public static class ReceivableAnticipationStatusExtension + public static class AnticipationStatusExtension { public static bool IsPending(this AnticipationStatus receivableAnticipationStatus) { diff --git a/Codout.Apis.Asaas/Models/Anticipation/SignAnticipationAgreementRequest.cs b/Codout.Apis.Asaas/Models/Anticipation/SignAnticipationAgreementRequest.cs deleted file mode 100644 index f991952..0000000 --- a/Codout.Apis.Asaas/Models/Anticipation/SignAnticipationAgreementRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Codout.Apis.Asaas.Models.Anticipation; - -public class SignAnticipationAgreementRequest -{ - public bool Agreed { get; set; } -} diff --git a/Codout.Apis.Asaas/Models/AsaasAccount/AccessToken.cs b/Codout.Apis.Asaas/Models/AsaasAccount/AccessToken.cs new file mode 100644 index 0000000..33bc574 --- /dev/null +++ b/Codout.Apis.Asaas/Models/AsaasAccount/AccessToken.cs @@ -0,0 +1,14 @@ +using System; + +namespace Codout.Apis.Asaas.Models.AsaasAccount; + +public class AccessToken +{ + public string Id { get; set; } + public string Name { get; set; } + public string Token { get; set; } + public string ApiKey { get; set; } + public DateTime? CreationDate { get; set; } + public DateTime? ExpirationDate { get; set; } + public bool Enabled { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/AsaasAccount/Account.cs b/Codout.Apis.Asaas/Models/AsaasAccount/Account.cs index c2399a4..dd6364f 100644 --- a/Codout.Apis.Asaas/Models/AsaasAccount/Account.cs +++ b/Codout.Apis.Asaas/Models/AsaasAccount/Account.cs @@ -1,43 +1,66 @@ -using Codout.Apis.Asaas.Models.Common.Enums; +using System; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.MyAccount; -namespace Codout.Apis.Asaas.Models.AsaasAccount +namespace Codout.Apis.Asaas.Models.AsaasAccount; + +public class Account { - public class Account - { - public string Name { get; set; } + public string Object { get; set; } + + public string Id { get; set; } + + public string Name { get; set; } + + public string Email { get; set; } + + public string LoginEmail { get; set; } + + public string Phone { get; set; } + + public string MobilePhone { get; set; } + + public string Address { get; set; } - public string Email { get; set; } + public string AddressNumber { get; set; } - public string LoginEmail { get; set; } + public string Complement { get; set; } - public string CpfCnpj { get; set; } + public string Province { get; set; } - public CompanyType? CompanyType { get; set; } + public string PostalCode { get; set; } - public string Phone { get; set; } + public string CpfCnpj { get; set; } - public string MobilePhone { get; set; } + public DateTime? BirthDate { get; set; } - public string Address { get; set; } + public PersonType? PersonType { get; set; } - public string AddressNumber { get; set; } + public CompanyType? CompanyType { get; set; } - public string Complement { get; set; } + /// + /// Schema oficial: integer (city id). Antes era string. + /// + public long? City { get; set; } - public string Province { get; set; } + public string State { get; set; } - public string PostalCode { get; set; } + public string Country { get; set; } - public PersonType? PersonType { get; set; } + public string TradingName { get; set; } - public string City { get; set; } + public string Site { get; set; } - public string State { get; set; } + public string WalletId { get; set; } - public string Country { get; set; } + public AccountNumber AccountNumber { get; set; } - public string ApiKey { get; set; } + public CommercialInfoExpiration CommercialInfoExpiration { get; set; } - public string WalletId { get; set; } - } + /// + /// Mantido por backwards-compat. NAO existe na response schema oficial + /// (apenas retornado em raros endpoints legados). Pode vir null. + /// + [Obsolete("Nao existe no schema AccountGetResponseDTO.")] + public string ApiKey { get; set; } } diff --git a/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccessTokenRequest.cs b/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccessTokenRequest.cs new file mode 100644 index 0000000..94b8067 --- /dev/null +++ b/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccessTokenRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Codout.Apis.Asaas.Models.AsaasAccount; + +public class CreateAccessTokenRequest +{ + public string Name { get; set; } + public DateTime? ExpirationDate { get; set; } + public bool? Enabled { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccountRequest.cs b/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccountRequest.cs index e8f6005..3649a8c 100644 --- a/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccountRequest.cs +++ b/Codout.Apis.Asaas/Models/AsaasAccount/CreateAccountRequest.cs @@ -1,31 +1,30 @@ -using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Common.Enums; -namespace Codout.Apis.Asaas.Models.AsaasAccount +namespace Codout.Apis.Asaas.Models.AsaasAccount; + +public class CreateAccountRequest { - public class CreateAccountRequest - { - public string Name { get; set; } + public string Name { get; set; } - public string Email { get; set; } + public string Email { get; set; } - public string LoginEmail { get; set; } + public string LoginEmail { get; set; } - public string CpfCnpj { get; set; } + public string CpfCnpj { get; set; } - public CompanyType? CompanyType { get; set; } + public CompanyType? CompanyType { get; set; } - public string Phone { get; set; } + public string Phone { get; set; } - public string MobilePhone { get; set; } + public string MobilePhone { get; set; } - public string Address { get; set; } + public string Address { get; set; } - public string AddressNumber { get; set; } + public string AddressNumber { get; set; } - public string Complement { get; set; } + public string Complement { get; set; } - public string Province { get; set; } + public string Province { get; set; } - public string PostalCode { get; set; } - } + public string PostalCode { get; set; } } diff --git a/Codout.Apis.Asaas/Models/AsaasAccount/UpdateAccessTokenRequest.cs b/Codout.Apis.Asaas/Models/AsaasAccount/UpdateAccessTokenRequest.cs new file mode 100644 index 0000000..de62425 --- /dev/null +++ b/Codout.Apis.Asaas/Models/AsaasAccount/UpdateAccessTokenRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Codout.Apis.Asaas.Models.AsaasAccount; + +public class UpdateAccessTokenRequest +{ + public string Name { get; set; } + public DateTime? ExpirationDate { get; set; } + public bool? Enabled { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Bill/BankSlipInfo.cs b/Codout.Apis.Asaas/Models/Bill/BankSlipInfo.cs index d781ffd..b9c8bc9 100644 --- a/Codout.Apis.Asaas/Models/Bill/BankSlipInfo.cs +++ b/Codout.Apis.Asaas/Models/Bill/BankSlipInfo.cs @@ -1,17 +1,43 @@ -using System; +using System; -namespace Codout.Apis.Asaas.Models.Bill +namespace Codout.Apis.Asaas.Models.Bill; + +public class BankSlipInfo { - public class BankSlipInfo - { - public string IdentificationField { get; set; } + public string IdentificationField { get; set; } + + public decimal Value { get; set; } + + public DateTime? DueDate { get; set; } + + public string CompanyName { get; set; } + + /// + /// Schema oficial: "bank" (codigo do banco). Antes era "bankCode" (inventado). + /// + public string Bank { get; set; } + + public string BeneficiaryCpfCnpj { get; set; } + + public string BeneficiaryName { get; set; } + + public bool AllowChangeValue { get; set; } + + public decimal MinValue { get; set; } + + public decimal MaxValue { get; set; } + + public decimal DiscountValue { get; set; } + + public decimal InterestValue { get; set; } + + public decimal FineValue { get; set; } - public decimal Value { get; set; } + public decimal OriginalValue { get; set; } - public DateTime DueDate { get; set; } + public decimal TotalDiscountValue { get; set; } - public string CompanyName { get; set; } + public decimal TotalAdditionalValue { get; set; } - public string BankCode { get; set; } - } + public bool IsOverdue { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Bill/BillPayment.cs b/Codout.Apis.Asaas/Models/Bill/BillPayment.cs index d03c9d4..48c25ab 100644 --- a/Codout.Apis.Asaas/Models/Bill/BillPayment.cs +++ b/Codout.Apis.Asaas/Models/Bill/BillPayment.cs @@ -1,34 +1,45 @@ -using System; +using System; +using System.Collections.Generic; using Codout.Apis.Asaas.Models.Bill.Enums; -namespace Codout.Apis.Asaas.Models.Bill +namespace Codout.Apis.Asaas.Models.Bill; + +public class BillPayment { - public class BillPayment - { - public string Id { get; set; } + public string Id { get; set; } + + public BillPaymentStatus Status { get; set; } + + public decimal Value { get; set; } + + public decimal Discount { get; set; } + + public decimal Interest { get; set; } - public BillPaymentStatus Status { get; set; } + public decimal Fine { get; set; } - public decimal Value { get; set; } + public string IdentificationField { get; set; } - public decimal Discount { get; set; } + public DateTime? DueDate { get; set; } - public string IdentificationField { get; set; } + public DateTime? ScheduleDate { get; set; } - public DateTime DueDate { get; set; } + public DateTime? PaymentDate { get; set; } - public DateTime ScheduleDate { get; set; } + public decimal Fee { get; set; } - public decimal Fee { get; set; } + public string Description { get; set; } - public string Description { get; set; } + public string CompanyName { get; set; } - public string CompanyName { get; set; } + public string TransactionReceiptUrl { get; set; } - public string TransactionReceiptUrl { get; set; } + public bool? CanBeCancelled { get; set; } - public bool CanBeCancelled { get; set; } + public string ExternalReference { get; set; } - public string FailReasons { get; set; } - } + /// + /// Schema retorna array de strings com os motivos da falha. + /// + public List FailReasons { get; set; } = []; } diff --git a/Codout.Apis.Asaas/Models/Bill/CreateBillPaymentRequest.cs b/Codout.Apis.Asaas/Models/Bill/CreateBillPaymentRequest.cs index 8b199ec..7a5ec84 100644 --- a/Codout.Apis.Asaas/Models/Bill/CreateBillPaymentRequest.cs +++ b/Codout.Apis.Asaas/Models/Bill/CreateBillPaymentRequest.cs @@ -1,19 +1,24 @@ -using System; +using System; -namespace Codout.Apis.Asaas.Models.Bill +namespace Codout.Apis.Asaas.Models.Bill; + +public class CreateBillPaymentRequest { - public class CreateBillPaymentRequest - { - public string IdentificationField { get; set; } + public string IdentificationField { get; set; } + + public DateTime? ScheduleDate { get; set; } + + public string Description { get; set; } + + public decimal? Discount { get; set; } - public DateTime ScheduleDate { get; set; } + public decimal? Interest { get; set; } - public string Description { get; set; } + public decimal? Fine { get; set; } - public decimal Discount { get; set; } + public DateTime? DueDate { get; set; } - public DateTime DueDate { get; set; } + public decimal? Value { get; set; } - public decimal Value { get; set; } - } + public string ExternalReference { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Bill/Enums/BillPaymentStatus.cs b/Codout.Apis.Asaas/Models/Bill/Enums/BillPaymentStatus.cs index 694605c..5d32813 100644 --- a/Codout.Apis.Asaas/Models/Bill/Enums/BillPaymentStatus.cs +++ b/Codout.Apis.Asaas/Models/Bill/Enums/BillPaymentStatus.cs @@ -1,39 +1,22 @@ -namespace Codout.Apis.Asaas.Models.Bill.Enums -{ - public enum BillPaymentStatus - { - PENDING, - BANK_PROCESSING, - PAID, - FAILED, - CANCELLED - } - - public static class BillPaymentStatusExtension - { - public static bool IsPending(this BillPaymentStatus status) - { - return status == BillPaymentStatus.PENDING; - } - - public static bool IsBankProcessing(this BillPaymentStatus status) - { - return status == BillPaymentStatus.BANK_PROCESSING; - } - - public static bool IsPaid(this BillPaymentStatus status) - { - return status == BillPaymentStatus.PAID; - } +namespace Codout.Apis.Asaas.Models.Bill.Enums; - public static bool IsFailed(this BillPaymentStatus status) - { - return status == BillPaymentStatus.FAILED; - } +public enum BillPaymentStatus +{ + PENDING, + BANK_PROCESSING, + PAID, + FAILED, + CANCELLED, + REFUNDED, + AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST +} - public static bool IsCancelled(this BillPaymentStatus status) - { - return status == BillPaymentStatus.CANCELLED; - } - } +public static class BillPaymentStatusExtension +{ + public static bool IsPending(this BillPaymentStatus status) => status == BillPaymentStatus.PENDING; + public static bool IsBankProcessing(this BillPaymentStatus status) => status == BillPaymentStatus.BANK_PROCESSING; + public static bool IsPaid(this BillPaymentStatus status) => status == BillPaymentStatus.PAID; + public static bool IsFailed(this BillPaymentStatus status) => status == BillPaymentStatus.FAILED; + public static bool IsCancelled(this BillPaymentStatus status) => status == BillPaymentStatus.CANCELLED; + public static bool IsRefunded(this BillPaymentStatus status) => status == BillPaymentStatus.REFUNDED; } diff --git a/Codout.Apis.Asaas/Models/Chargeback/Chargeback.cs b/Codout.Apis.Asaas/Models/Chargeback/Chargeback.cs new file mode 100644 index 0000000..3bb21a8 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/Chargeback.cs @@ -0,0 +1,35 @@ +using System; +using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Models.Chargeback.Enums; + +namespace Codout.Apis.Asaas.Models.Chargeback; + +public class Chargeback +{ + public string Id { get; set; } + + [JsonPropertyName("payment")] + public string PaymentId { get; set; } + + [JsonPropertyName("installment")] + public string InstallmentId { get; set; } + + [JsonPropertyName("customerAccount")] + public string CustomerAccountId { get; set; } + + public ChargebackStatus Status { get; set; } + + public ChargebackReason? Reason { get; set; } + + public DateTime? DisputeStartDate { get; set; } + + public decimal Value { get; set; } + + public DateTime? PaymentDate { get; set; } + + public ChargebackCreditCard CreditCard { get; set; } + + public ChargebackDisputeStatus? DisputeStatus { get; set; } + + public DateTime? DeadlineToSendDisputeDocuments { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Chargeback/ChargebackCreditCard.cs b/Codout.Apis.Asaas/Models/Chargeback/ChargebackCreditCard.cs new file mode 100644 index 0000000..a50d018 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/ChargebackCreditCard.cs @@ -0,0 +1,9 @@ +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Models.Chargeback; + +public class ChargebackCreditCard +{ + public string Number { get; set; } + public CreditCardBrand? Brand { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Chargeback/CreateChargebackDisputeRequest.cs b/Codout.Apis.Asaas/Models/Chargeback/CreateChargebackDisputeRequest.cs new file mode 100644 index 0000000..7d9d1f9 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/CreateChargebackDisputeRequest.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Core.Interfaces; + +namespace Codout.Apis.Asaas.Models.Chargeback; + +public class CreateChargebackDisputeRequest +{ + public string Description { get; set; } + public List Documents { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackDisputeStatus.cs b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackDisputeStatus.cs new file mode 100644 index 0000000..99698e7 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackDisputeStatus.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Chargeback.Enums; + +public enum ChargebackDisputeStatus +{ + REQUESTED, + ACCEPTED, + REJECTED +} diff --git a/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackReason.cs b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackReason.cs new file mode 100644 index 0000000..86a5345 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackReason.cs @@ -0,0 +1,37 @@ +namespace Codout.Apis.Asaas.Models.Chargeback.Enums; + +public enum ChargebackReason +{ + ABSENCE_OF_PRINT, + ABSENT_CARD_FRAUD, + CARD_ACTIVATED_PHONE_TRANSACTION, + CARD_FRAUD, + CARD_RECOVERY_BULLETIN, + COMMERCIAL_DISAGREEMENT, + COPY_NOT_RECEIVED, + CREDIT_OR_DEBIT_PRESENTATION_ERROR, + DIFFERENT_PAY_METHOD, + FRAUD, + INCORRECT_TRANSACTION_VALUE, + INVALID_CURRENCY, + INVALID_DATA, + LATE_PRESENTATION, + LOCAL_REGULATORY_OR_LEGAL_DISPUTE, + MULTIPLE_ROCS, + ORIGINAL_CREDIT_TRANSACTION_NOT_ACCEPTED, + OTHER_ABSENT_CARD_FRAUD, + PROCESS_ERROR, + RECEIVED_COPY_ILLEGIBLE_OR_INCOMPLETE, + RECURRENCE_CANCELED, + REQUIRED_AUTHORIZATION_NOT_GRANTED, + RIGHT_OF_FULL_RECOURSE_FOR_FRAUD, + SALE_CANCELED, + SERVICE_DISAGREEMENT_OR_DEFECTIVE_PRODUCT, + SERVICE_NOT_RECEIVED, + SPLIT_SALE, + TRANSFERS_OF_DIVERSE_RESPONSIBILITIES, + UNQUALIFIED_CAR_RENTAL_DEBIT, + USA_CARDHOLDER_DISPUTE, + VISA_FRAUD_MONITORING_PROGRAM, + WARNING_BULLETIN_FILE +} diff --git a/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackStatus.cs b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackStatus.cs new file mode 100644 index 0000000..1c18597 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Chargeback/Enums/ChargebackStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.Chargeback.Enums; + +public enum ChargebackStatus +{ + REQUESTED, + IN_DISPUTE, + DISPUTE_LOST, + REVERSED, + DONE +} diff --git a/Codout.Apis.Asaas/Models/Checkout/Checkout.cs b/Codout.Apis.Asaas/Models/Checkout/Checkout.cs new file mode 100644 index 0000000..3c887da --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/Checkout.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Checkout.Enums; + +namespace Codout.Apis.Asaas.Models.Checkout; + +public class Checkout +{ + public string Id { get; set; } + public string Link { get; set; } + public CheckoutStatus Status { get; set; } + public List BillingTypes { get; set; } = []; + public List ChargeTypes { get; set; } = []; + public int? MinutesToExpire { get; set; } + public string ExternalReference { get; set; } + public CheckoutCallback Callback { get; set; } + public List Items { get; set; } = []; + public CheckoutCustomerData CustomerData { get; set; } + public CheckoutSubscription Subscription { get; set; } + public CheckoutInstallment Installment { get; set; } + public List Split { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutCallback.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutCallback.cs new file mode 100644 index 0000000..87540d5 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutCallback.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutCallback +{ + public string SuccessUrl { get; set; } + public string CancelUrl { get; set; } + public string ExpiredUrl { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutCustomerData.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutCustomerData.cs new file mode 100644 index 0000000..1466f55 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutCustomerData.cs @@ -0,0 +1,15 @@ +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutCustomerData +{ + public string Name { get; set; } + public string CpfCnpj { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string Address { get; set; } + public int? AddressNumber { get; set; } + public string Complement { get; set; } + public string Province { get; set; } + public string PostalCode { get; set; } + public int? City { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutInstallment.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutInstallment.cs new file mode 100644 index 0000000..301252d --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutInstallment.cs @@ -0,0 +1,6 @@ +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutInstallment +{ + public int MaxInstallmentCount { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutItem.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutItem.cs new file mode 100644 index 0000000..155576c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutItem.cs @@ -0,0 +1,11 @@ +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutItem +{ + public string ExternalReference { get; set; } + public string Description { get; set; } + public string ImageBase64 { get; set; } + public string Name { get; set; } + public int Quantity { get; set; } + public decimal Value { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutSplit.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutSplit.cs new file mode 100644 index 0000000..5d170c1 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutSplit.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutSplit +{ + public string WalletId { get; set; } + public decimal? FixedValue { get; set; } + public decimal? PercentageValue { get; set; } + public decimal? TotalFixedValue { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CheckoutSubscription.cs b/Codout.Apis.Asaas/Models/Checkout/CheckoutSubscription.cs new file mode 100644 index 0000000..a40b5a7 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CheckoutSubscription.cs @@ -0,0 +1,11 @@ +using System; +using Codout.Apis.Asaas.Models.Subscription.Enums; + +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CheckoutSubscription +{ + public Cycle Cycle { get; set; } + public DateTime? EndDate { get; set; } + public DateTime? NextDueDate { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Checkout/CreateCheckoutRequest.cs b/Codout.Apis.Asaas/Models/Checkout/CreateCheckoutRequest.cs new file mode 100644 index 0000000..0998df5 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/CreateCheckoutRequest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Checkout.Enums; + +namespace Codout.Apis.Asaas.Models.Checkout; + +public class CreateCheckoutRequest +{ + public List BillingTypes { get; set; } = []; + public List ChargeTypes { get; set; } = []; + public int? MinutesToExpire { get; set; } + public string ExternalReference { get; set; } + public CheckoutCallback Callback { get; set; } + public List Items { get; set; } = []; + public CheckoutCustomerData CustomerData { get; set; } + public CheckoutSubscription Subscription { get; set; } + public CheckoutInstallment Installment { get; set; } + // Asaas usa "splits" (plural) no request e "split" (singular) no response. + // Nao "corrigir" essa assimetria — a API e assim por design. + public List Splits { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutBillingType.cs b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutBillingType.cs new file mode 100644 index 0000000..a34d243 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutBillingType.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Checkout.Enums; + +public enum CheckoutBillingType +{ + CREDIT_CARD, + PIX +} diff --git a/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutChargeType.cs b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutChargeType.cs new file mode 100644 index 0000000..c0692b3 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutChargeType.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Checkout.Enums; + +public enum CheckoutChargeType +{ + DETACHED, + RECURRENT, + INSTALLMENT +} diff --git a/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutStatus.cs b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutStatus.cs new file mode 100644 index 0000000..53b5778 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Checkout/Enums/CheckoutStatus.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.Checkout.Enums; + +public enum CheckoutStatus +{ + ACTIVE, + CANCELED, + EXPIRED, + PAID +} diff --git a/Codout.Apis.Asaas/Models/Common/CreditCard.cs b/Codout.Apis.Asaas/Models/Common/CreditCard.cs index fd38728..4628a3f 100644 --- a/Codout.Apis.Asaas/Models/Common/CreditCard.cs +++ b/Codout.Apis.Asaas/Models/Common/CreditCard.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Models.Common.Enums; namespace Codout.Apis.Asaas.Models.Common; @@ -8,7 +9,7 @@ public class CreditCard public string Number { get; set; } [JsonPropertyName("creditCardBrand")] - public string Brand { get; set; } + public CreditCardBrand? Brand { get; set; } [JsonPropertyName("creditCardToken")] public string Token { get; set; } diff --git a/Codout.Apis.Asaas/Models/Common/Enums/CreditCardBrand.cs b/Codout.Apis.Asaas/Models/Common/Enums/CreditCardBrand.cs new file mode 100644 index 0000000..6e3e5e8 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Common/Enums/CreditCardBrand.cs @@ -0,0 +1,21 @@ +namespace Codout.Apis.Asaas.Models.Common.Enums; + +/// +/// Bandeira do cartao usado. Schema oficial: 13 valores. +/// +public enum CreditCardBrand +{ + VISA, + MASTERCARD, + ELO, + DINERS, + DISCOVER, + AMEX, + CABAL, + BANESCARD, + CREDZ, + SOROCRED, + CREDSYSTEM, + JCB, + UNKNOWN +} diff --git a/Codout.Apis.Asaas/Models/Common/PaymentCallback.cs b/Codout.Apis.Asaas/Models/Common/PaymentCallback.cs new file mode 100644 index 0000000..d882f0c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Common/PaymentCallback.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Common; + +public class PaymentCallback +{ + public string SuccessUrl { get; set; } + + public bool? AutoRedirect { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Common/Taxes.cs b/Codout.Apis.Asaas/Models/Common/Taxes.cs index 297a633..5157d9d 100644 --- a/Codout.Apis.Asaas/Models/Common/Taxes.cs +++ b/Codout.Apis.Asaas/Models/Common/Taxes.cs @@ -1,19 +1,34 @@ -namespace Codout.Apis.Asaas.Models.Common -{ - public class Taxes - { - public bool RetainIss { get; set; } - - public decimal Iss { get; set; } +namespace Codout.Apis.Asaas.Models.Common; - public decimal Cofins { get; set; } +/// +/// Modelo unificado para InvoiceTaxesRequestDTO e InvoiceTaxesResponseDTO. +/// Campos required do schema (retainIss, iss, pis, cofins, csll, inss, ir) +/// sao non-nullable. Demais campos sao opcionais (nullable). Os campos da +/// Reforma Tributaria (stateIbs/municipalIbs/cbs) aparecem apenas no response. +/// +public class Taxes +{ + public string NbsCode { get; set; } + public string TaxSituationCode { get; set; } + public string TaxClassificationCode { get; set; } + public string OperationIndicatorCode { get; set; } - public decimal Csll { get; set; } + public bool RetainIss { get; set; } + public decimal Iss { get; set; } - public decimal Inss { get; set; } + public string PisCofinsRetentionType { get; set; } + public string PisCofinsTaxStatus { get; set; } - public decimal Ir { get; set; } + public decimal Pis { get; set; } + public decimal Cofins { get; set; } + public decimal Csll { get; set; } + public decimal Inss { get; set; } + public decimal Ir { get; set; } - public decimal Pis { get; set; } - } + public decimal? StateIbs { get; set; } + public decimal? StateIbsValue { get; set; } + public decimal? MunicipalIbs { get; set; } + public decimal? MunicipalIbsValue { get; set; } + public decimal? Cbs { get; set; } + public decimal? CbsValue { get; set; } } diff --git a/Codout.Apis.Asaas/Models/CreditBureauReport/CreateCreditBureauReportRequest.cs b/Codout.Apis.Asaas/Models/CreditBureauReport/CreateCreditBureauReportRequest.cs index 6c2e410..8356ed8 100644 --- a/Codout.Apis.Asaas/Models/CreditBureauReport/CreateCreditBureauReportRequest.cs +++ b/Codout.Apis.Asaas/Models/CreditBureauReport/CreateCreditBureauReportRequest.cs @@ -1,9 +1,7 @@ -namespace Codout.Apis.Asaas.Models.CreditBureauReport +namespace Codout.Apis.Asaas.Models.CreditBureauReport; + +public class CreateCreditBureauReportRequest { - public class CreateCreditBureauReportRequest - { - public string Customer { get; set; } - public string CpfCnpj { get; set; } - public string State { get; set; } - } + public string Customer { get; set; } + public string CpfCnpj { get; set; } } diff --git a/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReport.cs b/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReport.cs index 56e3dab..b2976e9 100644 --- a/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReport.cs +++ b/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReport.cs @@ -1,14 +1,22 @@ using System; -namespace Codout.Apis.Asaas.Models.CreditBureauReport +namespace Codout.Apis.Asaas.Models.CreditBureauReport; + +public class CreditBureauReport { - public class CreditBureauReport - { - public string Id { get; set; } - public string Customer { get; set; } - public string CpfCnpj { get; set; } - public string State { get; set; } - public string Status { get; set; } - public DateTime DateCreated { get; set; } - } + public string Id { get; set; } + + public DateTime? DateCreated { get; set; } + + public string CpfCnpj { get; set; } + + public string Customer { get; set; } + + public string DownloadUrl { get; set; } + + /// + /// PDF do relatorio em Base64. Retornado apenas quando o report e criado + /// (POST). Em GET por id e nos itens do List, vem null. + /// + public string ReportFile { get; set; } } diff --git a/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReportListFilter.cs b/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReportListFilter.cs new file mode 100644 index 0000000..166771a --- /dev/null +++ b/Codout.Apis.Asaas/Models/CreditBureauReport/CreditBureauReportListFilter.cs @@ -0,0 +1,19 @@ +using System; +using Codout.Apis.Asaas.Core; + +namespace Codout.Apis.Asaas.Models.CreditBureauReport; + +public class CreditBureauReportListFilter : RequestParameters +{ + public DateTime? StartDate + { + get => Get("startDate"); + set => Add("startDate", value); + } + + public DateTime? EndDate + { + get => Get("endDate"); + set => Add("endDate", value); + } +} diff --git a/Codout.Apis.Asaas/Models/CreditCard/PreAuthorizationConfig.cs b/Codout.Apis.Asaas/Models/CreditCard/PreAuthorizationConfig.cs new file mode 100644 index 0000000..4c1a544 --- /dev/null +++ b/Codout.Apis.Asaas/Models/CreditCard/PreAuthorizationConfig.cs @@ -0,0 +1,16 @@ +namespace Codout.Apis.Asaas.Models.CreditCard; + +/// +/// Schema oficial: apenas {daysToExpire: int required}. Antes tinha +/// campos inventados (Enabled, AutomaticCaptureDelay) que NAO existem +/// no schema CreditCardPreAuthorizationConfig. +/// +public class PreAuthorizationConfig +{ + public int DaysToExpire { get; set; } +} + +public class SavePreAuthorizationConfigRequest +{ + public int DaysToExpire { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Customer/CreateCustomerRequest.cs b/Codout.Apis.Asaas/Models/Customer/CreateCustomerRequest.cs index 9f6df89..cc7f1ad 100644 --- a/Codout.Apis.Asaas/Models/Customer/CreateCustomerRequest.cs +++ b/Codout.Apis.Asaas/Models/Customer/CreateCustomerRequest.cs @@ -24,7 +24,7 @@ public class CreateCustomerRequest public string ExternalReference { get; set; } - public bool NotificationDisabled { get; set; } + public bool? NotificationDisabled { get; set; } public string AdditionalEmails { get; set; } @@ -35,5 +35,9 @@ public class CreateCustomerRequest public string Observations { get; set; } public string GroupName { get; set; } + + public string Company { get; set; } + + public bool? ForeignCustomer { get; set; } } } diff --git a/Codout.Apis.Asaas/Models/Customer/Customer.cs b/Codout.Apis.Asaas/Models/Customer/Customer.cs index 057933d..57241d3 100644 --- a/Codout.Apis.Asaas/Models/Customer/Customer.cs +++ b/Codout.Apis.Asaas/Models/Customer/Customer.cs @@ -6,9 +6,12 @@ namespace Codout.Apis.Asaas.Models.Customer; public class Customer { + [JsonPropertyName("object")] + public string Object { get; set; } + public string Id { get; set; } - public DateTime DateCreated { get; set; } + public DateTime? DateCreated { get; set; } public string Name { get; set; } @@ -32,22 +35,32 @@ public class Customer public string ExternalReference { get; set; } - public bool NotificationDisabled { get; set; } + public bool? NotificationDisabled { get; set; } public string AdditionalEmails { get; set; } public string MunicipalInscription { get; set; } + public string StateInscription { get; set; } + public PersonType? PersonType { get; set; } - public bool Deleted { get; set; } + public bool? Deleted { get; set; } [JsonPropertyName("city")] public long? CityId { get; set; } + public string CityName { get; set; } + public string State { get; set; } public string Country { get; set; } public string Observations { get; set; } + + public string Company { get; set; } + + public string GroupName { get; set; } + + public bool? ForeignCustomer { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Customer/UpdateCustomerRequest.cs b/Codout.Apis.Asaas/Models/Customer/UpdateCustomerRequest.cs index 535a955..4e910a4 100644 --- a/Codout.Apis.Asaas/Models/Customer/UpdateCustomerRequest.cs +++ b/Codout.Apis.Asaas/Models/Customer/UpdateCustomerRequest.cs @@ -24,7 +24,7 @@ public class UpdateCustomerRequest public string ExternalReference { get; set; } - public bool NotificationDisabled { get; set; } + public bool? NotificationDisabled { get; set; } public string AdditionalEmails { get; set; } @@ -33,5 +33,11 @@ public class UpdateCustomerRequest public string StateInscription { get; set; } public string Observations { get; set; } + + public string GroupName { get; set; } + + public string Company { get; set; } + + public bool? ForeignCustomer { get; set; } } } diff --git a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/CustomerFiscalInfo.cs b/Codout.Apis.Asaas/Models/CustomerFiscalInfo/CustomerFiscalInfo.cs deleted file mode 100644 index 87830f8..0000000 --- a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/CustomerFiscalInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Codout.Apis.Asaas.Models.CustomerFiscalInfo -{ - public class CustomerFiscalInfo - { - public string Email { get; set; } - public string MunicipalInscription { get; set; } - public string StateInscription { get; set; } - public bool SimplesNacional { get; set; } - public bool CulturalProjectsPromoter { get; set; } - public string Cnae { get; set; } - public string SpecialTaxRegime { get; set; } - public string ServiceListItem { get; set; } - public string RpsSerie { get; set; } - public string RpsNumber { get; set; } - public string LoteNumber { get; set; } - public string Username { get; set; } - public string AccessToken { get; set; } - } -} diff --git a/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowFinishReason.cs b/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowFinishReason.cs new file mode 100644 index 0000000..7c973fa --- /dev/null +++ b/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowFinishReason.cs @@ -0,0 +1,11 @@ +namespace Codout.Apis.Asaas.Models.Escrow.Enums; + +public enum EscrowFinishReason +{ + CHARGEBACK, + EXPIRED, + INSUFFICIENT_BALANCE, + PAYMENT_REFUNDED, + REQUESTED_BY_CUSTOMER, + CUSTOMER_CONFIG_DISABLED +} diff --git a/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowStatus.cs b/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowStatus.cs new file mode 100644 index 0000000..066bcab --- /dev/null +++ b/Codout.Apis.Asaas/Models/Escrow/Enums/EscrowStatus.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Escrow.Enums; + +public enum EscrowStatus +{ + ACTIVE, + DONE +} diff --git a/Codout.Apis.Asaas/Models/Escrow/Escrow.cs b/Codout.Apis.Asaas/Models/Escrow/Escrow.cs new file mode 100644 index 0000000..d6b39c1 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Escrow/Escrow.cs @@ -0,0 +1,13 @@ +using System; +using Codout.Apis.Asaas.Models.Escrow.Enums; + +namespace Codout.Apis.Asaas.Models.Escrow; + +public class Escrow +{ + public string Id { get; set; } + public EscrowStatus Status { get; set; } + public DateTime? ExpirationDate { get; set; } + public DateTime? FinishDate { get; set; } + public EscrowFinishReason? FinishReason { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Escrow/EscrowConfig.cs b/Codout.Apis.Asaas/Models/Escrow/EscrowConfig.cs new file mode 100644 index 0000000..38e3d9d --- /dev/null +++ b/Codout.Apis.Asaas/Models/Escrow/EscrowConfig.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Escrow; + +public class EscrowConfig +{ + public int DaysToExpire { get; set; } + public bool? Enabled { get; set; } + public bool? IsFeePayer { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Escrow/SaveEscrowConfigRequest.cs b/Codout.Apis.Asaas/Models/Escrow/SaveEscrowConfigRequest.cs new file mode 100644 index 0000000..e849bdd --- /dev/null +++ b/Codout.Apis.Asaas/Models/Escrow/SaveEscrowConfigRequest.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Escrow; + +public class SaveEscrowConfigRequest +{ + public int DaysToExpire { get; set; } + public bool? Enabled { get; set; } + public bool? IsFeePayer { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Finance/Balance.cs b/Codout.Apis.Asaas/Models/Finance/Balance.cs new file mode 100644 index 0000000..422dea3 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Finance/Balance.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Codout.Apis.Asaas.Models.Finance; + +public class Balance +{ + [JsonPropertyName("balance")] + public decimal Value { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Finance/PaymentStatisticsFilter.cs b/Codout.Apis.Asaas/Models/Finance/PaymentStatisticsFilter.cs new file mode 100644 index 0000000..59018b2 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Finance/PaymentStatisticsFilter.cs @@ -0,0 +1,79 @@ +using System; +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Payment.Enums; + +namespace Codout.Apis.Asaas.Models.Finance; + +/// +/// Filtros para GET /v3/finance/payment/statistics. +/// Antes a chamada nao aceitava filtros — schema oficial expoe 11. +/// +public class PaymentStatisticsFilter : RequestParameters +{ + public string CustomerId + { + get => this["customer"]; + set => Add("customer", value); + } + + public BillingType? BillingType + { + get => Get("billingType"); + set => Add("billingType", value); + } + + public PaymentStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + + public bool? Anticipated + { + get => Get("anticipated"); + set => Add("anticipated", value); + } + + public DateTime? DateCreatedGE + { + get => Get("dateCreated[ge]"); + set => Add("dateCreated[ge]", value); + } + + public DateTime? DateCreatedLE + { + get => Get("dateCreated[le]"); + set => Add("dateCreated[le]", value); + } + + public DateTime? DueDateGE + { + get => Get("dueDate[ge]"); + set => Add("dueDate[ge]", value); + } + + public DateTime? DueDateLE + { + get => Get("dueDate[le]"); + set => Add("dueDate[le]", value); + } + + public DateTime? EstimatedCreditDateGE + { + get => Get("estimatedCreditDate[ge]"); + set => Add("estimatedCreditDate[ge]", value); + } + + public DateTime? EstimatedCreditDateLE + { + get => Get("estimatedCreditDate[le]"); + set => Add("estimatedCreditDate[le]", value); + } + + public string ExternalReference + { + get => this["externalReference"]; + set => Add("externalReference", value); + } +} diff --git a/Codout.Apis.Asaas/Models/Finance/SplitStatistics.cs b/Codout.Apis.Asaas/Models/Finance/SplitStatistics.cs index 11e4015..172984c 100644 --- a/Codout.Apis.Asaas/Models/Finance/SplitStatistics.cs +++ b/Codout.Apis.Asaas/Models/Finance/SplitStatistics.cs @@ -1,7 +1,15 @@ namespace Codout.Apis.Asaas.Models.Finance; +/// +/// Schema oficial: {income: number, value: number}. Antes o modelo tinha +/// {TotalPendingValue, TotalReceivedValue} (inventados — nao existem +/// na FinanceGetSplitStatisticsResponseDTO). +/// public class SplitStatistics { - public decimal TotalPendingValue { get; set; } - public decimal TotalReceivedValue { get; set; } + /// Valores a receber. + public decimal Income { get; set; } + + /// Valores a enviar. + public decimal Value { get; set; } } diff --git a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/CreateCustomerFiscalInfoRequest.cs b/Codout.Apis.Asaas/Models/FiscalInfo/CreateFiscalInfoRequest.cs similarity index 89% rename from Codout.Apis.Asaas/Models/CustomerFiscalInfo/CreateCustomerFiscalInfoRequest.cs rename to Codout.Apis.Asaas/Models/FiscalInfo/CreateFiscalInfoRequest.cs index d5ac4ea..dfc7400 100644 --- a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/CreateCustomerFiscalInfoRequest.cs +++ b/Codout.Apis.Asaas/Models/FiscalInfo/CreateFiscalInfoRequest.cs @@ -1,8 +1,8 @@ using Codout.Apis.Asaas.Core.Interfaces; -namespace Codout.Apis.Asaas.Models.CustomerFiscalInfo +namespace Codout.Apis.Asaas.Models.FiscalInfo { - public class CreateCustomerFiscalInfoRequest + public class CreateFiscalInfoRequest { public string Email { get; set; } public string MunicipalInscription { get; set; } diff --git a/Codout.Apis.Asaas/Models/FiscalInfo/FiscalInfo.cs b/Codout.Apis.Asaas/Models/FiscalInfo/FiscalInfo.cs new file mode 100644 index 0000000..a66f017 --- /dev/null +++ b/Codout.Apis.Asaas/Models/FiscalInfo/FiscalInfo.cs @@ -0,0 +1,41 @@ +using System; + +namespace Codout.Apis.Asaas.Models.FiscalInfo; + +public class FiscalInfo +{ + public string Object { get; set; } + public string Email { get; set; } + public string MunicipalInscription { get; set; } + public bool? SimplesNacional { get; set; } + public bool? CulturalProjectsPromoter { get; set; } + public string Cnae { get; set; } + public string SpecialTaxRegime { get; set; } + public string ServiceListItem { get; set; } + public string NbsCode { get; set; } + public string RpsSerie { get; set; } + + /// Schema oficial: integer (era string). + public int? RpsNumber { get; set; } + + /// Schema oficial: integer (era string). + public int? LoteNumber { get; set; } + + public string Username { get; set; } + + public bool? PasswordSent { get; set; } + + public bool? AccessTokenSent { get; set; } + + public bool? CertificateSent { get; set; } + + public string NationalPortalTaxCalculationRegime { get; set; } + + /// NAO existe no schema. Removido/obsoleto. + [Obsolete("Nao existe no schema FiscalInfoGetResponseDTO.")] + public string StateInscription { get; set; } + + /// Use AccessTokenSent (bool). Campo legado. + [Obsolete("Schema oficial expoe accessTokenSent (bool), nao accessToken (string).")] + public string AccessToken { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/MunicipalOption.cs b/Codout.Apis.Asaas/Models/FiscalInfo/MunicipalOption.cs similarity index 70% rename from Codout.Apis.Asaas/Models/CustomerFiscalInfo/MunicipalOption.cs rename to Codout.Apis.Asaas/Models/FiscalInfo/MunicipalOption.cs index 7bd4a08..1cc8ae9 100644 --- a/Codout.Apis.Asaas/Models/CustomerFiscalInfo/MunicipalOption.cs +++ b/Codout.Apis.Asaas/Models/FiscalInfo/MunicipalOption.cs @@ -1,4 +1,4 @@ -namespace Codout.Apis.Asaas.Models.CustomerFiscalInfo +namespace Codout.Apis.Asaas.Models.FiscalInfo { public class MunicipalOption { diff --git a/Codout.Apis.Asaas/Models/Invoice/MunicipalService.cs b/Codout.Apis.Asaas/Models/FiscalInfo/MunicipalService.cs similarity index 60% rename from Codout.Apis.Asaas/Models/Invoice/MunicipalService.cs rename to Codout.Apis.Asaas/Models/FiscalInfo/MunicipalService.cs index 1666a7b..004b63a 100644 --- a/Codout.Apis.Asaas/Models/Invoice/MunicipalService.cs +++ b/Codout.Apis.Asaas/Models/FiscalInfo/MunicipalService.cs @@ -1,4 +1,4 @@ -namespace Codout.Apis.Asaas.Models.Invoice +namespace Codout.Apis.Asaas.Models.FiscalInfo { public class MunicipalService { @@ -6,6 +6,6 @@ public class MunicipalService public string Description { get; set; } - public decimal Iss { get; set; } + public decimal IssTax { get; set; } } } diff --git a/Codout.Apis.Asaas/Models/Installment/CreateInstallmentRequest.cs b/Codout.Apis.Asaas/Models/Installment/CreateInstallmentRequest.cs new file mode 100644 index 0000000..ca2d4b5 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Installment/CreateInstallmentRequest.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Models.Common; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Models.Installment; + +public class CreateInstallmentRequest +{ + public int InstallmentCount { get; set; } + + [JsonPropertyName("customer")] + public string CustomerId { get; set; } + + public decimal Value { get; set; } + + public decimal? TotalValue { get; set; } + + public BillingType BillingType { get; set; } + + public DateTime DueDate { get; set; } + + public string Description { get; set; } + + public bool? PostalService { get; set; } + + public int? DaysAfterDueDateToRegistrationCancellation { get; set; } + + public string PaymentExternalReference { get; set; } + + public Discount Discount { get; set; } + + public Interest Interest { get; set; } + + public Fine Fine { get; set; } + + public List Splits { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Installment/CreateInstallmentWithCreditCardRequest.cs b/Codout.Apis.Asaas/Models/Installment/CreateInstallmentWithCreditCardRequest.cs new file mode 100644 index 0000000..de9591e --- /dev/null +++ b/Codout.Apis.Asaas/Models/Installment/CreateInstallmentWithCreditCardRequest.cs @@ -0,0 +1,14 @@ +using Codout.Apis.Asaas.Models.Common; + +namespace Codout.Apis.Asaas.Models.Installment; + +public class CreateInstallmentWithCreditCardRequest : CreateInstallmentRequest +{ + public CreditCardRequest CreditCard { get; set; } + + public CreditCardHolderInfoRequest CreditCardHolderInfo { get; set; } + + public string CreditCardToken { get; set; } + + public string RemoteIp { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Installment/Installment.cs b/Codout.Apis.Asaas/Models/Installment/Installment.cs index 78d6e60..dcc4e96 100644 --- a/Codout.Apis.Asaas/Models/Installment/Installment.cs +++ b/Codout.Apis.Asaas/Models/Installment/Installment.cs @@ -1,11 +1,16 @@ -using System; +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Models.Common; using Codout.Apis.Asaas.Models.Common.Enums; namespace Codout.Apis.Asaas.Models.Installment; public class Installment { + [JsonPropertyName("object")] + public string Object { get; set; } + [JsonPropertyName("id")] public string Id { get; set; } @@ -31,17 +36,44 @@ public class Installment public string Description { get; set; } [JsonPropertyName("expirationDay")] - public int ExpirationDay { get; set; } + public int? ExpirationDay { get; set; } + + [JsonPropertyName("dateCreated")] + public DateTime? DateCreated { get; set; } [JsonPropertyName("customer")] public string CustomerId { get; set; } [JsonPropertyName("deleted")] - public bool Deleted { get; set; } + public bool? Deleted { get; set; } [JsonPropertyName("paymentLink")] public string PaymentLink { get; set; } + [JsonPropertyName("checkoutSession")] + public string CheckoutSession { get; set; } + [JsonPropertyName("transactionReceiptUrl")] public string TransactionReceiptUrl { get; set; } + + [JsonPropertyName("creditCard")] + public Common.CreditCard CreditCard { get; set; } + + /// + /// Schema retorna array de refunds (mesma shape do PaymentRefund + paymentId). + /// + [JsonPropertyName("refunds")] + public List Refunds { get; set; } = []; +} + +public class InstallmentRefund +{ + public DateTime? DateCreated { get; set; } + public string Status { get; set; } + public decimal Value { get; set; } + public string EndToEndIdentifier { get; set; } + public string Description { get; set; } + public DateTime? EffectiveDate { get; set; } + public string TransactionReceiptUrl { get; set; } + public string PaymentId { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Installment/InstallmentSplitRequest.cs b/Codout.Apis.Asaas/Models/Installment/InstallmentSplitRequest.cs new file mode 100644 index 0000000..c23c92c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Installment/InstallmentSplitRequest.cs @@ -0,0 +1,18 @@ +namespace Codout.Apis.Asaas.Models.Installment; + +public class InstallmentSplitRequest +{ + public string WalletId { get; set; } + + public decimal? FixedValue { get; set; } + + public decimal? PercentualValue { get; set; } + + public decimal? TotalFixedValue { get; set; } + + public string ExternalReference { get; set; } + + public string Description { get; set; } + + public int? InstallmentNumber { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Installment/UpdateInstallmentSplitsRequest.cs b/Codout.Apis.Asaas/Models/Installment/UpdateInstallmentSplitsRequest.cs new file mode 100644 index 0000000..434eb68 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Installment/UpdateInstallmentSplitsRequest.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Codout.Apis.Asaas.Models.Installment; + +public class UpdateInstallmentSplitsRequest +{ + public List Splits { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Invoice/CreateInvoiceRequest.cs b/Codout.Apis.Asaas/Models/Invoice/CreateInvoiceRequest.cs index 4fd25d4..37d0328 100644 --- a/Codout.Apis.Asaas/Models/Invoice/CreateInvoiceRequest.cs +++ b/Codout.Apis.Asaas/Models/Invoice/CreateInvoiceRequest.cs @@ -1,38 +1,39 @@ -using System; +using System; using System.Text.Json.Serialization; using Codout.Apis.Asaas.Models.Common; -namespace Codout.Apis.Asaas.Models.Invoice +namespace Codout.Apis.Asaas.Models.Invoice; + +public class CreateInvoiceRequest { - public class CreateInvoiceRequest - { - [JsonPropertyName("payment")] - public string PaymentId { get; set; } + [JsonPropertyName("payment")] + public string PaymentId { get; set; } + + [JsonPropertyName("installment")] + public string InstallmentId { get; set; } - [JsonPropertyName("installment")] - public string InstallmentId { get; set; } + [JsonPropertyName("customer")] + public string CustomerId { get; set; } - [JsonPropertyName("customer")] - public string CustomerId { get; set; } + public string ServiceDescription { get; set; } - public string ServiceDescription { get; set; } + public string Observations { get; set; } - public string Observations { get; set; } + public decimal Value { get; set; } - public decimal Value { get; set; } + public decimal Deductions { get; set; } - public decimal Deductions { get; set; } + public DateTime EffectiveDate { get; set; } - public DateTime EffectiveDate { get; set; } + public string MunicipalServiceId { get; set; } - public string MunicipalServiceId { get; set; } + public string MunicipalServiceCode { get; set; } - public string MunicipalServiceCode { get; set; } + public string MunicipalServiceName { get; set; } - public string MunicipalServiceName { get; set; } + public string ExternalReference { get; set; } - public string ExternalReference { get; set; } + public bool? UpdatePayment { get; set; } - public Taxes Taxes { get; set; } - } + public Taxes Taxes { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Invoice/InvoiceListFilter.cs b/Codout.Apis.Asaas/Models/Invoice/InvoiceListFilter.cs index 42be9ec..a01fb58 100644 --- a/Codout.Apis.Asaas/Models/Invoice/InvoiceListFilter.cs +++ b/Codout.Apis.Asaas/Models/Invoice/InvoiceListFilter.cs @@ -1,39 +1,52 @@ -using System; +using System; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Models.Invoice.Enums; -namespace Codout.Apis.Asaas.Models.Invoice +namespace Codout.Apis.Asaas.Models.Invoice; + +public class InvoiceListFilter : RequestParameters { - public class InvoiceListFilter : RequestParameters + // Schema oficial usa "effectiveDate[Ge]" e "[Le]" com G/L maiusculos, + // diferente do padrao snake-case usado em outros filtros do Asaas. + public DateTime? EffectiveDateGE + { + get => Get("effectiveDate[Ge]"); + set => Add("effectiveDate[Ge]", value); + } + + public DateTime? EffectiveDateLE + { + get => Get("effectiveDate[Le]"); + set => Add("effectiveDate[Le]", value); + } + + public string PaymentId + { + get => this["payment"]; + set => Add("payment", value); + } + + public string InstallmentId + { + get => this["installment"]; + set => Add("installment", value); + } + + public string CustomerId + { + get => this["customer"]; + set => Add("customer", value); + } + + public string ExternalReference + { + get => this["externalReference"]; + set => Add("externalReference", value); + } + + public InvoiceStatus? Status { - public DateTime? EffectiveDateGE - { - get => Get("effectiveDate[ge]"); - set => Add("effectiveDate[ge]", value); - } - - public DateTime? EffectiveDateLE - { - get => Get("effectiveDate[le]"); - set => Add("effectiveDate[le]", value); - } - - public string PaymentId - { - get => this["payment"]; - set => Add("payment", value); - } - - public string InstallmentId - { - get => this["installment"]; - set => Add("installment", value); - } - - public InvoiceStatus? Status - { - get => Get("status"); - set => Add("status", value); - } + get => Get("status"); + set => Add("status", value); } } diff --git a/Codout.Apis.Asaas/Models/Invoice/UpdateInvoiceRequest.cs b/Codout.Apis.Asaas/Models/Invoice/UpdateInvoiceRequest.cs index f0f86da..7efb023 100644 --- a/Codout.Apis.Asaas/Models/Invoice/UpdateInvoiceRequest.cs +++ b/Codout.Apis.Asaas/Models/Invoice/UpdateInvoiceRequest.cs @@ -1,22 +1,23 @@ -using System; +using System; using Codout.Apis.Asaas.Models.Common; -namespace Codout.Apis.Asaas.Models.Invoice +namespace Codout.Apis.Asaas.Models.Invoice; + +public class UpdateInvoiceRequest { - public class UpdateInvoiceRequest - { - public string ServiceDescription { get; set; } + public string ServiceDescription { get; set; } + + public string Observations { get; set; } - public string Observations { get; set; } + public decimal? Value { get; set; } - public decimal? Value { get; set; } + public decimal? Deductions { get; set; } - public decimal? Deductions { get; set; } + public DateTime? EffectiveDate { get; set; } - public DateTime? EffectiveDate { get; set; } + public string ExternalReference { get; set; } - public string ExternalReference { get; set; } + public bool? UpdatePayment { get; set; } - public Taxes Taxes { get; set; } - } + public Taxes Taxes { get; set; } } diff --git a/Codout.Apis.Asaas/Models/MobilePhoneRecharge/CreateMobilePhoneRechargeRequest.cs b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/CreateMobilePhoneRechargeRequest.cs new file mode 100644 index 0000000..7ee8a70 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/CreateMobilePhoneRechargeRequest.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.MobilePhoneRecharge; + +public class CreateMobilePhoneRechargeRequest +{ + public string PhoneNumber { get; set; } + public decimal Value { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MobilePhoneRecharge/Enums/MobilePhoneRechargeStatus.cs b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/Enums/MobilePhoneRechargeStatus.cs new file mode 100644 index 0000000..9287808 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/Enums/MobilePhoneRechargeStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.MobilePhoneRecharge.Enums; + +public enum MobilePhoneRechargeStatus +{ + PENDING, + CONFIRMED, + CANCELLED, + REFUNDED, + WAITING_CRITICAL_ACTION +} diff --git a/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProvider.cs b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProvider.cs new file mode 100644 index 0000000..2967fc9 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Codout.Apis.Asaas.Models.MobilePhoneRecharge; + +public class MobilePhoneProvider +{ + public string Name { get; set; } + public List Values { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProviderValue.cs b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProviderValue.cs new file mode 100644 index 0000000..d99e4aa --- /dev/null +++ b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneProviderValue.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.MobilePhoneRecharge; + +public class MobilePhoneProviderValue +{ + public string Name { get; set; } + public string Description { get; set; } + public string Bonus { get; set; } + public decimal MinValue { get; set; } + public decimal MaxValue { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneRecharge.cs b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneRecharge.cs new file mode 100644 index 0000000..8da21b1 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneRecharge.cs @@ -0,0 +1,13 @@ +using Codout.Apis.Asaas.Models.MobilePhoneRecharge.Enums; + +namespace Codout.Apis.Asaas.Models.MobilePhoneRecharge; + +public class MobilePhoneRecharge +{ + public string Id { get; set; } + public decimal Value { get; set; } + public string PhoneNumber { get; set; } + public MobilePhoneRechargeStatus Status { get; set; } + public bool? CanBeCancelled { get; set; } + public string OperatorName { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/AccountDocument.cs b/Codout.Apis.Asaas/Models/MyAccount/AccountDocument.cs new file mode 100644 index 0000000..e92de86 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/AccountDocument.cs @@ -0,0 +1,9 @@ +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class AccountDocument +{ + public string Id { get; set; } + public AccountDocumentStatus Status { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentGroup.cs b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentGroup.cs new file mode 100644 index 0000000..9b8c4d6 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentGroup.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class AccountDocumentGroup +{ + public string Id { get; set; } + public AccountDocumentGroupStatus Status { get; set; } + public AccountDocumentType Type { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public AccountDocumentResponsible Responsible { get; set; } + public string OnboardingUrl { get; set; } + public DateTime? OnboardingUrlExpirationDate { get; set; } + public List Documents { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponse.cs b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponse.cs new file mode 100644 index 0000000..a7e7566 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class AccountDocumentResponse +{ + public string RejectReasons { get; set; } + public List Data { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponsible.cs b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponsible.cs new file mode 100644 index 0000000..18aa15a --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/AccountDocumentResponsible.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class AccountDocumentResponsible +{ + public string Name { get; set; } + public List Type { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/AccountStatus.cs b/Codout.Apis.Asaas/Models/MyAccount/AccountStatus.cs new file mode 100644 index 0000000..e929440 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/AccountStatus.cs @@ -0,0 +1,12 @@ +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class AccountStatus +{ + public string Id { get; set; } + public AccountApprovalStatus? CommercialInfo { get; set; } + public AccountApprovalStatus? Documentation { get; set; } + public AccountApprovalStatus? General { get; set; } + public AccountApprovalStatus? BankAccountInfo { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/CommercialInfoExpiration.cs b/Codout.Apis.Asaas/Models/MyAccount/CommercialInfoExpiration.cs new file mode 100644 index 0000000..79218c0 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/CommercialInfoExpiration.cs @@ -0,0 +1,9 @@ +using System; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class CommercialInfoExpiration +{ + public bool? IsExpired { get; set; } + public DateTime? ScheduledDate { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountApprovalStatus.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountApprovalStatus.cs new file mode 100644 index 0000000..4342d24 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountApprovalStatus.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +public enum AccountApprovalStatus +{ + PENDING, + APPROVED, + REJECTED, + AWAITING_APPROVAL +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentGroupStatus.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentGroupStatus.cs new file mode 100644 index 0000000..8dd11ad --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentGroupStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +public enum AccountDocumentGroupStatus +{ + NOT_SENT, + PENDING, + APPROVED, + REJECTED, + IGNORED +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentResponsibleType.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentResponsibleType.cs new file mode 100644 index 0000000..1c97de1 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentResponsibleType.cs @@ -0,0 +1,18 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +public enum AccountDocumentResponsibleType +{ + ALLOW_BANK_ACCOUNT_DEPOSIT_STATEMENT, + ASAAS_ACCOUNT_OWNER_EMANCIPATION_AGE, + ASAAS_ACCOUNT_OWNER, + ASSOCIATION, + BANK_ACCOUNT_OWNER_EMANCIPATION_AGE, + BANK_ACCOUNT_OWNER, + CUSTOM, + DIRECTOR, + INDIVIDUAL_COMPANY, + LIMITED_COMPANY, + MEI, + PARTNER, + POWER_OF_ATTORNEY +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentStatus.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentStatus.cs new file mode 100644 index 0000000..2c2f4b7 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentStatus.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +public enum AccountDocumentStatus +{ + NOT_SENT, + PENDING, + APPROVED, + REJECTED +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentType.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentType.cs new file mode 100644 index 0000000..3b7b0a4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountDocumentType.cs @@ -0,0 +1,17 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +public enum AccountDocumentType +{ + ALLOW_BANK_ACCOUNT_DEPOSIT_STATEMENT, + CUSTOM, + EMANCIPATION_OF_MINORS, + ENTREPRENEUR_REQUIREMENT, + IDENTIFICATION_SELFIE, + IDENTIFICATION, + INVOICE, + MEI_CERTIFICATE, + MINUTES_OF_CONSTITUTION, + MINUTES_OF_ELECTION, + POWER_OF_ATTORNEY, + SOCIAL_CONTRACT +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountInfoStatus.cs b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountInfoStatus.cs new file mode 100644 index 0000000..bd03ef4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/Enums/AccountInfoStatus.cs @@ -0,0 +1,12 @@ +namespace Codout.Apis.Asaas.Models.MyAccount.Enums; + +/// +/// Status do cadastro da conta (campo "status" de AccountInfoGetResponseDTO). +/// +public enum AccountInfoStatus +{ + APPROVED, + AWAITING_ACTION_AUTHORIZATION, + DENIED, + PENDING +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/Fees.cs b/Codout.Apis.Asaas/Models/MyAccount/Fees.cs index 3eaa2b6..3a9898c 100644 --- a/Codout.Apis.Asaas/Models/MyAccount/Fees.cs +++ b/Codout.Apis.Asaas/Models/MyAccount/Fees.cs @@ -22,7 +22,7 @@ public class BankSlip { public decimal? DefaultValue { get; set; } public decimal? DiscountValue { get; set; } - public string? ExpirationDate { get; set; } + public string ExpirationDate { get; set; } public int? DaysToReceive { get; set; } public decimal? MonthlyFeePercentage { get; set; } } @@ -44,7 +44,7 @@ public class CreditCard public decimal? DiscountUpToSixInstallmentsPercentage { get; set; } public decimal? DiscountUpToTwelveInstallmentsPercentage { get; set; } public decimal? DiscountUpToTwentyOneInstallmentsPercentage { get; set; } - public string? DiscountExpiration { get; set; } + public string DiscountExpiration { get; set; } public int? DaysToReceive { get; set; } public decimal? DetachedMonthlyFeeValue { get; set; } public decimal? InstallmentMonthlyFeeValue { get; set; } diff --git a/Codout.Apis.Asaas/Models/MyAccount/MyAccount.cs b/Codout.Apis.Asaas/Models/MyAccount/MyAccount.cs index f5bdad6..acf625d 100644 --- a/Codout.Apis.Asaas/Models/MyAccount/MyAccount.cs +++ b/Codout.Apis.Asaas/Models/MyAccount/MyAccount.cs @@ -1,42 +1,59 @@ -using System; +using System; +using System.Collections.Generic; using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.MyAccount.Enums; -namespace Codout.Apis.Asaas.Models.MyAccount +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class MyAccount { - public class MyAccount - { - public string Name { get; set; } + public AccountInfoStatus? Status { get; set; } + + public PersonType? PersonType { get; set; } + + public string CpfCnpj { get; set; } + + public string Name { get; set; } + + public DateTime? BirthDate { get; set; } + + public string CompanyName { get; set; } + + public CompanyType? CompanyType { get; set; } - public string Email { get; set; } + public decimal? IncomeValue { get; set; } - public string CpfCnpj { get; set; } + public string Email { get; set; } - public CompanyType? CompanyType { get; set; } + public string Phone { get; set; } - public string Phone { get; set; } + public string MobilePhone { get; set; } - public string MobilePhone { get; set; } + public string PostalCode { get; set; } - public string Address { get; set; } + public string Address { get; set; } - public string AddressNumber { get; set; } + public string AddressNumber { get; set; } - public string Complement { get; set; } + public string Complement { get; set; } - public string Province { get; set; } + public string Province { get; set; } - public string PostalCode { get; set; } + public City City { get; set; } - public PersonType? PersonType { get; set; } + public string DenialReason { get; set; } - public City City { get; set; } + public string TradingName { get; set; } - public string InscricaoEstadual { get; set; } + public string Site { get; set; } - public DateTime? BirthDate { get; set; } + public List AvailableCompanyNames { get; set; } = []; - public string Status { get; set; } + public CommercialInfoExpiration CommercialInfoExpiration { get; set; } - public string DenialReason { get; set; } - } + /// + /// Mantido por backwards-compat. Nao existe no schema atual. + /// + [Obsolete("Nao existe no schema oficial AccountInfoGetResponseDTO.")] + public string InscricaoEstadual { get; set; } } diff --git a/Codout.Apis.Asaas/Models/MyAccount/UpdateCommercialInfoRequest.cs b/Codout.Apis.Asaas/Models/MyAccount/UpdateCommercialInfoRequest.cs new file mode 100644 index 0000000..77c8b38 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/UpdateCommercialInfoRequest.cs @@ -0,0 +1,23 @@ +using System; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +public class UpdateCommercialInfoRequest +{ + public string Name { get; set; } + public string Email { get; set; } + public string CpfCnpj { get; set; } + public DateTime? BirthDate { get; set; } + public string CompanyName { get; set; } + public CompanyType? CompanyType { get; set; } + public decimal? IncomeValue { get; set; } + public string Phone { get; set; } + public string MobilePhone { get; set; } + public string PostalCode { get; set; } + public string Address { get; set; } + public string AddressNumber { get; set; } + public string Complement { get; set; } + public string Province { get; set; } + public string Site { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/MyAccount/UploadAccountDocumentRequest.cs b/Codout.Apis.Asaas/Models/MyAccount/UploadAccountDocumentRequest.cs new file mode 100644 index 0000000..45df441 --- /dev/null +++ b/Codout.Apis.Asaas/Models/MyAccount/UploadAccountDocumentRequest.cs @@ -0,0 +1,16 @@ +using Codout.Apis.Asaas.Core.Interfaces; +using Codout.Apis.Asaas.Models.MyAccount.Enums; + +namespace Codout.Apis.Asaas.Models.MyAccount; + +/// +/// Request multipart/form-data para upload de documentos da conta. +/// Schema oficial: campos sao "documentFile" (binary) e "type" (enum). +/// Os nomes das propriedades do C# casam exatamente com o nome do form-field +/// apos a conversao firstCharToLower feita no PostMultipartFormDataContentAsync. +/// +public class UploadAccountDocumentRequest +{ + public AccountDocumentType? Type { get; set; } + public IAsaasFile DocumentFile { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Notification/Enums/NotificationEvent.cs b/Codout.Apis.Asaas/Models/Notification/Enums/NotificationEvent.cs new file mode 100644 index 0000000..901c719 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Notification/Enums/NotificationEvent.cs @@ -0,0 +1,15 @@ +namespace Codout.Apis.Asaas.Models.Notification.Enums; + +/// +/// Eventos que disparam notificacao para o cliente. Schema oficial: +/// NotificationGetResponseNotificationEvent enum (6 valores). +/// +public enum NotificationEvent +{ + PAYMENT_CREATED, + PAYMENT_UPDATED, + PAYMENT_RECEIVED, + PAYMENT_OVERDUE, + PAYMENT_DUEDATE_WARNING, + SEND_LINHA_DIGITAVEL +} diff --git a/Codout.Apis.Asaas/Models/Notification/Notification.cs b/Codout.Apis.Asaas/Models/Notification/Notification.cs index f2a9cb6..dbbcc04 100644 --- a/Codout.Apis.Asaas/Models/Notification/Notification.cs +++ b/Codout.Apis.Asaas/Models/Notification/Notification.cs @@ -1,16 +1,20 @@ -namespace Codout.Apis.Asaas.Models.Notification +using Codout.Apis.Asaas.Models.Notification.Enums; + +namespace Codout.Apis.Asaas.Models.Notification; + +public class Notification { - public class Notification - { - public string Id { get; set; } - public string Customer { get; set; } - public bool Enabled { get; set; } - public bool EmailEnabledForProvider { get; set; } - public bool SmsEnabledForProvider { get; set; } - public bool EmailEnabledForCustomer { get; set; } - public bool SmsEnabledForCustomer { get; set; } - public bool PhoneCallEnabledForCustomer { get; set; } - public bool WhatsappEnabledForCustomer { get; set; } - public int? ScheduleOffset { get; set; } - } + public string Object { get; set; } + public string Id { get; set; } + public string Customer { get; set; } + public bool? Enabled { get; set; } + public bool? EmailEnabledForProvider { get; set; } + public bool? SmsEnabledForProvider { get; set; } + public bool? EmailEnabledForCustomer { get; set; } + public bool? SmsEnabledForCustomer { get; set; } + public bool? PhoneCallEnabledForCustomer { get; set; } + public bool? WhatsappEnabledForCustomer { get; set; } + public NotificationEvent? Event { get; set; } + public int? ScheduleOffset { get; set; } + public bool? Deleted { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Notification/UpdateNotificationRequest.cs b/Codout.Apis.Asaas/Models/Notification/UpdateNotificationRequest.cs index 2aa9aa5..5503ec0 100644 --- a/Codout.Apis.Asaas/Models/Notification/UpdateNotificationRequest.cs +++ b/Codout.Apis.Asaas/Models/Notification/UpdateNotificationRequest.cs @@ -1,14 +1,13 @@ -namespace Codout.Apis.Asaas.Models.Notification +namespace Codout.Apis.Asaas.Models.Notification; + +public class UpdateNotificationRequest { - public class UpdateNotificationRequest - { - public bool Enabled { get; set; } - public bool EmailEnabledForProvider { get; set; } - public bool SmsEnabledForProvider { get; set; } - public bool EmailEnabledForCustomer { get; set; } - public bool SmsEnabledForCustomer { get; set; } - public bool PhoneCallEnabledForCustomer { get; set; } - public bool WhatsappEnabledForCustomer { get; set; } - public int? ScheduleOffset { get; set; } - } + public bool? Enabled { get; set; } + public bool? EmailEnabledForProvider { get; set; } + public bool? SmsEnabledForProvider { get; set; } + public bool? EmailEnabledForCustomer { get; set; } + public bool? SmsEnabledForCustomer { get; set; } + public bool? PhoneCallEnabledForCustomer { get; set; } + public bool? WhatsappEnabledForCustomer { get; set; } + public int? ScheduleOffset { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Payment/CapturePaymentRequest.cs b/Codout.Apis.Asaas/Models/Payment/CapturePaymentRequest.cs new file mode 100644 index 0000000..03e2939 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/CapturePaymentRequest.cs @@ -0,0 +1,6 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class CapturePaymentRequest +{ + public decimal? Value { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/CreatePaymentRequest.cs b/Codout.Apis.Asaas/Models/Payment/CreatePaymentRequest.cs index 03b4eef..8137f07 100644 --- a/Codout.Apis.Asaas/Models/Payment/CreatePaymentRequest.cs +++ b/Codout.Apis.Asaas/Models/Payment/CreatePaymentRequest.cs @@ -35,6 +35,12 @@ public class CreatePaymentRequest public bool PostalService { get; set; } + public int? DaysAfterDueDateToRegistrationCancellation { get; set; } + + public PaymentCallback Callback { get; set; } + + public string PixAutomaticAuthorizationId { get; set; } + public CreditCardRequest CreditCard { get; set; } public CreditCardHolderInfoRequest CreditCardHolderInfo { get; set; } @@ -43,6 +49,6 @@ public class CreatePaymentRequest public List Split { get; set; } = []; - public string? CreditCardToken { get; set; } + public string CreditCardToken { get; set; } } } diff --git a/Codout.Apis.Asaas/Models/Payment/Enums/PaymentStatus.cs b/Codout.Apis.Asaas/Models/Payment/Enums/PaymentStatus.cs index 4a0779b..4cce7f5 100644 --- a/Codout.Apis.Asaas/Models/Payment/Enums/PaymentStatus.cs +++ b/Codout.Apis.Asaas/Models/Payment/Enums/PaymentStatus.cs @@ -9,6 +9,7 @@ public enum PaymentStatus REFUNDED, RECEIVED_IN_CASH, REFUND_REQUESTED, + REFUND_IN_PROGRESS, CHARGEBACK_REQUESTED, CHARGEBACK_DISPUTE, AWAITING_CHARGEBACK_REVERSAL, diff --git a/Codout.Apis.Asaas/Models/Payment/PayWithCreditCardRequest.cs b/Codout.Apis.Asaas/Models/Payment/PayWithCreditCardRequest.cs new file mode 100644 index 0000000..fd361d4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PayWithCreditCardRequest.cs @@ -0,0 +1,11 @@ +using Codout.Apis.Asaas.Models.Common; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PayWithCreditCardRequest +{ + public CreditCardRequest CreditCard { get; set; } + public CreditCardHolderInfoRequest CreditCardHolderInfo { get; set; } + public string CreditCardToken { get; set; } + public string RemoteIp { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/Payment.cs b/Codout.Apis.Asaas/Models/Payment/Payment.cs index f0c7f1e..7a47014 100644 --- a/Codout.Apis.Asaas/Models/Payment/Payment.cs +++ b/Codout.Apis.Asaas/Models/Payment/Payment.cs @@ -11,7 +11,7 @@ public class Payment { public string Id { get; set; } - public DateTime DateCreated { get; set; } + public DateTime? DateCreated { get; set; } [JsonPropertyName("customer")] public string CustomerId { get; set; } @@ -22,7 +22,7 @@ public class Payment [JsonPropertyName("installment")] public string InstallmentId { get; set; } - public DateTime DueDate { get; set; } + public DateTime? DueDate { get; set; } public decimal Value { get; set; } @@ -42,7 +42,7 @@ public class Payment public string ExternalReference { get; set; } - public DateTime OriginalDueDate { get; set; } + public DateTime? OriginalDueDate { get; set; } public decimal? OriginalValue { get; set; } @@ -60,11 +60,38 @@ public class Payment public string InvoiceNumber { get; set; } - public bool Deleted { get; set; } + public bool? Deleted { get; set; } - public bool PostalService { get; set; } + public bool? PostalService { get; set; } - public bool Anticipated { get; set; } + public bool? Anticipated { get; set; } + + public bool? Anticipable { get; set; } + + public bool? CanBePaidAfterDueDate { get; set; } + + public string Object { get; set; } + + public string PixTransaction { get; set; } + + public string PixQrCodeId { get; set; } + + public string CheckoutSession { get; set; } + + [JsonPropertyName("paymentLink")] + public string PaymentLinkId { get; set; } + + public int? InstallmentNumber { get; set; } + + public DateTime? CreditDate { get; set; } + + public DateTime? EstimatedCreditDate { get; set; } + + public string TransactionReceiptUrl { get; set; } + + public string NossoNumero { get; set; } + + public int? DaysAfterDueDateToRegistrationCancellation { get; set; } public Common.CreditCard CreditCard { get; set; } diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfo.cs b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfo.cs new file mode 100644 index 0000000..3411701 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfo.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentBillingInfo +{ + public PaymentBillingInfoPix Pix { get; set; } + public PaymentBillingInfoCreditCard CreditCard { get; set; } + public PaymentBillingInfoBankSlip BankSlip { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoBankSlip.cs b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoBankSlip.cs new file mode 100644 index 0000000..91707e0 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoBankSlip.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentBillingInfoBankSlip +{ + public string IdentificationField { get; set; } + public string NossoNumero { get; set; } + public string BarCode { get; set; } + public string BankSlipUrl { get; set; } + public int? DaysAfterDueDateToRegistrationCancellation { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoCreditCard.cs b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoCreditCard.cs new file mode 100644 index 0000000..4a1c8d9 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoCreditCard.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentBillingInfoCreditCard +{ + public string CreditCardNumber { get; set; } + public string CreditCardBrand { get; set; } + public string CreditCardToken { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoPix.cs b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoPix.cs new file mode 100644 index 0000000..32bc36b --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentBillingInfoPix.cs @@ -0,0 +1,11 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentBillingInfoPix +{ + public string EncodedImage { get; set; } + public string Payload { get; set; } + public DateTime? ExpirationDate { get; set; } + public string Description { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentDocument.cs b/Codout.Apis.Asaas/Models/Payment/PaymentDocument.cs new file mode 100644 index 0000000..b1613fa --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentDocument.cs @@ -0,0 +1,12 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentDocument +{ + public string Id { get; set; } + public string Name { get; set; } + public bool Available { get; set; } + public string Type { get; set; } + public DateTime? DateCreated { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentLimits.cs b/Codout.Apis.Asaas/Models/Payment/PaymentLimits.cs new file mode 100644 index 0000000..8b9c870 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentLimits.cs @@ -0,0 +1,6 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentLimits +{ + public PaymentLimitsCreation Creation { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentLimitsCreation.cs b/Codout.Apis.Asaas/Models/Payment/PaymentLimitsCreation.cs new file mode 100644 index 0000000..d41c41c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentLimitsCreation.cs @@ -0,0 +1,6 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentLimitsCreation +{ + public PaymentLimitsDaily Daily { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentLimitsDaily.cs b/Codout.Apis.Asaas/Models/Payment/PaymentLimitsDaily.cs new file mode 100644 index 0000000..951b8b9 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentLimitsDaily.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentLimitsDaily +{ + public long Limit { get; set; } + public long Used { get; set; } + public bool? WasReached { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentListFilter.cs b/Codout.Apis.Asaas/Models/Payment/PaymentListFilter.cs index cb9983e..338d09d 100644 --- a/Codout.Apis.Asaas/Models/Payment/PaymentListFilter.cs +++ b/Codout.Apis.Asaas/Models/Payment/PaymentListFilter.cs @@ -1,82 +1,149 @@ -using System; +using System; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Invoice.Enums; using Codout.Apis.Asaas.Models.Payment.Enums; -namespace Codout.Apis.Asaas.Models.Payment +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentListFilter : RequestParameters { - public class PaymentListFilter : RequestParameters - { - public string CustomerId - { - get => this["customer"]; - set => Add("customer", value); - } - - public string SubscriptionId - { - get => this["subscription"]; - set => Add("subscription", value); - } - - public string InstallmentId - { - get => this["installment"]; - set => Add("installment", value); - } - - public BillingType? BillingType - { - get => Get("billingType"); - set => Add("billingType", value); - } - - public PaymentStatus? Status - { - get => Get("status"); - set => Add("status", value); - } - - public string ExternalReference - { - get => this["externalReference"]; - set => Add("externalReference", value); - } - - public DateTime? PaymentDate - { - get => Get("paymentDate"); - set => Add("paymentDate", value); - } - - public bool? Anticipated - { - get => Get("anticipated"); - set => Add("anticipated", value); - } - - public DateTime? PaymentDateGE - { - get => Get("paymentDate[ge]"); - set => Add("paymentDate[ge]", value); - } - - public DateTime? PaymentDateLE - { - get => Get("paymentDate[le]"); - set => Add("paymentDate[le]", value); - } - - public DateTime? DueDateGE - { - get => Get("dueDate[ge]"); - set => Add("dueDate[ge]", value); - } - - public DateTime? DueDateLE - { - get => Get("dueDate[le]"); - set => Add("dueDate[le]", value); - } - } -} \ No newline at end of file + public string CustomerId + { + get => this["customer"]; + set => Add("customer", value); + } + + public string CustomerGroupName + { + get => this["customerGroupName"]; + set => Add("customerGroupName", value); + } + + public string SubscriptionId + { + get => this["subscription"]; + set => Add("subscription", value); + } + + public string InstallmentId + { + get => this["installment"]; + set => Add("installment", value); + } + + public BillingType? BillingType + { + get => Get("billingType"); + set => Add("billingType", value); + } + + public PaymentStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + + public string ExternalReference + { + get => this["externalReference"]; + set => Add("externalReference", value); + } + + public DateTime? PaymentDate + { + get => Get("paymentDate"); + set => Add("paymentDate", value); + } + + public InvoiceStatus? InvoiceStatus + { + get => Get("invoiceStatus"); + set => Add("invoiceStatus", value); + } + + public DateTime? EstimatedCreditDate + { + get => Get("estimatedCreditDate"); + set => Add("estimatedCreditDate", value); + } + + public string PixQrCodeId + { + get => this["pixQrCodeId"]; + set => Add("pixQrCodeId", value); + } + + public bool? Anticipated + { + get => Get("anticipated"); + set => Add("anticipated", value); + } + + public bool? Anticipable + { + get => Get("anticipable"); + set => Add("anticipable", value); + } + + public string User + { + get => this["user"]; + set => Add("user", value); + } + + public string CheckoutSession + { + get => this["checkoutSession"]; + set => Add("checkoutSession", value); + } + + // Schema usa [ge]/[le] LOWERCASE para Payment (diferente de Invoice que usa [Ge]/[Le]). + public DateTime? DateCreatedGE + { + get => Get("dateCreated[ge]"); + set => Add("dateCreated[ge]", value); + } + + public DateTime? DateCreatedLE + { + get => Get("dateCreated[le]"); + set => Add("dateCreated[le]", value); + } + + public DateTime? PaymentDateGE + { + get => Get("paymentDate[ge]"); + set => Add("paymentDate[ge]", value); + } + + public DateTime? PaymentDateLE + { + get => Get("paymentDate[le]"); + set => Add("paymentDate[le]", value); + } + + public DateTime? EstimatedCreditDateGE + { + get => Get("estimatedCreditDate[ge]"); + set => Add("estimatedCreditDate[ge]", value); + } + + public DateTime? EstimatedCreditDateLE + { + get => Get("estimatedCreditDate[le]"); + set => Add("estimatedCreditDate[le]", value); + } + + public DateTime? DueDateGE + { + get => Get("dueDate[ge]"); + set => Add("dueDate[ge]", value); + } + + public DateTime? DueDateLE + { + get => Get("dueDate[le]"); + set => Add("dueDate[le]", value); + } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentRefund.cs b/Codout.Apis.Asaas/Models/Payment/PaymentRefund.cs new file mode 100644 index 0000000..af4f80d --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentRefund.cs @@ -0,0 +1,14 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentRefund +{ + public DateTime? DateCreated { get; set; } + public string Status { get; set; } + public decimal Value { get; set; } + public string EndToEndIdentifier { get; set; } + public string Description { get; set; } + public DateTime? EffectiveDate { get; set; } + public string TransactionReceiptUrl { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentSplitView.cs b/Codout.Apis.Asaas/Models/Payment/PaymentSplitView.cs new file mode 100644 index 0000000..90306cd --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentSplitView.cs @@ -0,0 +1,17 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentSplitView +{ + public string Id { get; set; } + public string Payment { get; set; } + public string WalletId { get; set; } + public decimal? FixedValue { get; set; } + public decimal? PercentualValue { get; set; } + public decimal? TotalValue { get; set; } + public string Status { get; set; } + public DateTime? CreditDate { get; set; } + public string ExternalReference { get; set; } + public string Description { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentStatusInfo.cs b/Codout.Apis.Asaas/Models/Payment/PaymentStatusInfo.cs new file mode 100644 index 0000000..dae098b --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentStatusInfo.cs @@ -0,0 +1,8 @@ +using Codout.Apis.Asaas.Models.Payment.Enums; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentStatusInfo +{ + public PaymentStatus Status { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/PaymentViewingInfo.cs b/Codout.Apis.Asaas/Models/Payment/PaymentViewingInfo.cs new file mode 100644 index 0000000..7be1a9b --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/PaymentViewingInfo.cs @@ -0,0 +1,9 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class PaymentViewingInfo +{ + public DateTime? BankSlipViewedDate { get; set; } + public DateTime? PaymentCheckoutViewedDate { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/ReceiveInCashRequest.cs b/Codout.Apis.Asaas/Models/Payment/ReceiveInCashRequest.cs new file mode 100644 index 0000000..88a9378 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/ReceiveInCashRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class ReceiveInCashRequest +{ + public DateTime PaymentDate { get; set; } + public decimal Value { get; set; } + public bool NotifyCustomer { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatePaymentRequest.cs b/Codout.Apis.Asaas/Models/Payment/SimulatePaymentRequest.cs new file mode 100644 index 0000000..0ddf10c --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatePaymentRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Common.Enums; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatePaymentRequest +{ + public decimal Value { get; set; } + + public List BillingTypes { get; set; } = []; + + public int? InstallmentCount { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatedPayment.cs b/Codout.Apis.Asaas/Models/Payment/SimulatedPayment.cs new file mode 100644 index 0000000..97bb878 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatedPayment.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatedPayment +{ + public decimal Value { get; set; } + public SimulatedPaymentCreditCard CreditCard { get; set; } + public SimulatedPaymentBankSlip BankSlip { get; set; } + public SimulatedPaymentPix Pix { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentBankSlip.cs b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentBankSlip.cs new file mode 100644 index 0000000..6b4982f --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentBankSlip.cs @@ -0,0 +1,8 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatedPaymentBankSlip +{ + public decimal NetValue { get; set; } + public decimal FeeValue { get; set; } + public SimulatedPaymentInstallment Installment { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentCreditCard.cs b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentCreditCard.cs new file mode 100644 index 0000000..71caf24 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentCreditCard.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatedPaymentCreditCard +{ + public decimal NetValue { get; set; } + public decimal FeePercentage { get; set; } + public decimal OperationFee { get; set; } + public SimulatedPaymentInstallment Installment { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentInstallment.cs b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentInstallment.cs new file mode 100644 index 0000000..fba9a3a --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentInstallment.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatedPaymentInstallment +{ + public decimal PaymentNetValue { get; set; } + public decimal PaymentValue { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentPix.cs b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentPix.cs new file mode 100644 index 0000000..ec67964 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/SimulatedPaymentPix.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class SimulatedPaymentPix +{ + public decimal NetValue { get; set; } + public decimal? FeePercentage { get; set; } + public decimal FeeValue { get; set; } + public SimulatedPaymentInstallment Installment { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/UpdatePaymentDocumentRequest.cs b/Codout.Apis.Asaas/Models/Payment/UpdatePaymentDocumentRequest.cs new file mode 100644 index 0000000..d2fc2f1 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/UpdatePaymentDocumentRequest.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Payment; + +public class UpdatePaymentDocumentRequest +{ + public bool? Available { get; set; } + public string Type { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Payment/UploadPaymentDocumentRequest.cs b/Codout.Apis.Asaas/Models/Payment/UploadPaymentDocumentRequest.cs new file mode 100644 index 0000000..998133f --- /dev/null +++ b/Codout.Apis.Asaas/Models/Payment/UploadPaymentDocumentRequest.cs @@ -0,0 +1,12 @@ +using Codout.Apis.Asaas.Core.Interfaces; + +namespace Codout.Apis.Asaas.Models.Payment; + +public class UploadPaymentDocumentRequest +{ + public string Type { get; set; } + + public bool? Available { get; set; } + + public IAsaasFile File { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningHistoryStatus.cs b/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningHistoryStatus.cs new file mode 100644 index 0000000..e97a1a3 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningHistoryStatus.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.PaymentDunning.Enums; + +public enum PaymentDunningHistoryStatus +{ + IN_NEGOTIATION, + NEGOTIATION_FAIL, + NEGOTIATED, + PAID +} diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningType.cs b/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningType.cs index 6607a6e..fdede92 100644 --- a/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningType.cs +++ b/Codout.Apis.Asaas/Models/PaymentDunning/Enums/PaymentDunningType.cs @@ -1,15 +1,18 @@ -namespace Codout.Apis.Asaas.Models.PaymentDunning.Enums +namespace Codout.Apis.Asaas.Models.PaymentDunning.Enums; + +public enum PaymentDunningType { - public enum PaymentDunningType - { - CREDIT_BUREAU - } + CREDIT_BUREAU, + // DEBT_RECOVERY_ASSISTANCE so e aceito pelo FILTER do List endpoint + // (PaymentDunningListRequestPaymentDunningType). Save/response retornam + // sempre CREDIT_BUREAU. Mantido aqui para que o filter compile. + DEBT_RECOVERY_ASSISTANCE +} - public static class PaymentDunningTypeExtension +public static class PaymentDunningTypeExtension +{ + public static bool IsCreditBureau(this PaymentDunningType type) { - public static bool IsCreditBureau(this PaymentDunningType type) - { - return type == PaymentDunningType.CREDIT_BUREAU; - } + return type == PaymentDunningType.CREDIT_BUREAU; } } diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunning.cs b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunning.cs index 8a14196..240fa75 100644 --- a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunning.cs +++ b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunning.cs @@ -1,40 +1,43 @@ -using System; -using Codout.Apis.Asaas.Models.PaymentDunning.Enums; +using System; using System.Text.Json.Serialization; +using Codout.Apis.Asaas.Models.PaymentDunning.Enums; + +namespace Codout.Apis.Asaas.Models.PaymentDunning; -namespace Codout.Apis.Asaas.Models.PaymentDunning +public class PaymentDunning { - public class PaymentDunning { + public string Id { get; set; } - public string Id { get; set; } + public int? DunningNumber { get; set; } - public string DunningNumber { get; set; } + public PaymentDunningStatus Status { get; set; } - public PaymentDunningStatus Status { get; set; } + public PaymentDunningType Type { get; set; } - public PaymentDunningType Type { get; set; } + [JsonPropertyName("payment")] + public string PaymentId { get; set; } - [JsonPropertyName("payment")] - public string PaymentId { get; set; } + public DateTime RequestDate { get; set; } - public DateTime RequestDate { get; set; } + public string Description { get; set; } - public string Description { get; set; } + public decimal Value { get; set; } - public decimal Value { get; set; } + public decimal FeeValue { get; set; } - public decimal FeeValue { get; set; } + public decimal NetValue { get; set; } - public decimal NetValue { get; set; } + [Obsolete("Campo deprecated no schema oficial.")] + public decimal ReceivedInCashFeeValue { get; set; } - public decimal ReceivedInCashFeeValue { get; set; } + public string DenialReason { get; set; } - public string DenialReason { get; set; } + [Obsolete("Campo deprecated no schema oficial.")] + public decimal CancellationFeeValue { get; set; } - public decimal CancellationFeeValue { get; set; } + public bool? IsNecessaryResendDocumentation { get; set; } - public bool IsNecessaryResendDocumentation { get; set; } + public bool? CanBeCancelled { get; set; } - public bool CanBeCancelled { get; set; } - } + public string CannotBeCancelledReason { get; set; } } diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningEventHistory.cs b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningEventHistory.cs index 494469b..0cdbe7f 100644 --- a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningEventHistory.cs +++ b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningEventHistory.cs @@ -1,13 +1,13 @@ -using System; +using System; +using Codout.Apis.Asaas.Models.PaymentDunning.Enums; -namespace Codout.Apis.Asaas.Models.PaymentDunning +namespace Codout.Apis.Asaas.Models.PaymentDunning; + +public class PaymentDunningEventHistory { - public class PaymentDunningEventHistory - { - public string Status { get; set; } + public PaymentDunningHistoryStatus Status { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public DateTime EventDate { get; set; } - } + public DateTime EventDate { get; set; } } diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningPaymentAvailable.cs b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningPaymentAvailable.cs index fb72552..df08aab 100644 --- a/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningPaymentAvailable.cs +++ b/Codout.Apis.Asaas/Models/PaymentDunning/PaymentDunningPaymentAvailable.cs @@ -1,26 +1,28 @@ -using System; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; using Codout.Apis.Asaas.Models.Common.Enums; using Codout.Apis.Asaas.Models.Payment.Enums; -using System.Text.Json.Serialization; -namespace Codout.Apis.Asaas.Models.PaymentDunning +namespace Codout.Apis.Asaas.Models.PaymentDunning; + +public class PaymentDunningPaymentAvailable { - public class PaymentDunningPaymentAvailable - { - [JsonPropertyName("payment")] - public string PaymentId { get; set; } + [JsonPropertyName("payment")] + public string PaymentId { get; set; } - [JsonPropertyName("customer")] - public string CustomerId { get; set; } + [JsonPropertyName("customer")] + public string CustomerId { get; set; } - public decimal Value { get; set; } + public decimal Value { get; set; } - public PaymentStatus Status { get; set; } + public PaymentStatus Status { get; set; } - public BillingType BillingType { get; set; } + public BillingType BillingType { get; set; } - public DateTime DueDate { get; set; } + public DateTime DueDate { get; set; } - public PaymentDunningTypeSimulations TypeSimulations { get; set; } - } + // Schema retorna ARRAY (simulacao por tipo). Antes do fix B-22m era + // objeto unico, o que causava InvalidCastException na deserializacao. + public List TypeSimulations { get; set; } = []; } diff --git a/Codout.Apis.Asaas/Models/PaymentDunning/SimulatedPaymentDunning.cs b/Codout.Apis.Asaas/Models/PaymentDunning/SimulatedPaymentDunning.cs index 18f6fdd..b49ada4 100644 --- a/Codout.Apis.Asaas/Models/PaymentDunning/SimulatedPaymentDunning.cs +++ b/Codout.Apis.Asaas/Models/PaymentDunning/SimulatedPaymentDunning.cs @@ -1,13 +1,16 @@ -using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; -namespace Codout.Apis.Asaas.Models.PaymentDunning +namespace Codout.Apis.Asaas.Models.PaymentDunning; + +public class SimulatedPaymentDunning { - public class SimulatedPaymentDunning { - [JsonPropertyName("payment")] - public string PaymentId { get; set; } + [JsonPropertyName("payment")] + public string PaymentId { get; set; } - public decimal Value { get; set; } + public decimal Value { get; set; } - public PaymentDunningTypeSimulations TypeSimulations { get; set; } - } + // Schema retorna ARRAY (simulacao por tipo). Antes do fix B-22m era + // objeto unico, o que causava InvalidCastException na deserializacao. + public List TypeSimulations { get; set; } = []; } diff --git a/Codout.Apis.Asaas/Models/PaymentLink/PaymentLink.cs b/Codout.Apis.Asaas/Models/PaymentLink/PaymentLink.cs index 4f21491..28ce2d8 100644 --- a/Codout.Apis.Asaas/Models/PaymentLink/PaymentLink.cs +++ b/Codout.Apis.Asaas/Models/PaymentLink/PaymentLink.cs @@ -1,24 +1,27 @@ using System; using Codout.Apis.Asaas.Models.Common.Enums; using Codout.Apis.Asaas.Models.PaymentLink.Enums; +using Codout.Apis.Asaas.Models.Subscription.Enums; -namespace Codout.Apis.Asaas.Models.PaymentLink +namespace Codout.Apis.Asaas.Models.PaymentLink; + +public class PaymentLink { - public class PaymentLink - { - public string Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public string Url { get; set; } - public decimal Value { get; set; } - public bool Active { get; set; } - public BillingType BillingType { get; set; } - public ChargeType ChargeType { get; set; } - public int DueDateLimitDays { get; set; } - public string SubscriptionCycle { get; set; } - public int MaxInstallmentCount { get; set; } - public bool NotificationEnabled { get; set; } - public DateTime? EndDate { get; set; } - public bool Deleted { get; set; } - } + public string Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Url { get; set; } + public decimal? Value { get; set; } + public bool? Active { get; set; } + public BillingType BillingType { get; set; } + public ChargeType ChargeType { get; set; } + public int? DueDateLimitDays { get; set; } + public Cycle? SubscriptionCycle { get; set; } + public int? MaxInstallmentCount { get; set; } + public bool? NotificationEnabled { get; set; } + public DateTime? EndDate { get; set; } + public bool? Deleted { get; set; } + public int? ViewCount { get; set; } + public bool? IsAddressRequired { get; set; } + public string ExternalReference { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Pix/Enums/PixAddressKeyStatus.cs b/Codout.Apis.Asaas/Models/Pix/Enums/PixAddressKeyStatus.cs new file mode 100644 index 0000000..ebef488 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/Enums/PixAddressKeyStatus.cs @@ -0,0 +1,11 @@ +namespace Codout.Apis.Asaas.Models.Pix.Enums; + +public enum PixAddressKeyStatus +{ + AWAITING_ACTIVATION, + ACTIVE, + AWAITING_DELETION, + AWAITING_ACCOUNT_DELETION, + DELETED, + ERROR +} diff --git a/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionFinality.cs b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionFinality.cs new file mode 100644 index 0000000..4e3e788 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionFinality.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Pix.Enums; + +public enum PixTransactionFinality +{ + WITHDRAWAL, + CHANGE +} diff --git a/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionOriginType.cs b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionOriginType.cs new file mode 100644 index 0000000..c263feb --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionOriginType.cs @@ -0,0 +1,11 @@ +namespace Codout.Apis.Asaas.Models.Pix.Enums; + +public enum PixTransactionOriginType +{ + MANUAL, + ADDRESS_KEY, + STATIC_QRCODE, + DYNAMIC_QRCODE, + PAYMENT_INITIATION_SERVICE, + AUTOMATIC_RECURRING +} diff --git a/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionStatus.cs b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionStatus.cs index 23b0ad6..736b3b0 100644 --- a/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionStatus.cs +++ b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionStatus.cs @@ -1,11 +1,16 @@ -namespace Codout.Apis.Asaas.Models.Pix.Enums +namespace Codout.Apis.Asaas.Models.Pix.Enums; + +public enum PixTransactionStatus { - public enum PixTransactionStatus - { - PENDING, - DONE, - CANCELLED, - SCHEDULED, - FAILED - } + AWAITING_BALANCE_VALIDATION, + AWAITING_INSTANT_PAYMENT_ACCOUNT_BALANCE, + AWAITING_CRITICAL_ACTION_AUTHORIZATION, + AWAITING_CHECKOUT_RISK_ANALYSIS_REQUEST, + AWAITING_CASH_IN_RISK_ANALYSIS_REQUEST, + SCHEDULED, + AWAITING_REQUEST, + REQUESTED, + DONE, + REFUSED, + CANCELLED } diff --git a/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionType.cs b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionType.cs new file mode 100644 index 0000000..8f22e72 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/Enums/PixTransactionType.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.Pix.Enums; + +public enum PixTransactionType +{ + DEBIT, + CREDIT, + CREDIT_REFUND, + DEBIT_REFUND, + DEBIT_REFUND_CANCELLATION +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixAddressKey.cs b/Codout.Apis.Asaas/Models/Pix/PixAddressKey.cs index 4a182dc..372f4e2 100644 --- a/Codout.Apis.Asaas/Models/Pix/PixAddressKey.cs +++ b/Codout.Apis.Asaas/Models/Pix/PixAddressKey.cs @@ -1,14 +1,16 @@ using System; using Codout.Apis.Asaas.Models.Pix.Enums; -namespace Codout.Apis.Asaas.Models.Pix +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixAddressKey { - public class PixAddressKey - { - public string Id { get; set; } - public string Key { get; set; } - public PixAddressKeyType Type { get; set; } - public string Status { get; set; } - public DateTime DateCreated { get; set; } - } + public string Id { get; set; } + public string Key { get; set; } + public PixAddressKeyType Type { get; set; } + public PixAddressKeyStatus Status { get; set; } + public DateTime? DateCreated { get; set; } + public bool? CanBeDeleted { get; set; } + public string CannotBeDeletedReason { get; set; } + public PixAddressKeyQrCode QrCode { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Pix/PixAddressKeyQrCode.cs b/Codout.Apis.Asaas/Models/Pix/PixAddressKeyQrCode.cs new file mode 100644 index 0000000..438e87b --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixAddressKeyQrCode.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixAddressKeyQrCode +{ + public string EncodedImage { get; set; } + public string Payload { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixAddressKeyTokenBucket.cs b/Codout.Apis.Asaas/Models/Pix/PixAddressKeyTokenBucket.cs new file mode 100644 index 0000000..9b16ede --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixAddressKeyTokenBucket.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixAddressKeyTokenBucket +{ + public int RemainingTokens { get; set; } + public int MaxTokens { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixOriginalTransaction.cs b/Codout.Apis.Asaas/Models/Pix/PixOriginalTransaction.cs new file mode 100644 index 0000000..cf77da6 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixOriginalTransaction.cs @@ -0,0 +1,11 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixOriginalTransaction +{ + public string Id { get; set; } + public string EndToEndIdentifier { get; set; } + public decimal? Value { get; set; } + public DateTime? EffectiveDate { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixTransaction.cs b/Codout.Apis.Asaas/Models/Pix/PixTransaction.cs index 465d30e..81ad8b0 100644 --- a/Codout.Apis.Asaas/Models/Pix/PixTransaction.cs +++ b/Codout.Apis.Asaas/Models/Pix/PixTransaction.cs @@ -1,16 +1,36 @@ using System; using Codout.Apis.Asaas.Models.Pix.Enums; -namespace Codout.Apis.Asaas.Models.Pix +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixTransaction { - public class PixTransaction - { - public string Id { get; set; } - public string Payment { get; set; } - public PixTransactionStatus Status { get; set; } - public decimal Value { get; set; } - public string Description { get; set; } - public DateTime? TransactionDate { get; set; } - public DateTime? ScheduleDate { get; set; } - } + public string Id { get; set; } + public string EndToEndIdentifier { get; set; } + public PixTransactionFinality? Finality { get; set; } + public decimal Value { get; set; } + public decimal? ChangeValue { get; set; } + public decimal? RefundedValue { get; set; } + public DateTime? EffectiveDate { get; set; } + public DateTime? ScheduledDate { get; set; } + public PixTransactionStatus Status { get; set; } + public PixTransactionType? Type { get; set; } + public PixTransactionOriginType? OriginType { get; set; } + public string ConciliationIdentifier { get; set; } + public string Description { get; set; } + public string TransactionReceiptUrl { get; set; } + public string RefusalReason { get; set; } + public bool? CanBeCanceled { get; set; } + public PixOriginalTransaction OriginalTransaction { get; set; } + public PixTransactionExternalAccount ExternalAccount { get; set; } + public PixTransactionQrCode QrCode { get; set; } + public string Payment { get; set; } + public bool? CanBeRefunded { get; set; } + public string RefundDisabledReason { get; set; } + public decimal? ChargedFeeValue { get; set; } + public DateTime? DateCreated { get; set; } + public string AddressKey { get; set; } + public PixAddressKeyType? AddressKeyType { get; set; } + public string TransferId { get; set; } + public string ExternalReference { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Pix/PixTransactionExternalAccount.cs b/Codout.Apis.Asaas/Models/Pix/PixTransactionExternalAccount.cs new file mode 100644 index 0000000..1ba9af4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixTransactionExternalAccount.cs @@ -0,0 +1,13 @@ +using Codout.Apis.Asaas.Models.Pix.Enums; + +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixTransactionExternalAccount +{ + public string Ispb { get; set; } + public string IspbName { get; set; } + public string Name { get; set; } + public string CpfCnpj { get; set; } + public string AddressKey { get; set; } + public PixAddressKeyType? AddressKeyType { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixTransactionListFilter.cs b/Codout.Apis.Asaas/Models/Pix/PixTransactionListFilter.cs new file mode 100644 index 0000000..4840ccf --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixTransactionListFilter.cs @@ -0,0 +1,25 @@ +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.Pix.Enums; + +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixTransactionListFilter : RequestParameters +{ + public PixTransactionStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + + public PixTransactionType? Type + { + get => Get("type"); + set => Add("type", value); + } + + public string EndToEndIdentifier + { + get => this["endToEndIdentifier"]; + set => Add("endToEndIdentifier", value); + } +} diff --git a/Codout.Apis.Asaas/Models/Pix/PixTransactionQrCode.cs b/Codout.Apis.Asaas/Models/Pix/PixTransactionQrCode.cs new file mode 100644 index 0000000..5d1ab5b --- /dev/null +++ b/Codout.Apis.Asaas/Models/Pix/PixTransactionQrCode.cs @@ -0,0 +1,22 @@ +using System; + +namespace Codout.Apis.Asaas.Models.Pix; + +public class PixTransactionQrCode +{ + public PixTransactionQrCodePayer Payer { get; set; } + public string ConciliationIdentifier { get; set; } + public decimal? OriginalValue { get; set; } + public DateTime? DueDate { get; set; } + public decimal? Interest { get; set; } + public decimal? Fine { get; set; } + public decimal? Discount { get; set; } + public DateTime? ExpirationDate { get; set; } + public string Description { get; set; } +} + +public class PixTransactionQrCodePayer +{ + public string Name { get; set; } + public string CpfCnpj { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticAuthorizationRequest.cs b/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticAuthorizationRequest.cs new file mode 100644 index 0000000..da73c8c --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticAuthorizationRequest.cs @@ -0,0 +1,18 @@ +using System; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class CreatePixAutomaticAuthorizationRequest +{ + public PixAutomaticRecurringFrequency Frequency { get; set; } + public string ContractId { get; set; } + public DateTime StartDate { get; set; } + public DateTime? FinishDate { get; set; } + public decimal? Value { get; set; } + public string Description { get; set; } + public string CustomerId { get; set; } + public CreatePixAutomaticImmediateQrCodeRequest ImmediateQrCode { get; set; } + public decimal? MinLimitValue { get; set; } + public PixAutomaticPaymentCreationMode? PaymentCreationMode { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticImmediateQrCodeRequest.cs b/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticImmediateQrCodeRequest.cs new file mode 100644 index 0000000..c0b0d0e --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/CreatePixAutomaticImmediateQrCodeRequest.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class CreatePixAutomaticImmediateQrCodeRequest +{ + public string PixKey { get; set; } + public int ExpirationSeconds { get; set; } + public decimal OriginalValue { get; set; } + public string Description { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticAuthorizationStatus.cs b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticAuthorizationStatus.cs new file mode 100644 index 0000000..53f31ac --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticAuthorizationStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +public enum PixAutomaticAuthorizationStatus +{ + CREATED, + ACTIVE, + CANCELLED, + REFUSED, + EXPIRED +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticOriginType.cs b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticOriginType.cs new file mode 100644 index 0000000..47bdb1a --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticOriginType.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +public enum PixAutomaticOriginType +{ + IMMEDIATE_PAYMENT_AND_RECURRING_QR_CODE, + PAYMENT_AND_RECURRING_OFFER_QR_CODE +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentCreationMode.cs b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentCreationMode.cs new file mode 100644 index 0000000..f644603 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentCreationMode.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +public enum PixAutomaticPaymentCreationMode +{ + MANUAL, + SUBSCRIPTION +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentInstructionStatus.cs b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentInstructionStatus.cs new file mode 100644 index 0000000..9f91119 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticPaymentInstructionStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +public enum PixAutomaticPaymentInstructionStatus +{ + AWAITING_REQUEST, + SCHEDULED, + DONE, + CANCELLED, + REFUSED +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticRecurringFrequency.cs b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticRecurringFrequency.cs new file mode 100644 index 0000000..01f2cf7 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/Enums/PixAutomaticRecurringFrequency.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +public enum PixAutomaticRecurringFrequency +{ + WEEKLY, + MONTHLY, + QUARTERLY, + SEMIANNUALLY, + ANNUALLY +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorization.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorization.cs new file mode 100644 index 0000000..9e23dba --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorization.cs @@ -0,0 +1,26 @@ +using System; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class PixAutomaticAuthorization +{ + public string Id { get; set; } + public decimal? MinLimitValue { get; set; } + public DateTime? CancellationDate { get; set; } + public string CancellationReason { get; set; } + public string ContractId { get; set; } + public string CustomerId { get; set; } + public string Description { get; set; } + public DateTime? FinishDate { get; set; } + public PixAutomaticRecurringFrequency? Frequency { get; set; } + public string EndToEndIdentifier { get; set; } + public DateTime? StartDate { get; set; } + public PixAutomaticAuthorizationStatus Status { get; set; } + public decimal? Value { get; set; } + public string Payload { get; set; } + public string EncodedImage { get; set; } + public PixAutomaticImmediateQrCode ImmediateQrCode { get; set; } + public PixAutomaticOriginType? OriginType { get; set; } + public string SubscriptionId { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorizationListFilter.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorizationListFilter.cs new file mode 100644 index 0000000..e839479 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorizationListFilter.cs @@ -0,0 +1,18 @@ +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class PixAutomaticAuthorizationListFilter : RequestParameters +{ + public PixAutomaticAuthorizationStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + public string CustomerId + { + get => this["customerId"]; + set => Add("customerId", value); + } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticImmediateQrCode.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticImmediateQrCode.cs new file mode 100644 index 0000000..a390c0c --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticImmediateQrCode.cs @@ -0,0 +1,9 @@ +using System; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class PixAutomaticImmediateQrCode +{ + public string ConciliationIdentifier { get; set; } + public DateTime? ExpirationDate { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstruction.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstruction.cs new file mode 100644 index 0000000..d3ce1c4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstruction.cs @@ -0,0 +1,15 @@ +using System; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class PixAutomaticPaymentInstruction +{ + public string Id { get; set; } + public string EndToEndIdentifier { get; set; } + public PixAutomaticPaymentInstructionAuthorization Authorization { get; set; } + public DateTime? DueDate { get; set; } + public PixAutomaticPaymentInstructionStatus Status { get; set; } + public string PaymentId { get; set; } + public string RefusalReason { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionAuthorization.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionAuthorization.cs new file mode 100644 index 0000000..4c2f368 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionAuthorization.cs @@ -0,0 +1,12 @@ +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +/// +/// Subobjeto com referencia minima a autorizacao linkada a uma payment instruction. +/// Aparece dentro de . +/// +public class PixAutomaticPaymentInstructionAuthorization +{ + public string Id { get; set; } + public string EndToEndIdentifier { get; set; } + public string CustomerId { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionListFilter.cs b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionListFilter.cs new file mode 100644 index 0000000..969df0f --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticPaymentInstructionListFilter.cs @@ -0,0 +1,31 @@ +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.PixAutomatic.Enums; + +namespace Codout.Apis.Asaas.Models.PixAutomatic; + +public class PixAutomaticPaymentInstructionListFilter : RequestParameters +{ + public string AuthorizationId + { + get => this["authorizationId"]; + set => Add("authorizationId", value); + } + + public string CustomerId + { + get => this["customerId"]; + set => Add("customerId", value); + } + + public string PaymentId + { + get => this["paymentId"]; + set => Add("paymentId", value); + } + + public PixAutomaticPaymentInstructionStatus? Status + { + get => Get("status"); + set => Add("status", value); + } +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringFrequency.cs b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringFrequency.cs new file mode 100644 index 0000000..5217439 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringFrequency.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.PixRecurring.Enums; + +public enum PixRecurringFrequency +{ + WEEKLY, + MONTHLY +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringItemStatus.cs b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringItemStatus.cs new file mode 100644 index 0000000..3032404 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringItemStatus.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.PixRecurring.Enums; + +public enum PixRecurringItemStatus +{ + PENDING, + CANCELLED, + REFUSED, + DONE +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringOrigin.cs b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringOrigin.cs new file mode 100644 index 0000000..6a929ab --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringOrigin.cs @@ -0,0 +1,6 @@ +namespace Codout.Apis.Asaas.Models.PixRecurring.Enums; + +public enum PixRecurringOrigin +{ + PIX +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringStatus.cs b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringStatus.cs new file mode 100644 index 0000000..0dee396 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/Enums/PixRecurringStatus.cs @@ -0,0 +1,10 @@ +namespace Codout.Apis.Asaas.Models.PixRecurring.Enums; + +public enum PixRecurringStatus +{ + AWAITING_CRITICAL_ACTION_AUTHORIZATION, + PENDING, + SCHEDULED, + CANCELLED, + DONE +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringExternalAccount.cs b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringExternalAccount.cs new file mode 100644 index 0000000..21001a4 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringExternalAccount.cs @@ -0,0 +1,9 @@ +namespace Codout.Apis.Asaas.Models.PixRecurring; + +public class PixRecurringExternalAccount +{ + public string Name { get; set; } + public string FinancialInstitutionName { get; set; } + public string CpfCnpj { get; set; } + public string PixKey { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItem.cs b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItem.cs new file mode 100644 index 0000000..9d63983 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItem.cs @@ -0,0 +1,17 @@ +using System; +using Codout.Apis.Asaas.Models.PixRecurring.Enums; + +namespace Codout.Apis.Asaas.Models.PixRecurring; + +public class PixRecurringItem +{ + public string Id { get; set; } + public PixRecurringItemStatus Status { get; set; } + public DateTime? ScheduledDate { get; set; } + public bool? CanBeCancelled { get; set; } + public int? RecurrenceNumber { get; set; } + public int? Quantity { get; set; } + public decimal Value { get; set; } + public string RefusalReasonDescription { get; set; } + public PixRecurringExternalAccount ExternalAccount { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItemsResponse.cs b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItemsResponse.cs new file mode 100644 index 0000000..1b9324e --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItemsResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Codout.Apis.Asaas.Models.PixRecurring; + +public class PixRecurringItemsResponse +{ + public List Data { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransaction.cs b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransaction.cs new file mode 100644 index 0000000..0d0ec1c --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransaction.cs @@ -0,0 +1,18 @@ +using System; +using Codout.Apis.Asaas.Models.PixRecurring.Enums; + +namespace Codout.Apis.Asaas.Models.PixRecurring; + +public class PixRecurringTransaction +{ + public string Id { get; set; } + public PixRecurringStatus Status { get; set; } + public PixRecurringOrigin? Origin { get; set; } + public decimal Value { get; set; } + public PixRecurringFrequency? Frequency { get; set; } + public int Quantity { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? FinishDate { get; set; } + public bool? CanBeCancelled { get; set; } + public PixRecurringExternalAccount ExternalAccount { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransactionListFilter.cs b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransactionListFilter.cs new file mode 100644 index 0000000..176d466 --- /dev/null +++ b/Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransactionListFilter.cs @@ -0,0 +1,25 @@ +using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Models.PixRecurring.Enums; + +namespace Codout.Apis.Asaas.Models.PixRecurring; + +public class PixRecurringTransactionListFilter : RequestParameters +{ + public PixRecurringStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + + public decimal? Value + { + get => Get("value"); + set => Add("value", value); + } + + public string SearchText + { + get => this["searchText"]; + set => Add("searchText", value); + } +} diff --git a/Codout.Apis.Asaas/Models/Subscription/Enums/Cycle.cs b/Codout.Apis.Asaas/Models/Subscription/Enums/Cycle.cs index 891caa2..66cac95 100644 --- a/Codout.Apis.Asaas/Models/Subscription/Enums/Cycle.cs +++ b/Codout.Apis.Asaas/Models/Subscription/Enums/Cycle.cs @@ -5,6 +5,7 @@ public enum Cycle WEEKLY, BIWEEKLY, MONTHLY, + BIMONTHLY, QUARTERLY, SEMIANNUALLY, YEARLY diff --git a/Codout.Apis.Asaas/Models/Subscription/Enums/SubscriptionStatus.cs b/Codout.Apis.Asaas/Models/Subscription/Enums/SubscriptionStatus.cs index 61141e6..c711156 100644 --- a/Codout.Apis.Asaas/Models/Subscription/Enums/SubscriptionStatus.cs +++ b/Codout.Apis.Asaas/Models/Subscription/Enums/SubscriptionStatus.cs @@ -3,7 +3,8 @@ public enum SubscriptionStatus { ACTIVE, - EXPIRED + EXPIRED, + INACTIVE } public static class SubscriptionStatusExtension @@ -17,5 +18,10 @@ public static bool IsExpired(this SubscriptionStatus status) { return status == SubscriptionStatus.EXPIRED; } + + public static bool IsInactive(this SubscriptionStatus status) + { + return status == SubscriptionStatus.INACTIVE; + } } } diff --git a/Codout.Apis.Asaas/Models/Subscription/Subscription.cs b/Codout.Apis.Asaas/Models/Subscription/Subscription.cs index 2cb5fc1..c3f5636 100644 --- a/Codout.Apis.Asaas/Models/Subscription/Subscription.cs +++ b/Codout.Apis.Asaas/Models/Subscription/Subscription.cs @@ -1,45 +1,56 @@ -using System; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; using Codout.Apis.Asaas.Models.Common; using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Payment; using Codout.Apis.Asaas.Models.Subscription.Enums; -using System.Text.Json.Serialization; -namespace Codout.Apis.Asaas.Models.Subscription +namespace Codout.Apis.Asaas.Models.Subscription; + +public class Subscription { - public class Subscription { - public string Id { get; set; } + public string Object { get; set; } + + public string Id { get; set; } + + public DateTime? DateCreated { get; set; } + + [JsonPropertyName("customer")] + public string CustomerId { get; set; } + + [JsonPropertyName("paymentLink")] + public string PaymentLinkId { get; set; } - public DateTime DateCreated { get; set; } + public BillingType BillingType { get; set; } - [JsonPropertyName("customer")] - public string CustomerId { get; set; } + public decimal Value { get; set; } - public BillingType BillingType { get; set; } + public DateTime? NextDueDate { get; set; } - public decimal Value { get; set; } + public Discount Discount { get; set; } - public DateTime NextDueDate { get; set; } + public Interest Interest { get; set; } - public Discount Discount { get; set; } + public Fine Fine { get; set; } - public Interest Interest { get; set; } + public Cycle Cycle { get; set; } - public Fine Fine { get; set; } + public string Description { get; set; } - public Cycle Cycle { get; set; } + public DateTime? EndDate { get; set; } - public string Description { get; set; } + public int? MaxPayments { get; set; } - public DateTime? EndDate { get; set; } + public SubscriptionStatus Status { get; set; } - public int? MaxPayments { get; set; } + public string ExternalReference { get; set; } - public SubscriptionStatus Status { get; set; } + public string CheckoutSession { get; set; } - public string ExternalReference { get; set; } + public Common.CreditCard CreditCard { get; set; } - public Common.CreditCard CreditCard { get; set; } + public bool? Deleted { get; set; } - public bool Deleted { get; set; } - } + public List Split { get; set; } = []; } diff --git a/Codout.Apis.Asaas/Models/Subscription/SubscriptionListFilter.cs b/Codout.Apis.Asaas/Models/Subscription/SubscriptionListFilter.cs index ed729ca..317d958 100644 --- a/Codout.Apis.Asaas/Models/Subscription/SubscriptionListFilter.cs +++ b/Codout.Apis.Asaas/Models/Subscription/SubscriptionListFilter.cs @@ -1,26 +1,62 @@ -using Codout.Apis.Asaas.Core; +using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Models.Common.Enums; +using Codout.Apis.Asaas.Models.Subscription.Enums; -namespace Codout.Apis.Asaas.Models.Subscription +namespace Codout.Apis.Asaas.Models.Subscription; + +public class SubscriptionListFilter : RequestParameters { - public class SubscriptionListFilter : RequestParameters - { - public string CustomerId - { - get => this["customer"]; - set => Add("customer", value); - } - - public BillingType? BillingType - { - get => Get("billingType"); - set => Add("billingType", value); - } - - public bool? IncludeDeleted - { - get => Get("includeDeleted"); - set => Add("includeDeleted", value); - } + public string CustomerId + { + get => this["customer"]; + set => Add("customer", value); + } + + public string CustomerGroupName + { + get => this["customerGroupName"]; + set => Add("customerGroupName", value); + } + + public BillingType? BillingType + { + get => Get("billingType"); + set => Add("billingType", value); + } + + public SubscriptionStatus? Status + { + get => Get("status"); + set => Add("status", value); + } + + public bool? IncludeDeleted + { + get => Get("includeDeleted"); + set => Add("includeDeleted", value); + } + + public bool? DeletedOnly + { + get => Get("deletedOnly"); + set => Add("deletedOnly", value); + } + + public string ExternalReference + { + get => this["externalReference"]; + set => Add("externalReference", value); + } + + public string Order + { + get => this["order"]; + set => Add("order", value); + } + + public string Sort + { + get => this["sort"]; + set => Add("sort", value); } } diff --git a/Codout.Apis.Asaas/Models/Subscription/UpdateSubscriptionCreditCardRequest.cs b/Codout.Apis.Asaas/Models/Subscription/UpdateSubscriptionCreditCardRequest.cs new file mode 100644 index 0000000..c6c8424 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Subscription/UpdateSubscriptionCreditCardRequest.cs @@ -0,0 +1,11 @@ +using Codout.Apis.Asaas.Models.Common; + +namespace Codout.Apis.Asaas.Models.Subscription; + +public class UpdateSubscriptionCreditCardRequest +{ + public CreditCardRequest CreditCard { get; set; } + public CreditCardHolderInfoRequest CreditCardHolderInfo { get; set; } + public string CreditCardToken { get; set; } + public string RemoteIp { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Transfer/Bank.cs b/Codout.Apis.Asaas/Models/Transfer/Bank.cs index 0620663..b931e20 100644 --- a/Codout.Apis.Asaas/Models/Transfer/Bank.cs +++ b/Codout.Apis.Asaas/Models/Transfer/Bank.cs @@ -1,6 +1,8 @@ -namespace Codout.Apis.Asaas.Models.Transfer +namespace Codout.Apis.Asaas.Models.Transfer; + +public class Bank { - public class Bank { - public string Code { get; set; } - } + public string Ispb { get; set; } + public string Code { get; set; } + public string Name { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Transfer/BankAccount.cs b/Codout.Apis.Asaas/Models/Transfer/BankAccount.cs index 0be01b5..9ea51d9 100644 --- a/Codout.Apis.Asaas/Models/Transfer/BankAccount.cs +++ b/Codout.Apis.Asaas/Models/Transfer/BankAccount.cs @@ -1,25 +1,20 @@ -using System; +using System; using Codout.Apis.Asaas.Models.Transfer.Enums; -namespace Codout.Apis.Asaas.Models.Transfer -{ - public class BankAccount { - public Bank Bank { get; set; } - - public string AccountName { get; set; } - - public string OwnerName { get; set; } - - public DateTime? OwnerBirthDate { get; set; } - - public string CpfCnpj { get; set; } +namespace Codout.Apis.Asaas.Models.Transfer; - public string Agency { get; set; } - - public string Account { get; set; } - - public string AccountDigit { get; set; } - - public BankAccountType BankAccountType { get; set; } - } +public class BankAccount +{ + public Bank Bank { get; set; } + public string AccountName { get; set; } + public string OwnerName { get; set; } + public DateTime? OwnerBirthDate { get; set; } + public string CpfCnpj { get; set; } + public string Agency { get; set; } + public string AgencyDigit { get; set; } + public string Account { get; set; } + public string AccountDigit { get; set; } + public BankAccountType BankAccountType { get; set; } + public string PixAddressKey { get; set; } + public string Ispb { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Transfer/BankAccountTransfer.cs b/Codout.Apis.Asaas/Models/Transfer/BankAccountTransfer.cs index 5f351fc..7828c94 100644 --- a/Codout.Apis.Asaas/Models/Transfer/BankAccountTransfer.cs +++ b/Codout.Apis.Asaas/Models/Transfer/BankAccountTransfer.cs @@ -4,8 +4,6 @@ namespace Codout.Apis.Asaas.Models.Transfer { public class BankAccountTransfer : BaseTransfer { - public decimal NetValue { get; set; } - public BankAccountTransferStatus Status { get; set; } public BankAccount BankAccount { get; set; } diff --git a/Codout.Apis.Asaas/Models/Transfer/Base/BaseTransfer.cs b/Codout.Apis.Asaas/Models/Transfer/Base/BaseTransfer.cs index 7582bb7..18a0130 100644 --- a/Codout.Apis.Asaas/Models/Transfer/Base/BaseTransfer.cs +++ b/Codout.Apis.Asaas/Models/Transfer/Base/BaseTransfer.cs @@ -1,25 +1,41 @@ -using System; +using System; using Codout.Apis.Asaas.Models.Transfer.Enums; -namespace Codout.Apis.Asaas.Models.Transfer.Base +namespace Codout.Apis.Asaas.Models.Transfer.Base; + +public class BaseTransfer { - public class BaseTransfer { - public string Id { get; set; } + public string Object { get; set; } + + public string Id { get; set; } + + public TransferType Type { get; set; } + + public TransferOperationType? OperationType { get; set; } + + public DateTime? DateCreated { get; set; } + + public decimal Value { get; set; } + + public decimal NetValue { get; set; } + + public decimal TransferFee { get; set; } + + public DateTime? EffectiveDate { get; set; } - public TransferType Type { get; set; } + public DateTime? ScheduleDate { get; set; } - public DateTime DateCreated { get; set; } + public string EndToEndIdentifier { get; set; } - public decimal Value { get; set; } + public bool? Authorized { get; set; } - public decimal TransferFee { get; set; } + public string FailReason { get; set; } - public DateTime? EffectiveDate { get; set; } + public string ExternalReference { get; set; } - public DateTime? ScheduleDate { get; set; } + public string TransactionReceiptUrl { get; set; } - public bool Authorized { get; set; } + public string Description { get; set; } - public string TransactionReceiptUrl { get; set; } - } + public string Recurring { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Transfer/Enums/AsaasAccountTransferStatus.cs b/Codout.Apis.Asaas/Models/Transfer/Enums/AsaasAccountTransferStatus.cs index 607000d..55cd5b2 100644 --- a/Codout.Apis.Asaas/Models/Transfer/Enums/AsaasAccountTransferStatus.cs +++ b/Codout.Apis.Asaas/Models/Transfer/Enums/AsaasAccountTransferStatus.cs @@ -1,9 +1,15 @@ -namespace Codout.Apis.Asaas.Models.Transfer.Enums +namespace Codout.Apis.Asaas.Models.Transfer.Enums; + +/// +/// Schema oficial unifica todos os tipos de transfer no mesmo enum +/// (PENDING, BANK_PROCESSING, DONE, CANCELLED, FAILED). Antes faltavam +/// BANK_PROCESSING e FAILED em AsaasAccountTransferStatus. +/// +public enum AsaasAccountTransferStatus { - public enum AsaasAccountTransferStatus - { - PENDING, - DONE, - CANCELLED - } + PENDING, + BANK_PROCESSING, + DONE, + CANCELLED, + FAILED } diff --git a/Codout.Apis.Asaas/Models/Transfer/Enums/TransferOperationType.cs b/Codout.Apis.Asaas/Models/Transfer/Enums/TransferOperationType.cs new file mode 100644 index 0000000..9c6a002 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Transfer/Enums/TransferOperationType.cs @@ -0,0 +1,13 @@ +namespace Codout.Apis.Asaas.Models.Transfer.Enums; + +/// +/// Modalidade da transferencia retornada pelo campo operationType do schema: +/// PIX (chave Pix), TED (transferencia para banco diferente), INTERNAL (entre +/// contas Asaas). +/// +public enum TransferOperationType +{ + PIX, + TED, + INTERNAL +} diff --git a/Codout.Apis.Asaas/Models/Transfer/TransferListFilter.cs b/Codout.Apis.Asaas/Models/Transfer/TransferListFilter.cs index 4ff8be4..e5f34b9 100644 --- a/Codout.Apis.Asaas/Models/Transfer/TransferListFilter.cs +++ b/Codout.Apis.Asaas/Models/Transfer/TransferListFilter.cs @@ -1,21 +1,44 @@ -using System; +using System; using Codout.Apis.Asaas.Core; using Codout.Apis.Asaas.Models.Transfer.Enums; -namespace Codout.Apis.Asaas.Models.Transfer +namespace Codout.Apis.Asaas.Models.Transfer; + +public class TransferListFilter : RequestParameters { - public class TransferListFilter : RequestParameters + public DateTime? DateCreated + { + get => Get("dateCreated"); + set => Add("dateCreated", value); + } + + public DateTime? DateCreatedGE { - public DateTime? DateCreated - { - get => Get("dateCreated"); - set => Add("dateCreated", value); - } + get => Get("dateCreated[ge]"); + set => Add("dateCreated[ge]", value); + } + + public DateTime? DateCreatedLE + { + get => Get("dateCreated[le]"); + set => Add("dateCreated[le]", value); + } - public TransferType? TransferType - { - get => Get("type"); - set => Add("type", value); - } + public DateTime? TransferDateGE + { + get => Get("transferDate[ge]"); + set => Add("transferDate[ge]", value); + } + + public DateTime? TransferDateLE + { + get => Get("transferDate[le]"); + set => Add("transferDate[le]", value); + } + + public TransferType? TransferType + { + get => Get("type"); + set => Add("type", value); } } diff --git a/Codout.Apis.Asaas/Models/Wallet/Wallet.cs b/Codout.Apis.Asaas/Models/Wallet/Wallet.cs index 1bbdccd..d0fd6a1 100644 --- a/Codout.Apis.Asaas/Models/Wallet/Wallet.cs +++ b/Codout.Apis.Asaas/Models/Wallet/Wallet.cs @@ -1,6 +1,7 @@ -namespace Codout.Apis.Asaas.Models.Wallet +namespace Codout.Apis.Asaas.Models.Wallet; + +public class Wallet { - public class Wallet { - public string Id { get; set; } - } + public string Object { get; set; } + public string Id { get; set; } } diff --git a/Codout.Apis.Asaas/Models/Webhook/CreateWebhookRequest.cs b/Codout.Apis.Asaas/Models/Webhook/CreateWebhookRequest.cs new file mode 100644 index 0000000..6dda682 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Webhook/CreateWebhookRequest.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Webhook.Enums; + +namespace Codout.Apis.Asaas.Models.Webhook; + +public class CreateWebhookRequest +{ + public string Name { get; set; } + + public string Url { get; set; } + + public string Email { get; set; } + + public bool Enabled { get; set; } + + public bool Interrupted { get; set; } + + public int ApiVersion { get; set; } = 3; + + public string AuthToken { get; set; } + + public WebhookSendType SendType { get; set; } = WebhookSendType.SEQUENTIALLY; + + public List Events { get; set; } = []; +} diff --git a/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookEvent.cs b/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookEvent.cs new file mode 100644 index 0000000..b4c7c84 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookEvent.cs @@ -0,0 +1,116 @@ +namespace Codout.Apis.Asaas.Models.Webhook.Enums; + +public enum WebhookEvent +{ + PAYMENT_AUTHORIZED, + PAYMENT_AWAITING_RISK_ANALYSIS, + PAYMENT_APPROVED_BY_RISK_ANALYSIS, + PAYMENT_REPROVED_BY_RISK_ANALYSIS, + PAYMENT_CREATED, + PAYMENT_UPDATED, + PAYMENT_CONFIRMED, + PAYMENT_RECEIVED, + PAYMENT_ANTICIPATED, + PAYMENT_OVERDUE, + PAYMENT_DELETED, + PAYMENT_RESTORED, + PAYMENT_REFUNDED, + PAYMENT_REFUND_IN_PROGRESS, + PAYMENT_REFUND_DENIED, + PAYMENT_RECEIVED_IN_CASH_UNDONE, + PAYMENT_CHARGEBACK_REQUESTED, + PAYMENT_CHARGEBACK_DISPUTE, + PAYMENT_AWAITING_CHARGEBACK_REVERSAL, + PAYMENT_DUNNING_RECEIVED, + PAYMENT_DUNNING_REQUESTED, + PAYMENT_BANK_SLIP_CANCELLED, + PAYMENT_BANK_SLIP_VIEWED, + PAYMENT_CHECKOUT_VIEWED, + PAYMENT_CREDIT_CARD_CAPTURE_REFUSED, + PAYMENT_PARTIALLY_REFUNDED, + PAYMENT_SPLIT_CANCELLED, + PAYMENT_SPLIT_DIVERGENCE_BLOCK, + PAYMENT_SPLIT_DIVERGENCE_BLOCK_FINISHED, + INVOICE_CREATED, + INVOICE_UPDATED, + INVOICE_SYNCHRONIZED, + INVOICE_AUTHORIZED, + INVOICE_PROCESSING_CANCELLATION, + INVOICE_CANCELED, + INVOICE_CANCELLATION_DENIED, + INVOICE_ERROR, + TRANSFER_CREATED, + TRANSFER_PENDING, + TRANSFER_IN_BANK_PROCESSING, + TRANSFER_BLOCKED, + TRANSFER_DONE, + TRANSFER_FAILED, + TRANSFER_CANCELLED, + BILL_CREATED, + BILL_PENDING, + BILL_BANK_PROCESSING, + BILL_PAID, + BILL_CANCELLED, + BILL_FAILED, + BILL_REFUNDED, + RECEIVABLE_ANTICIPATION_CANCELLED, + RECEIVABLE_ANTICIPATION_SCHEDULED, + RECEIVABLE_ANTICIPATION_PENDING, + RECEIVABLE_ANTICIPATION_CREDITED, + RECEIVABLE_ANTICIPATION_DEBITED, + RECEIVABLE_ANTICIPATION_DENIED, + RECEIVABLE_ANTICIPATION_OVERDUE, + MOBILE_PHONE_RECHARGE_PENDING, + MOBILE_PHONE_RECHARGE_CANCELLED, + MOBILE_PHONE_RECHARGE_CONFIRMED, + MOBILE_PHONE_RECHARGE_REFUNDED, + ACCOUNT_STATUS_BANK_ACCOUNT_INFO_APPROVED, + ACCOUNT_STATUS_BANK_ACCOUNT_INFO_AWAITING_APPROVAL, + ACCOUNT_STATUS_BANK_ACCOUNT_INFO_PENDING, + ACCOUNT_STATUS_BANK_ACCOUNT_INFO_REJECTED, + ACCOUNT_STATUS_COMMERCIAL_INFO_APPROVED, + ACCOUNT_STATUS_COMMERCIAL_INFO_AWAITING_APPROVAL, + ACCOUNT_STATUS_COMMERCIAL_INFO_EXPIRED, + ACCOUNT_STATUS_COMMERCIAL_INFO_EXPIRING_SOON, + ACCOUNT_STATUS_COMMERCIAL_INFO_PENDING, + ACCOUNT_STATUS_COMMERCIAL_INFO_REJECTED, + ACCOUNT_STATUS_DOCUMENT_APPROVED, + ACCOUNT_STATUS_DOCUMENT_AWAITING_APPROVAL, + ACCOUNT_STATUS_DOCUMENT_PENDING, + ACCOUNT_STATUS_DOCUMENT_REJECTED, + ACCOUNT_STATUS_GENERAL_APPROVAL_APPROVED, + ACCOUNT_STATUS_GENERAL_APPROVAL_AWAITING_APPROVAL, + ACCOUNT_STATUS_GENERAL_APPROVAL_PENDING, + ACCOUNT_STATUS_GENERAL_APPROVAL_REJECTED, + SUBSCRIPTION_CREATED, + SUBSCRIPTION_UPDATED, + SUBSCRIPTION_INACTIVATED, + SUBSCRIPTION_DELETED, + SUBSCRIPTION_SPLIT_DISABLED, + SUBSCRIPTION_SPLIT_DIVERGENCE_BLOCK, + SUBSCRIPTION_SPLIT_DIVERGENCE_BLOCK_FINISHED, + CHECKOUT_CREATED, + CHECKOUT_CANCELED, + CHECKOUT_EXPIRED, + CHECKOUT_PAID, + BALANCE_VALUE_BLOCKED, + BALANCE_VALUE_UNBLOCKED, + INTERNAL_TRANSFER_CREDIT, + INTERNAL_TRANSFER_DEBIT, + ACCESS_TOKEN_CREATED, + ACCESS_TOKEN_DELETED, + ACCESS_TOKEN_DISABLED, + ACCESS_TOKEN_ENABLED, + ACCESS_TOKEN_EXPIRED, + ACCESS_TOKEN_EXPIRING_SOON, + PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CREATED, + PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED, + PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CANCELLED, + PIX_AUTOMATIC_RECURRING_AUTHORIZATION_EXPIRED, + PIX_AUTOMATIC_RECURRING_AUTHORIZATION_REFUSED, + PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CREATED, + PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_SCHEDULED, + PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_REFUSED, + PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CANCELLED, + PIX_AUTOMATIC_RECURRING_ELIGIBILITY_UPDATED +} diff --git a/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookSendType.cs b/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookSendType.cs new file mode 100644 index 0000000..f9d9646 --- /dev/null +++ b/Codout.Apis.Asaas/Models/Webhook/Enums/WebhookSendType.cs @@ -0,0 +1,7 @@ +namespace Codout.Apis.Asaas.Models.Webhook.Enums; + +public enum WebhookSendType +{ + SEQUENTIALLY, + NON_SEQUENTIALLY +} diff --git a/Codout.Apis.Asaas/Models/Webhook/UpdateWebhookRequest.cs b/Codout.Apis.Asaas/Models/Webhook/UpdateWebhookRequest.cs new file mode 100644 index 0000000..b2c362a --- /dev/null +++ b/Codout.Apis.Asaas/Models/Webhook/UpdateWebhookRequest.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Webhook.Enums; + +namespace Codout.Apis.Asaas.Models.Webhook; + +public class UpdateWebhookRequest +{ + public string Name { get; set; } + + public string Url { get; set; } + + public string Email { get; set; } + + public bool? Enabled { get; set; } + + public bool? Interrupted { get; set; } + + public int? ApiVersion { get; set; } + + public string AuthToken { get; set; } + + public WebhookSendType? SendType { get; set; } + + public List Events { get; set; } +} diff --git a/Codout.Apis.Asaas/Models/Webhook/Webhook.cs b/Codout.Apis.Asaas/Models/Webhook/Webhook.cs index b7066d2..d1a4523 100644 --- a/Codout.Apis.Asaas/Models/Webhook/Webhook.cs +++ b/Codout.Apis.Asaas/Models/Webhook/Webhook.cs @@ -1,16 +1,29 @@ -namespace Codout.Apis.Asaas.Models.Webhook +using System.Collections.Generic; +using Codout.Apis.Asaas.Models.Webhook.Enums; + +namespace Codout.Apis.Asaas.Models.Webhook; + +public class Webhook { - public class Webhook { - public string Url { get; set; } + public string Id { get; set; } + + public string Name { get; set; } + + public string Url { get; set; } + + public string Email { get; set; } + + public bool Enabled { get; set; } + + public bool Interrupted { get; set; } - public string Email { get; set; } + public int ApiVersion { get; set; } - public int ApiVersion { get; set; } + public bool HasAuthToken { get; set; } - public bool Enabled { get; set; } + public WebhookSendType SendType { get; set; } - public bool Interrupted { get; set; } + public int PenalizedRequestsCount { get; set; } - public string AuthToken { get; set; } - } + public List Events { get; set; } = []; } diff --git a/Codout.Apis.Asaas/Models/Webhook/WebhookListFilter.cs b/Codout.Apis.Asaas/Models/Webhook/WebhookListFilter.cs new file mode 100644 index 0000000..31f5adb --- /dev/null +++ b/Codout.Apis.Asaas/Models/Webhook/WebhookListFilter.cs @@ -0,0 +1,24 @@ +using Codout.Apis.Asaas.Core; + +namespace Codout.Apis.Asaas.Models.Webhook; + +public class WebhookListFilter : RequestParameters +{ + public string Name + { + get => this["name"]; + set => Add("name", value); + } + + public bool? Enabled + { + get => Get("enabled"); + set => Add("enabled", value); + } + + public bool? Interrupted + { + get => Get("interrupted"); + set => Add("interrupted", value); + } +} diff --git a/Codout.Apis.Asaas/Models/Webhook/WebhookRequest.cs b/Codout.Apis.Asaas/Models/Webhook/WebhookRequest.cs deleted file mode 100644 index 27a3d2d..0000000 --- a/Codout.Apis.Asaas/Models/Webhook/WebhookRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Codout.Apis.Asaas.Models.Webhook -{ - public class WebhookRequest { - public string Url { get; set; } - - public string Email { get; set; } - - public int ApiVersion { get; set; } - - public bool Enabled { get; set; } - - public bool Interrupted { get; set; } - - public string AuthToken { get; set; } - } -} diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..6d2c7ea --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,367 @@ +# Plano de Implementação — Auditoria de Conformidade Asaas SDK + +> **Branch:** `audit/asaas-api-conformance` +> **Branch base:** `master` +> **Versão alvo:** `3.0.0` (breaking changes diretos, sem deprecation period) +> **Estratégia:** branch monolítica com commits atômicos (1 commit ≈ 1 PR conceitual) +> **Cobertura de testes:** ampla — cada fix e cada endpoint novo ganha teste + +--- + +## Princípios desta entrega + +1. **Cada commit deve compilar e passar nos testes existentes** — facilita bisect e revisão. +2. **Cada bug fix tem ao menos um teste de regressão** que falharia antes do fix e passa depois (usar `AssertRequestMethod` e `AssertRequestUrl`). +3. **Cada endpoint novo tem ao menos um teste de happy path + 1 de erro**. +4. **Tipos de request/response novos** ganham teste de serialização em [SerializationTests.cs](Codout.Apis.Asaas.Tests/Models/SerializationTests.cs). +5. **Breaking changes vão direto** — sem `[Obsolete]`, sem alias. Versão 3.0.0 é o sinal. +6. **Mensagens de commit em português**, padrão Conventional Commits (`fix:`, `feat:`, `refactor:`, `feat!:` para breaking, `BREAKING CHANGE:` no rodapé quando aplicável). +7. **Manter zero dependências externas** — toda nova feature usa só `System.Text.Json` + BCL. + +--- + +## Sprint 1 — Bugs Bloqueantes (5 commits) + +Objetivo: derrubar todos os 🔴 que quebram chamadas reais. + +### Commit 1 — `fix: usar PUT em vez de POST nas atualizações` + +**Arquivos a alterar:** +- [Managers/CustomerManager.cs](Codout.Apis.Asaas/Managers/CustomerManager.cs) — `Update`: trocar `PostAsync` por `PutAsync` +- [Managers/PaymentManager.cs](Codout.Apis.Asaas/Managers/PaymentManager.cs) — `Update` +- [Managers/SubscriptionManager.cs](Codout.Apis.Asaas/Managers/SubscriptionManager.cs) — `Update`, `UpdateInvoiceSettings` +- [Managers/NotificationManager.cs](Codout.Apis.Asaas/Managers/NotificationManager.cs) — `Update`, `BatchUpdate` + +**Testes a adicionar:** +- Em [CustomerManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/CustomerManagerTests.cs): `Update_ShouldSendPutRequest()` com `AssertRequestMethod(HttpMethod.Put)` +- Análogo em PaymentManagerTests, SubscriptionManagerTests, NotificationManagerTests +- Ajustar testes existentes que usem `Assert.Equal(HttpMethod.Post, ...)` nesses casos + +**Critério de aceite:** todos os 6 métodos enviam `PUT`, todos os testes existentes continuam verdes. + +--- + +### Commit 2 — `fix(fiscalInfo): corrigir rota para /fiscalInfo e mover ListMunicipalServices` + +**Arquivos a alterar:** +- [Managers/CustomerFiscalInfoManager.cs](Codout.Apis.Asaas/Managers/CustomerFiscalInfoManager.cs) — `CustomerFiscalInfoRoute = "/fiscalInfo"` (já com `/v3` adicionado pelo BaseManager) +- [Managers/InvoiceManager.cs](Codout.Apis.Asaas/Managers/InvoiceManager.cs) — remover `ListMunicipalServices` (será movido) +- Renomear `CustomerFiscalInfoManager` → `FiscalInfoManager` (e mover `Models/CustomerFiscalInfo/` → `Models/FiscalInfo/`). Atualizar referências em `AsaasApi.cs` e em testes. +- Mover `MunicipalService` de `Models/Invoice/` → `Models/FiscalInfo/` (se o tipo for usado só nesse contexto) +- Adicionar método `ListServices(string description)` em `FiscalInfoManager` chamando `/fiscalInfo/services` + +**Testes a adicionar:** +- Em [CustomerFiscalInfoManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/CustomerFiscalInfoManagerTests.cs): renomear arquivo, `AssertRequestUrl("/v3/fiscalInfo/...")` +- Mover testes de `ListMunicipalServices` para o novo manager +- Em [InvoiceManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/InvoiceManagerTests.cs): remover teste antigo de `ListMunicipalServices` + +**Breaking change:** classe pública renomeada. Documentar no CHANGELOG. + +--- + +### Commit 3 — `fix(finance): Balance retorna objeto { balance: decimal }` + +**Arquivos a alterar:** +- Criar `Models/Finance/Balance.cs` com `[JsonPropertyName("balance")] public decimal Value { get; set; }` +- [Managers/FinanceManager.cs](Codout.Apis.Asaas/Managers/FinanceManager.cs) — assinatura: `Task> GetBalance()` + +**Testes a adicionar:** +- Em [FinanceManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/FinanceManagerTests.cs): `GetBalance_ShouldParseObjectResponse()` com payload `{"balance": 5210.96}` e `Assert.Equal(5210.96m, response.Data.Value)` +- Test de serialização do `Balance` em SerializationTests.cs + +**Breaking change:** método renomeado `Balance()` → `GetBalance()` e tipo de retorno mudou. + +--- + +### Commit 4 — `fix(anticipation): remover SignAgreement (endpoint inexistente)` + +**Arquivos a alterar:** +- [Managers/AnticipationManager.cs](Codout.Apis.Asaas/Managers/AnticipationManager.cs) — remover `SignAgreement` +- Remover `Models/Anticipation/SignAnticipationAgreementRequest.cs` + +**Testes a alterar:** +- [AnticipationManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/AnticipationManagerTests.cs) — remover teste correspondente + +**Nota:** Antes do commit, vou rodar uma busca via MCP do Asaas (`mcp__asaas__search`) por "agreement" para confirmar 100% que o endpoint não existe sob outro path. Se existir, criamos endpoint correto em vez de remover. + +**Breaking change:** método público removido. + +--- + +### Commit 5 — `feat(installments): adicionar Create e endpoints faltantes` + +**Arquivos a criar:** +- `Models/Installment/CreateInstallmentRequest.cs` — campos básicos + sem cartão +- `Models/Installment/CreateInstallmentWithCreditCardRequest.cs` — herda + cartão +- `Models/Installment/UpdateInstallmentSplitsRequest.cs` + +**Arquivos a alterar:** +- [Managers/InstallmentManager.cs](Codout.Apis.Asaas/Managers/InstallmentManager.cs) — adicionar: + - `Create(CreateInstallmentRequest)` → POST `/installments` + - `CreateWithCreditCard(CreateInstallmentWithCreditCardRequest)` → POST `/installments/` (com barra final — entender se o BaseManager preserva, **provavelmente não** — adaptar `BuildApiRoute`) + - `ListPayments(string installmentId, int offset, int limit)` → GET `/installments/{id}/payments` + - `CancelPendingPayments(string installmentId)` → DELETE `/installments/{id}/payments` + - `UpdateSplits(string installmentId, UpdateInstallmentSplitsRequest)` → PUT `/installments/{id}/splits` + +**Atenção ao detalhe da barra final:** o `BaseManager.BuildApiRoute` atualmente faz `$"/v3{(resource[0] == '/' ? string.Empty : "/")}{resource}"`. Não preserva barra final. **Vai ser preciso ajustar** ou usar uma sinalização (`AppendTrailingSlash`). + +**Testes a adicionar (em InstallmentManagerTests.cs):** +- `Create_ShouldSendPostToInstallmentsRoot()` +- `CreateWithCreditCard_ShouldSendPostToInstallmentsRootWithTrailingSlash()` — valida que a URL final é `/v3/installments/` e não `/v3/installments` +- `ListPayments_ShouldSendGet()` +- `CancelPendingPayments_ShouldSendDelete()` +- `UpdateSplits_ShouldSendPut()` + +--- + +## Sprint 2 — Reescrita do WebhookManager (1 commit grande) + +### Commit 6 — `feat!(webhooks): migrar para CRUD por ID em /v3/webhooks` + +**BREAKING CHANGE** — toda a API pública do `WebhookManager` muda. + +**Arquivos a deletar:** +- Tudo em `Models/Webhook/` (criar do zero — Webhook.cs, WebhookRequest.cs com forma antiga) + +**Arquivos a criar:** +- `Models/Webhook/Webhook.cs` — com `Id`, `Name`, `Url`, `Email`, `Enabled`, `Interrupted`, `ApiVersion`, `HasAuthToken`, `SendType`, `PenalizedRequestsCount`, `List Events` +- `Models/Webhook/CreateWebhookRequest.cs` — campos required: `Name`, `Url`, `Email`, `Enabled`, `Interrupted`, `ApiVersion`, `AuthToken` (min 32 chars), `SendType`, `Events` +- `Models/Webhook/UpdateWebhookRequest.cs` — similar mas todos opcionais +- `Models/Webhook/WebhookListFilter.cs` — query params (validar quais a API aceita) +- `Models/Webhook/Enums/WebhookSendType.cs` — `SEQUENTIALLY`, `NON_SEQUENTIALLY` +- `Models/Webhook/Enums/WebhookEvent.cs` — **~100 valores** (extrair do OpenAPI via `mcp__asaas__get-endpoint`) + +**Arquivos a alterar:** +- [Managers/WebhookManager.cs](Codout.Apis.Asaas/Managers/WebhookManager.cs) — reescrever: + - `Create(CreateWebhookRequest)` → POST `/webhooks` + - `List(int offset, int limit, WebhookListFilter? filter = null)` → GET `/webhooks` + - `Find(string id)` → GET `/webhooks/{id}` + - `Update(string id, UpdateWebhookRequest)` → PUT `/webhooks/{id}` + - `Delete(string id)` → DELETE `/webhooks/{id}` + - `RemoveBackoff(string id)` → POST `/webhooks/{id}/removeBackoff` + +**Testes a reescrever (WebhookManagerTests.cs):** +- Remover todos os testes do padrão antigo +- Adicionar 1 teste por método novo (6 testes) + 1 teste de error +- 1 teste de serialização do enum `WebhookEvent` cobrindo pelo menos 5 valores variados + +**CHANGELOG:** entrada explícita de breaking + exemplo de migração. + +--- + +## Sprint 3 — Endpoints faltantes em managers existentes (8 commits) + +### Commit 7 — `feat(payment): documentos, simulate, limits, billingInfo, viewingInfo, status, refunds, bankSlip/refund, payWithCard, captureAuthorizedPayment` + +13 endpoints novos. Subdividir em sub-tópicos no mesmo commit: + +**Modelos novos:** +- `Models/Payment/PaymentDocument.cs` +- `Models/Payment/CreatePaymentDocumentRequest.cs` (multipart) +- `Models/Payment/UpdatePaymentDocumentRequest.cs` +- `Models/Payment/PaymentBillingInfo.cs` +- `Models/Payment/PaymentViewingInfo.cs` +- `Models/Payment/PaymentStatusInfo.cs` (só `status`) +- `Models/Payment/SimulatePaymentRequest.cs` +- `Models/Payment/SimulatedPayment.cs` +- `Models/Payment/PaymentLimits.cs` +- `Models/Payment/PaymentRefund.cs` (para a lista) +- `Models/Payment/CapturePaymentRequest.cs` +- `Models/Payment/PayWithCreditCardRequest.cs` + +**PaymentManager — adicionar métodos:** +| Método | HTTP | Rota | +|---|---|---| +| `CreateWithCreditCard(CreatePaymentRequest)` | POST | `/payments/` | +| `CaptureAuthorizedPayment(string id, CapturePaymentRequest)` | POST | `/payments/{id}/captureAuthorizedPayment` | +| `PayWithCreditCard(string id, PayWithCreditCardRequest)` | POST | `/payments/{id}/payWithCreditCard` | +| `GetBillingInfo(string id)` | GET | `/payments/{id}/billingInfo` | +| `GetViewingInfo(string id)` | GET | `/payments/{id}/viewingInfo` | +| `GetStatus(string id)` | GET | `/payments/{id}/status` | +| `Simulate(SimulatePaymentRequest)` | POST | `/payments/simulate` | +| `GetLimits()` | GET | `/payments/limits` | +| `UploadDocument(string id, AsaasFile)` | POST multipart | `/payments/{id}/documents` | +| `ListDocuments(string id, int o, int l)` | GET | `/payments/{id}/documents` | +| `FindDocument(string id, string docId)` | GET | `/payments/{id}/documents/{docId}` | +| `UpdateDocument(string id, string docId, UpdatePaymentDocumentRequest)` | PUT | `/payments/{id}/documents/{docId}` | +| `DeleteDocument(string id, string docId)` | DELETE | `/payments/{id}/documents/{docId}` | +| `ListRefunds(string id, int o, int l)` | GET | `/payments/{id}/refunds` | +| `RefundBankSlip(string id)` | POST | `/payments/{id}/bankSlip/refund` | + +**Adicionar à `Payment` (response model):** `object`, `checkoutSession`, `paymentLink`, `installmentNumber`, `pixTransaction`, `pixQrCodeId`, `creditDate`, `estimatedCreditDate`, `transactionReceiptUrl`, `nossoNumero`, `anticipable`, `daysAfterDueDateToRegistrationCancellation`, `canBePaidAfterDueDate`, `chargeback`, `escrow`, `refunds`. + +**Adicionar à `CreatePaymentRequest`:** `daysAfterDueDateToRegistrationCancellation`, `callback` (com `successUrl` e `autoRedirect`), `pixAutomaticAuthorizationId`. + +**Adicionar à `PaymentStatus`:** `REFUND_IN_PROGRESS`. + +**Testes:** 1 happy + 1 erro para cada método novo (30 testes); testes de serialização para cada novo model. + +--- + +### Commit 8 — `feat(subscription): updateCreditCard` +- `UpdateCreditCard(string id, UpdateSubscriptionCreditCardRequest)` → PUT `/subscriptions/{id}/creditCard` + +### Commit 9 — `feat(anticipation): cancel, limits, configurations` +- `Cancel(string id)` → POST `/anticipations/{id}/cancel` +- `GetLimits()` → GET `/anticipations/limits` +- `GetAutomaticConfiguration()` → GET `/anticipations/configurations` +- `UpdateAutomaticConfiguration(UpdateAnticipationConfigRequest)` → PUT `/anticipations/configurations` + +### Commit 10 — `feat(creditCard): preAuthorization config` +- `SavePreAuthorizationConfig(SavePreAuthConfigRequest)` → POST `/creditCard/preAuthorization/config` +- `GetPreAuthorizationConfig()` → GET `/creditCard/preAuthorization/config` + +### Commit 11 — `feat(transfer): cancel + roteamento correto para /transfers/` +- Renomear overloads `Execute` para `TransferToBankAccount` (POST `/transfers`) e `TransferToAsaasAccount` (POST `/transfers/` — preservar barra) +- `Cancel(string id)` → DELETE `/transfers/{id}/cancel` + +### Commit 12 — `feat(pix): static qrCode delete, transaction find, tokenBucket` +- `DeleteStaticQrCode(string id)` → DELETE `/pix/qrCodes/static/{id}` +- `FindTransaction(string id)` → GET `/pix/transactions/{id}` +- `GetAddressKeyTokenBucket()` → GET `/pix/tokenBucket/addressKey` + +### Commit 13 — `feat(myAccount): commercialInfo, status, documents` + +**Breaking:** o `Find()` atual aponta para `/myAccount` mas esse endpoint na verdade é DELETE (excluir White Label). Corrigir e renomear. + +- Renomear `Find()` → `GetCommercialInfo()` (GET `/myAccount/commercialInfo`) +- `UpdateCommercialInfo(UpdateCommercialInfoRequest)` → POST `/myAccount/commercialInfo` +- `GetStatus()` → GET `/myAccount/status` +- `DeleteWhiteLabelAccount()` → DELETE `/myAccount` +- `ListPendingDocuments()` → GET `/myAccount/documents` +- `SubmitDocument(string id, List)` → POST multipart `/myAccount/documents/{id}` +- `ViewDocumentFile(string id)`, `UpdateDocumentFile(string id, AsaasFile)`, `DeleteDocumentFile(string id)` → GET/POST/DELETE `/myAccount/documents/files/{id}` + +### Commit 14 — `feat(asaasAccount): find, accessTokens, resendActivationLink` +- `Find(string id)` → GET `/accounts/{id}` +- `ResendActivationLink(string id)` → POST `/accounts/{id}/resendActivationLink` +- `CreateAccessToken(string id, CreateAccessTokenRequest)` → POST `/accounts/{id}/accessTokens` +- `ListAccessTokens(string id, int o, int l)` → GET `/accounts/{id}/accessTokens` +- `UpdateAccessToken(string id, string tokenId, UpdateAccessTokenRequest)` → PUT `/accounts/{id}/accessTokens/{tokenId}` +- `DeleteAccessToken(string id, string tokenId)` → DELETE `/accounts/{id}/accessTokens/{tokenId}` + +**Testes:** 1 happy + 1 erro por método. + +--- + +## Sprint 4 — Novos Domínios (6 commits) + +### Commit 15 — `feat: ChargebackManager` +- Novo manager + modelos (`Chargeback`, `ChargebackDispute`, enums `ChargebackStatus`, `ChargebackReason`, `ChargebackDisputeStatus`) +- 3 endpoints: List `/chargebacks`, FindByPayment `/payments/{id}/chargeback`, CreateDispute `/chargebacks/{id}/dispute` +- Adicionar `Lazy` em [AsaasApi.cs](Codout.Apis.Asaas/AsaasApi.cs) +- `Testable` + ManagerTests file +- Testes: 3 happy + 3 erro + serialização + +### Commit 16 — `feat: EscrowManager` +- 5 endpoints: salvar/recuperar config por subconta, default, finish, recuperar de payment +- Modelos: `EscrowConfig`, `Escrow` + +### Commit 17 — `feat: CheckoutManager` +- 2 endpoints + modelos `Checkout`, `CreateCheckoutRequest` + +### Commit 18 — `feat: MobilePhoneRechargeManager` +- 4 endpoints + modelos: `MobilePhoneRecharge`, `CreateMobilePhoneRechargeRequest`, `MobilePhoneProvider` + +### Commit 19 — `feat(payment): splits queries` +- Adicionar em `PaymentManager`: + - `ListPaidSplits(int o, int l)`, `FindPaidSplit(string id)` + - `ListReceivedSplits(int o, int l)`, `FindReceivedSplit(string id)` +- Modelo `PaidSplit` / `ReceivedSplit` + +### Commit 20 — `feat: SandboxManager` +- 3 endpoints sandbox-only: ApproveAccount, ConfirmPayment, ForceOverdue +- **Adicionar guarda:** método lança `InvalidOperationException` se `apiSettings.AsaasEnvironment.IsProduction()` + +--- + +## Sprint 5 — Pix Evolução (2 commits) + +### Commit 21 — `feat: PixAutomaticManager` +- 6 endpoints sob `/pix/automatic/*` +- Modelos: `PixAutomaticAuthorization`, `PixAutomaticPaymentInstruction`, enums + +### Commit 22 — `feat: PixRecurringManager` +- 5 endpoints sob `/pix/transactions/recurrings/*` +- Modelos: `PixRecurringTransaction`, `PixRecurringItem` + +--- + +## Sprint 6 — Polimento e Quality (4 commits) + +### Commit 23 — `refactor(models): completar campos faltantes em response DTOs` +- Customer: `object`, `cityName`, `foreignCustomer`, `stateInscription`, `groupName`, `company` +- CreateCustomerRequest/UpdateCustomerRequest: `company`, `foreignCustomer` (e `groupName` no Update) +- Demais ajustes pontuais identificados na auditoria +- Testes de serialização para cada campo adicionado + +### Commit 24 — `refactor(models): bool? em campos opcionais, revisar DateTime` +- Converter `bool` → `bool?` em campos não-required de responses (Customer.Deleted, Customer.NotificationDisabled, Payment.PostalService, Payment.Anticipated, Payment.Deleted, etc.) +- Adicionar conversor `JsonConverter` (já que .NET 10) para campos `format: date` se necessário +- Atualizar testes para cobrir caso nulo + +### Commit 25 — `refactor(core): usar IHttpClientFactory` +- Adicionar `AddAsaasClient` em `Codout.Apis.Asaas/Extensions/ServiceCollectionExtensions.cs` (nova) +- Refatorar `BaseManager` para receber `IHttpClientFactory` ao invés de criar `new HttpClient()` por requisição +- Manter `ApiSettings`/`AsaasApi(ApiSettings)` para compatibilidade não-DI +- **Atenção:** ajustar `TestableManagerFactory` (testes que sobrescrevem `BuildHttpClient`) + +### Commit 26 — `chore: WasSuccessful (manter WasSucessfull como alias temporário)` +- Adicionar `WasSuccessful()` correto em `BaseResponse` +- Manter `WasSucessfull()` chamando o novo, marcado `[Obsolete("Use WasSuccessful (typo correction).")]` +- Atualizar uso interno + +--- + +## Commit final — `chore(release): bump version to 3.0.0 + CHANGELOG` + +**Arquivos a alterar:** +- [Codout.Apis.Asaas.csproj](Codout.Apis.Asaas/Codout.Apis.Asaas.csproj) — `3.0.0` +- [CHANGELOG.md](CHANGELOG.md) — entrada `## 3.0.0 — 2026-MM-DD` + - Seção "Breaking changes" listando todas as renomeações e remoções + - Seção "Bug fixes" com os 7 fixes + - Seção "New endpoints" agrupados por manager + - Seção "New managers" (Chargeback, Escrow, Checkout, MobilePhoneRecharge, Sandbox, PixAutomatic, PixRecurring) + - Guia de migração 2.x → 3.x + +--- + +## Verificação Final (antes de fazer push) + +```powershell +dotnet build Codout.Apis.Asaas/ --configuration Release +dotnet test Codout.Apis.Asaas.Tests/ --configuration Release +dotnet pack Codout.Apis.Asaas/ --configuration Release --no-build +``` + +Critérios: +- Build sem warnings novos (exceto os ignorados em `NoWarn`) +- **Todos os testes passando** (existentes + novos — deve fechar com ~600+ testes) +- Pacote NuGet gerado com sucesso +- AUDIT.md ainda no repo como histórico — depois de released, mover para `docs/` + +--- + +## Métricas-alvo desta entrega + +| Métrica | Antes | Depois | +|---|---|---| +| Endpoints SDK | ~70 | ~156 (100%) | +| Managers | 20 | 27 (+7 novos) | +| Testes | ~400 | ~600+ | +| Bugs bloqueantes conhecidos | 7 | 0 | +| Versão | 2.0.2 | 3.0.0 | + +--- + +## Ordem de execução nesta sessão + +Vou executar sprint a sprint, **fazendo um commit por vez**, sempre após: +1. Implementar a mudança +2. Adicionar/ajustar testes +3. Rodar `dotnet build` + `dotnet test` localmente +4. Só então `git add` + `git commit` + +Se algum commit falhar build/testes, **paro e te aviso** antes de prosseguir. Você pode interromper a qualquer momento e me dizer "pula esse" ou "ajusta isso". + +A cada fim de Sprint farei uma pausa pra você revisar o que foi feito, antes de continuar para o próximo. diff --git a/README.md b/README.md index 16f11db..badac74 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,20 @@ - + +

-SDK .NET **no-oficial** para integrar com a plataforma de pagamentos [Asaas](https://www.asaas.com). Cobre a **API v3** com suporte a mais de 90 endpoints. +SDK .NET **nao-oficial** para integrar com a plataforma de pagamentos [Asaas](https://www.asaas.com). Cobre **100% da API v3** documentada — mais de **150 endpoints** distribuidos em **27 managers**. > **Zero dependencias externas** - usa apenas `System.Text.Json` (built-in). +> **v3.2.0** — auditoria schema-first **completa** contra MCP oficial (`https://docs.asaas.com/mcp`). +> **27/27 managers** auditados endpoint-a-endpoint, **42 famílias de bugs** corrigidas, +> contract tests validando shape JSON com fixtures dos exemplos oficiais, +> integration tests sandbox com workflow CI dedicado. +> Veja [CONFORMANCE.md](CONFORMANCE.md) §50 para o consolidado de padrões. + --- ## Instalacao @@ -56,7 +63,7 @@ var customerResponse = await asaas.Customer.Create(new CreateCustomerRequest Email = "maria@email.com" }); -if (customerResponse.WasSucessfull()) +if (customerResponse.WasSuccessful()) { var customer = customerResponse.Data; @@ -70,7 +77,7 @@ if (customerResponse.WasSucessfull()) Description = "Pedido #1234" }); - if (paymentResponse.WasSucessfull()) + if (paymentResponse.WasSuccessful()) { // 4. Obter QR Code Pix var pixQr = await asaas.Payment.GetPixQrCode(paymentResponse.Data.Id); @@ -96,7 +103,7 @@ Toda chamada retorna `ResponseObject` (item unico) ou `ResponseList` (list // Resposta de item unico ResponseObject response = await asaas.Customer.Find("cus_123"); -if (response.WasSucessfull()) +if (response.WasSuccessful()) { Customer customer = response.Data; Console.WriteLine(customer.Name); @@ -147,30 +154,37 @@ var customers = await asaas.Customer.List(0, 50, new CustomerListFilter ## Modulos Disponiveis -O SDK expoe 20 managers via `AsaasApi`: - -| Manager | Propriedade | Endpoints | Descricao | -|---------|-------------|-----------|-----------| -| **Clientes** | `asaas.Customer` | Create, Find, List, Update, Delete, Restore | Cadastro de clientes | -| **Cobrancas** | `asaas.Payment` | Create, Find, List, Update, Delete, Restore, Refund, ReceiveInCash, UndoReceivedInCash, GetBankSlipBarCode, GetPixQrCode | Boletos, Pix, Cartao | -| **Parcelamentos** | `asaas.Installment` | Find, List, Delete, Refund, ListPaymentBook | Carnezinhos/parcelas | -| **Assinaturas** | `asaas.Subscription` | Create, Find, List, Update, Delete, ListPayments, ListPaymentBook, ListInvoice, CRUD InvoiceSettings | Cobran cas recorrentes | -| **Links de Pagamento** | `asaas.PaymentLink` | Create, Find, List, Update, Delete, Restore, AddImage, ListImages, FindImage, DeleteImage, SetMainImage | Links compartilhaveis | -| **Pix** | `asaas.Pix` | ListTransactions, CancelTransaction, CreateStaticQrCode, DecodeQrCode, PayQrCode, CRUD AddressKeys | Pix completo | -| **Notas Fiscais** | `asaas.Invoice` | Schedule, Find, List, Update, Authorize, Cancel, ListMunicipalServices | NFS-e | -| **Financeiro** | `asaas.Finance` | Balance, ListTransactions, PaymentStatistics, SplitStatistics | Saldo e extrato | -| **Transferencias** | `asaas.Transfer` | Find, List, Execute (Asaas/Banco) | TED e transferencias | -| **Antecipacoes** | `asaas.ReceivableAnticipation` | Create, Simulate, Find, List, SignAgreement | Antecipacao de recebiveis | -| **Negativacoes** | `asaas.PaymentDunning` | Create, Simulate, Find, List, ListEventHistory, ListPartialPayments, ListPaymentsAvailable, ResendDocument, Cancel | Protesto e SERASA | -| **Pagto de Contas** | `asaas.BillPayment` | Create, Simulate, Find, List, Cancel | Pagamento de boletos | -| **Webhooks** | `asaas.Webhook` | CRUD Payment, Invoice e MobilePhoneRecharge | Notificacoes | -| **Notificacoes** | `asaas.Notification` | Update, BatchUpdate | Config de notificacoes | -| **Cartao de Credito** | `asaas.CreditCard` | TokenizeCreditCard | Tokenizacao | -| **Contas Asaas** | `asaas.AsaasAccount` | Create, List | Subcontas white-label | -| **Minha Conta** | `asaas.MyAccount` | Find, FindFees, FindAccountNumber, CRUD PaymentCheckoutConfig | Dados da conta | -| **Carteiras** | `asaas.Wallet` | List | Carteiras digitais | -| **Consulta SERASA** | `asaas.CreditBureauReport` | Create, Find, List | Consulta de credito | -| **Info Fiscal** | `asaas.CustomerFiscalInfo` | CreateOrUpdate, Find, ListMunicipalOptions | Config fiscal NFS-e | +O SDK expoe 27 managers via `AsaasApi`: + +| Manager | Propriedade | Descricao | +|---------|-------------|-----------| +| **Clientes** | `asaas.Customer` | CRUD de clientes | +| **Cobrancas** | `asaas.Payment` | Boletos, Pix, Cartao, documentos, splits, refunds, billing/viewing info, simulate, limits, etc | +| **Parcelamentos** | `asaas.Installment` | Create, CreateWithCreditCard, Find, List, Delete, Refund, ListPayments, CancelPendingPayments, UpdateSplits, ListPaymentBook | +| **Assinaturas** | `asaas.Subscription` | CRUD + UpdateCreditCard + InvoiceSettings + ListPayments | +| **Links de Pagamento** | `asaas.PaymentLink` | CRUD + imagens (add, list, find, delete, setMain) | +| **Pix** | `asaas.Pix` | CRUD AddressKeys, QrCodes (estatico, decode, pay, delete), Transactions (list, find, cancel), tokenBucket | +| **Pix Automatico** | `asaas.PixAutomatic` | Autorizacoes recorrentes (Pix Bacen) e payment instructions | +| **Pix Recorrente** | `asaas.PixRecurring` | Recorrencias parceladas e itens | +| **Webhooks** | `asaas.Webhook` | CRUD generico em `/v3/webhooks/{id}` com ~100 eventos disponiveis | +| **Notas Fiscais** | `asaas.Invoice` | Schedule, Find, List, Update, Authorize, Cancel | +| **Info Fiscal** | `asaas.FiscalInfo` | CreateOrUpdate, Find, ListMunicipalOptions, ListServices | +| **Financeiro** | `asaas.Finance` | GetBalance, ListTransactions, GetPaymentStatistics, GetSplitStatistics | +| **Transferencias** | `asaas.Transfer` | TransferToAsaasAccount, TransferToBankAccount, Find, List, Cancel | +| **Antecipacoes** | `asaas.Anticipation` | Create, Simulate, Find, List, Cancel, GetLimits, GetAutomaticConfiguration, UpdateAutomaticConfiguration | +| **Negativacoes** | `asaas.PaymentDunning` | Create, Simulate, Find, List, ListEventHistory, ListPartialPayments, ListPaymentsAvailable, ResendDocument, Cancel | +| **Pagto de Contas** | `asaas.BillPayment` | Create, Simulate, Find, List, Cancel | +| **Recarga Celular** | `asaas.MobilePhoneRecharge` | Create, Find, List, Cancel, GetProvider | +| **Notificacoes** | `asaas.Notification` | Update, BatchUpdate | +| **Cartao de Credito** | `asaas.CreditCard` | TokenizeCreditCard + PreAuthorization config | +| **Contas Asaas** | `asaas.AsaasAccount` | Create, Find, List, ResendActivationLink, CRUD AccessTokens | +| **Minha Conta** | `asaas.MyAccount` | GetCommercialInfo, UpdateCommercialInfo, GetStatus, DeleteWhiteLabelAccount, GetFees, GetAccountNumber, PaymentCheckoutConfig, Documents | +| **Carteiras** | `asaas.Wallet` | List | +| **Consulta SERASA** | `asaas.CreditBureauReport` | Create, Find, List | +| **Chargebacks** | `asaas.Chargeback` | List, FindByPayment, CreateDispute | +| **Escrow** | `asaas.Escrow` | SaveSubaccountConfig, GetSubaccountConfig, SaveDefaultConfig, GetDefaultConfig, FinishPaymentEscrow, GetPaymentEscrow | +| **Checkout** | `asaas.Checkout` | Create, Cancel | +| **Sandbox (testes)** | `asaas.Sandbox` | ApproveAccount, ConfirmPayment, ForceOverdue (so funciona em `AsaasEnvironment.SANDBOX`) | ## Exemplos por Modulo @@ -227,7 +241,7 @@ var link = await asaas.PaymentLink.Create(new CreatePaymentLinkRequest DueDateLimitDays = 10 }); -if (link.WasSucessfull()) +if (link.WasSuccessful()) { Console.WriteLine($"Link: {link.Data.Url}"); } @@ -257,20 +271,45 @@ var newKey = await asaas.Pix.CreateAddressKey(new CreatePixAddressKeyRequest }); ``` +### Webhooks + +```csharp +using Codout.Apis.Asaas.Models.Webhook; +using Codout.Apis.Asaas.Models.Webhook.Enums; + +var webhook = await asaas.Webhook.Create(new CreateWebhookRequest +{ + Name = "Notificacoes de pagamento", + Url = "https://meusite.com/webhook/asaas", + Email = "ops@meusite.com", + Enabled = true, + Interrupted = false, + ApiVersion = 3, + AuthToken = "whsec_min_32_caracteres_para_assinar_callbacks", + SendType = WebhookSendType.SEQUENTIALLY, + Events = + [ + WebhookEvent.PAYMENT_CONFIRMED, + WebhookEvent.PAYMENT_RECEIVED, + WebhookEvent.PAYMENT_OVERDUE + ] +}); +``` + ### Transferencias ```csharp using Codout.Apis.Asaas.Models.Transfer; -// Transferencia para conta Asaas -var transfer = await asaas.Transfer.Execute(new AsaasAccountTransferRequest +// Transferencia para conta Asaas (POST /v3/transfers/) +var transfer = await asaas.Transfer.TransferToAsaasAccount(new AsaasAccountTransferRequest { WalletId = "wallet_destino", Value = 500.00m }); -// Transferencia bancaria (TED) -var ted = await asaas.Transfer.Execute(new BankAccountTransferRequest +// Transferencia bancaria (TED / Pix para outra instituicao - POST /v3/transfers) +var ted = await asaas.Transfer.TransferToBankAccount(new BankAccountTransferRequest { Value = 1000.00m, BankAccount = new BankAccount @@ -285,6 +324,9 @@ var ted = await asaas.Transfer.Execute(new BankAccountTransferRequest BankAccountType = BankAccountType.CONTA_CORRENTE } }); + +// Cancelar transferencia +await asaas.Transfer.Cancel("trans_123"); ``` ### Notas Fiscais (NFS-e) @@ -301,13 +343,17 @@ var invoice = await asaas.Invoice.Schedule(new CreateInvoiceRequest // Autorizar emissao await asaas.Invoice.Authorize("inv_123"); + +// Listar codigos de servico do municipio (FiscalInfo, nao Invoice) +var services = await asaas.FiscalInfo.ListServices("consultoria"); ``` ### Saldo e Extrato ```csharp -// Saldo atual -var balance = await asaas.Finance.Balance(); +// Saldo atual (retorna { balance: number } - acesse via .Value) +var balance = await asaas.Finance.GetBalance(); +Console.WriteLine($"Saldo: R$ {balance.Data.Value}"); // Extrato var transactions = await asaas.Finance.ListTransactions(0, 50); @@ -316,6 +362,17 @@ var transactions = await asaas.Finance.ListTransactions(0, 50); var stats = await asaas.Finance.GetPaymentStatistics(); ``` +### Sandbox (somente em testes) + +```csharp +// Em AsaasEnvironment.SANDBOX, voce pode acelerar fluxos de teste: +await asaas.Sandbox.ApproveAccount(); // aprova conta sandbox +await asaas.Sandbox.ConfirmPayment("pay_123"); // confirma pagamento +await asaas.Sandbox.ForceOverdue("pay_123"); // forca vencimento + +// Em PRODUCTION, todos lancam InvalidOperationException antes de fazer HTTP. +``` + ## Configuracao Avancada ### Timeout Customizado @@ -339,12 +396,17 @@ dotnet build Codout.Apis.Asaas/Codout.Apis.Asaas.csproj ### Testes -O projeto possui **400 testes unitarios** cobrindo todos os 20 managers, modelos, serializacao e extensions: +O projeto possui **664 testes unit/contract** cobrindo todos os 27 managers +(unit tests + contract tests validando shape JSON com fixtures dos exemplos +oficiais do MCP) + **15 integration tests** opcionais contra o sandbox real. ```bash -# Rodar todos os testes +# Rodar todos os testes unit/contract (integration skipa sem ASAAS_SANDBOX_TOKEN) dotnet test Codout.Apis.Asaas.Tests/ +# Rodar somente unit/contract, excluindo integration +dotnet test Codout.Apis.Asaas.Tests/ --filter "Category!=Integration" + # Rodar testes de um manager especifico dotnet test Codout.Apis.Asaas.Tests/ --filter "FullyQualifiedName~CustomerManagerTests" @@ -352,6 +414,22 @@ dotnet test Codout.Apis.Asaas.Tests/ --filter "FullyQualifiedName~CustomerManage dotnet test Codout.Apis.Asaas.Tests/ --verbosity normal ``` +#### Integration tests (sandbox real) + +Os integration tests fazem chamadas reais contra `api-sandbox.asaas.com` +para validar o comportamento end-to-end. Por padrao **sao puladas com skip +explicito** quando a variavel de ambiente `ASAAS_SANDBOX_TOKEN` nao esta +definida — assim o CI local nao quebra. + +```powershell +# Definir o token de sandbox e rodar: +$env:ASAAS_SANDBOX_TOKEN = "aact_YTU0...seu_token_sandbox..." +dotnet test Codout.Apis.Asaas.Tests/ --filter "Category=Integration" +``` + +Veja [CONFORMANCE.md §99](CONFORMANCE.md) para a lista de tests e o que +cada um valida. + ### Gerar pacote NuGet ```bash @@ -362,23 +440,27 @@ dotnet pack Codout.Apis.Asaas/ -c Release ``` Codout.Apis.Asaas/ -├── AsaasApi.cs # Facade principal (entry point) +├── AsaasApi.cs # Facade principal (entry point) com 27 managers ├── Core/ │ ├── ApiSettings.cs # Configuracao (token, ambiente, timeout) -│ ├── BaseManager.cs # Base HTTP (GET/POST/PUT/DELETE) -│ ├── JsonSerializerConfiguration.cs # Config System.Text.Json +│ ├── BaseManager.cs # Base HTTP (GET/POST/PUT/DELETE) com SocketsHttpHandler compartilhado +│ ├── JsonSerializerConfiguration.cs # Config System.Text.Json (camelCase, SafeEnumConverterFactory, FlexibleDateTimeConverter) │ ├── RequestParameters.cs # Query string builder │ ├── Response/ -│ │ ├── Base/BaseResponse.cs # StatusCode, Errors, WasSucessfull() +│ │ ├── Base/BaseResponse.cs # StatusCode, Errors, WasSuccessful() │ │ ├── ResponseObject.cs # Resposta de item unico │ │ └── ResponseList.cs # Resposta paginada │ └── Extension/ # Helpers (DateTime, StatusCode, String) -├── Managers/ # 20 domain managers +├── Managers/ # 27 domain managers │ ├── CustomerManager.cs │ ├── PaymentManager.cs │ ├── PixManager.cs +│ ├── PixAutomaticManager.cs +│ ├── PixRecurringManager.cs +│ ├── ChargebackManager.cs +│ ├── EscrowManager.cs │ └── ... -└── Models/ # Request/Response por dominio +└── Models/ # Request/Response por dominio (1 classe por arquivo) ├── Customer/ ├── Payment/ ├── Pix/ diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..c3c39ff --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,236 @@ +# Code Review — branch `audit/asaas-api-conformance` (v3.0.0) + +> Revisado em: 2026-05-24 +> Fontes: (1) subagent code-reviewer (qualidade de código + testes), (2) verificação manual de schemas contra o MCP oficial do Asaas +> Commits revisados: 28 (master..HEAD) + +--- + +## Sumário executivo + +O refactor entrega o objetivo macro: cobertura sobe de ~45% para 100% dos endpoints documentados, 7 bugs bloqueantes originais corrigidos, 7 novos managers, 478 testes passando. **MAS** vários modelos novos foram criados sem leitura cuidadosa do schema OpenAPI — vou rebatizar como "API conformance bugs" os casos em que o cliente vai falhar contra a API real apesar dos testes locais passarem (porque testes mockam JSON). + +Encontrei **8 🔴 bloqueantes** (1 do subagent, 7 de schema), **9 🟡 importantes**, **3 🟢 polimento**. + +--- + +## 🔴 BLOQUEANTES + +### B-01 — `PostMultipartFormDataContentAsync`: NRE em propriedades nulas +**Fonte:** subagent | **Arquivo:** [Core/BaseManager.cs:61](Codout.Apis.Asaas/Core/BaseManager.cs#L61) + +```csharp +multipartContent.Add(new StringContent(prop.GetValue(payload).ToString()), jsonPropertyName); +``` + +Crash com `NullReferenceException` quando qualquer propriedade não-arquivo do payload é `null`. Novos call sites introduzidos neste PR que expõem o bug: +- `PaymentManager.UploadDocument` (UploadPaymentDocumentRequest.Available é `bool?`, garantido de ser null em uploads sem esse parâmetro) +- `PaymentManager.UploadDocument` (UploadPaymentDocumentRequest.Type — string) +- `ChargebackManager.CreateDispute` (CreateChargebackDisputeRequest.Description — string) + +**Fix:** ignorar propriedades nulas no loop de reflection. + +--- + +### B-02 — Modelo `PaymentLimits` tem shape errado (deserialização vai virar `null`/0) +**Fonte:** schema review | **Arquivo:** [Models/Payment/PaymentLimits.cs](Codout.Apis.Asaas/Models/Payment/PaymentLimits.cs) + +API real (`/v3/payments/limits`): +```json +{ + "creation": { + "daily": { "limit": 10, "used": 5, "wasReached": false } + } +} +``` + +Meu modelo tinha `CreditCard`/`Pix`/`BankSlip` cada um com `Daily`/`Monthly`/`AverageTicket` — **completamente diferente**. + +**Fix:** reescrever. + +--- + +### B-03 — `SimulatePaymentRequest` e `SimulatedPayment` com shape errado +**Fonte:** schema review | **Arquivo:** [Models/Payment/SimulatePaymentRequest.cs](Codout.Apis.Asaas/Models/Payment/SimulatePaymentRequest.cs), [Models/Payment/SimulatedPayment.cs](Codout.Apis.Asaas/Models/Payment/SimulatedPayment.cs) + +Request real exige `value` + `billingTypes` (lista, plural) + opcional `installmentCount`. Meu modelo tinha `BillingType` singular, mais campos inventados (`DiscountValue`, `Splits`). + +Response real é `{ value, creditCard: {...}, bankSlip: {...}, pix: {...} }`. Meu modelo era flat com `NetValue`, `Fee`, `InstallmentValue`. + +API vai rejeitar requests e responses não vão deserializar. + +**Fix:** reescrever ambos. + +--- + +### B-04 — `Checkout` / `CreateCheckoutRequest` com shape muito errado +**Fonte:** schema review | **Arquivo:** [Models/Checkout/Checkout.cs](Codout.Apis.Asaas/Models/Checkout/Checkout.cs) + +API real: +- Response (`CheckoutSessionResponseDTO`) tem `id`, `link` (não `checkoutUrl`), `status`, `billingTypes`, `chargeTypes`, `minutesToExpire`, `externalReference`, `callback`, `items`, `customerData`, `subscription`, `installment`, `split` +- Request (`CheckoutSessionSaveRequestDTO`) tem campos **obrigatórios** `billingTypes`, `chargeTypes`, `callback`, `items` — meu modelo nem tinha `items`! +- `CheckoutCustomerData.City` é `int` (código IBGE), não string +- `CheckoutCustomerData.AddressNumber` é `int` (?), não string +- `CheckoutCallback` tem `cancelUrl` obrigatório e `expiredUrl` opcional + +Meu modelo tinha `Value`, `DueDate`, `Customer`, `CustomerData` — vários inventados, vários ausentes. + +**Status enum** correto: `ACTIVE, CANCELED, EXPIRED, PAID`. + +**Fix:** reescrita substancial. + +--- + +### B-05 — `MobilePhoneRecharge` tem campo errado e enum como string +**Fonte:** schema review | **Arquivo:** [Models/MobilePhoneRecharge/MobilePhoneRecharge.cs](Codout.Apis.Asaas/Models/MobilePhoneRecharge/MobilePhoneRecharge.cs) + +API real: +- `operatorName` (não `provider`) +- `canBeCancelled` (faltando) +- Status é enum: `PENDING, CONFIRMED, CANCELLED, REFUNDED, WAITING_CRITICAL_ACTION` +- Não tem `DateCreated` nem `ConfirmedDate` + +**Fix:** ajustar nome do campo, adicionar `canBeCancelled`, converter status para enum. + +--- + +### B-06 — `PixAutomaticAuthorization` totalmente diferente do schema da API +**Fonte:** schema review | **Arquivo:** [Models/PixAutomatic/PixAutomaticAuthorization.cs](Codout.Apis.Asaas/Models/PixAutomatic/PixAutomaticAuthorization.cs) + +API real: +- Response: `id, minLimitValue, cancellationDate, cancellationReason, contractId, customerId, description, finishDate, frequency` (não `periodicity`), `endToEndIdentifier, startDate, status, value, payload, encodedImage, immediateQrCode, originType, subscriptionId` +- Request: requer `frequency, contractId, startDate, customerId, immediateQrCode` (objeto complexo!) — `immediateQrCode` é totalmente novo e obrigatório + +Meu modelo tinha campos inventados (`PayerCpfCnpj`, `PayerName`, `ApprovalDate`) e faltava o `immediateQrCode` (obrigatório). API vai rejeitar todo request criado pelo SDK. + +**Enums a criar:** +- `PixAutomaticRecurringFrequency`: WEEKLY, MONTHLY, QUARTERLY, SEMIANNUALLY, ANNUALLY +- `PixAutomaticAuthorizationStatus`: CREATED, ACTIVE, CANCELLED, REFUSED, EXPIRED +- `PixAutomaticRecurringOriginType`: IMMEDIATE_PAYMENT_AND_RECURRING_QR_CODE, PAYMENT_AND_RECURRING_OFFER_QR_CODE +- `PixAutomaticRecurringPaymentCreationMode`: MANUAL, SUBSCRIPTION + +**Fix:** reescrita completa. + +--- + +### B-07 — `AccountDocument` envelope errado em `ListPendingDocuments` +**Fonte:** schema review | **Arquivo:** [Managers/MyAccountManager.cs:62](Codout.Apis.Asaas/Managers/MyAccountManager.cs#L62) + +API real para `GET /v3/myAccount/documents` retorna `{ rejectReasons, data: [...] }` — NÃO é o envelope `{hasMore, totalCount, limit, offset, data}` que `ResponseList` espera. Meu método chama `GetListAsync` que vai deserializar `rejectReasons` como ignorado mas `hasMore/totalCount/limit/offset` virão `null`/0. + +Mais crítico: o shape de `AccountDocumentSection` está errado também — falta `type`, `responsible`, `onboardingUrl`, `onboardingUrlExpirationDate`; `Documents` interno tem só `id, status` (não os campos que coloquei). + +**Fix:** criar `AccountDocumentResponse` wrapper e mudar retorno para `ResponseObject`. Reescrever `AccountDocument` model. + +--- + +### B-08 — `PaymentBillingInfo.Nossonumero` grafia errada + tipos errados +**Fonte:** schema review + subagent I-06 | **Arquivo:** [Models/Payment/PaymentBillingInfo.cs](Codout.Apis.Asaas/Models/Payment/PaymentBillingInfo.cs) + +- `Nossonumero` → deveria ser `NossoNumero` (camelCase consistente) +- Faltam: `bankSlipUrl`, `daysAfterDueDateToRegistrationCancellation` no `BankSlip` +- Faltam: `description` no `Pix` +- `CreditCard` na API usa `creditCardNumber/creditCardBrand/creditCardToken` (igual ao `CreditCardTokenizeResponseDTO`) — meu modelo `PaymentBillingInfoCreditCard` está OK, só rever nomes + +**Fix:** ajustes pontuais nos sub-objetos. + +--- + +## 🟡 IMPORTANTES + +### I-01 — `SandboxManager._settings` duplica `BaseManager._settings` +**Fonte:** subagent | **Arquivo:** [Managers/SandboxManager.cs:12](Codout.Apis.Asaas/Managers/SandboxManager.cs#L12) + +Campo redundante. Fix: tornar `_settings` em `BaseManager` como `protected` e remover do SandboxManager. Não causa bug hoje, mas é confuso. + +### I-02 — ~120 chamadas a `WasSucessfull()` `[Obsolete]` em testes gerando warnings CS0618 +**Fonte:** subagent | **Arquivos:** todos `*ManagerTests.cs`, `ResponseObjectTests.cs`, `ResponseListTests.cs`, `Sample/Program.cs` + +CHANGELOG diz que migrou para `WasSuccessful()`, mas testes ainda usam grafia antiga. Polui output de CI. **Fix:** replace global. + +### I-03 — `SerializationTests` usa `JsonStringEnumConverter` (padrão) em vez de `SafeEnumConverterFactory` (do SDK) +**Fonte:** subagent | **Arquivo:** [Tests/Models/SerializationTests.cs:20-32](Codout.Apis.Asaas.Tests/Models/SerializationTests.cs#L20-L32) + +Testes não exercitam os conversores customizados. Um enum desconhecido passaria no teste e quebraria em produção. + +### I-04 — `Installment.Deleted` é `bool` (não `bool?`) — inconsistente com migração 3.0.0 +**Fonte:** subagent | **Arquivo:** [Models/Installment/Installment.cs:46](Codout.Apis.Asaas/Models/Installment/Installment.cs#L46) + +Customer.Deleted e Payment.Deleted foram convertidos; Installment ficou de fora. + +### I-05 — `SandboxManager`: só `ApproveAccount` tem teste de guarda de produção +**Fonte:** subagent | **Arquivo:** [Tests/Managers/SandboxManagerTests.cs](Codout.Apis.Asaas.Tests/Managers/SandboxManagerTests.cs) + +`ConfirmPayment_InProduction_Throws` e `ForceOverdue_InProduction_Throws` faltando. + +### I-06 — `AsaasApi.ReceivableAnticipation` vs todos os outros managers sem prefixo +**Fonte:** subagent | **Arquivo:** [AsaasApi.cs:50](Codout.Apis.Asaas/AsaasApi.cs#L50) + +Inconsistência pré-existente (não introduzida neste PR), mas vale ajustar para `Anticipation` num major. + +### I-07 — Múltiplas classes por arquivo em modelos novos +**Fonte:** subagent | **Arquivos:** Chargeback.cs, Escrow.cs, Checkout.cs, AccessToken.cs, PixAutomaticAuthorization.cs, PixRecurringTransaction.cs, MobilePhoneRecharge.cs, PaymentBillingInfo.cs, PaymentLimits.cs, AccountDocument.cs + +Viola convenção pré-existente "1 classe por arquivo". Dificulta navegação no editor. + +### I-08 — Testes de erro ausentes em 5 managers novos +**Fonte:** subagent | **Arquivos:** CheckoutManagerTests.cs, EscrowManagerTests.cs, PixAutomaticManagerTests.cs, PixRecurringManagerTests.cs, MobilePhoneRechargeManagerTests.cs + +Plano dizia "1 happy + 1 erro por endpoint" — nenhum desses tem teste de erro. + +### I-09 — `AccountStatus` deveria usar enum em vez de string +**Fonte:** schema review | **Arquivo:** [Models/MyAccount/AccountStatus.cs](Codout.Apis.Asaas/Models/MyAccount/AccountStatus.cs) + +API documenta enum `PENDING|APPROVED|REJECTED|AWAITING_APPROVAL` para todos os campos de status. Strings funcionam (`SafeEnumConverterFactory` cobre default), mas perde type safety. + +--- + +## 🟢 POLIMENTO + +### P-01 — `SerializationTests.cs` namespace inconsistente com diretório +**Fonte:** subagent | Em `Tests/Models/` mas namespace é `Tests.Serialization`. + +### P-02 — Estilo de namespace inconsistente em `Models/AsaasAccount/` +**Fonte:** subagent | `Account.cs` usa bloco `{}`, `AccessToken.cs` usa file-scoped `;`. + +### P-03 — Cast `(object)` em `EscrowManager.FinishPaymentEscrow` precisa de comentário +**Fonte:** subagent | Necessário pra resolver `??` mas não-óbvio. + +--- + +## Verificações que NÃO são bugs (já confirmadas) + +| Verificação | Resultado | +|---|---| +| `BuildApiRoute` preserva trailing slash? | ✅ Sim | +| `disposeHandler: false` + `using` é seguro? | ✅ Sim | +| `(object)requestObj ?? new RequestParameters()` resolve corretamente? | ✅ Sim | +| Todos os 27 managers expostos em `AsaasApi.cs`? | ✅ Sim | +| Todos têm `Testable*Manager`? | ✅ Sim | +| `WasSucessfull` chamado internamente no SDK fora de testes? | ✅ Não | +| Endpoint `/v3/customers` schema (Customer, Create, Update) | ✅ Sim (verificado nos commits 1+23) | +| `/v3/finance/balance` shape | ✅ Sim (verificado no commit 3) | +| `/v3/webhooks` CRUD | ✅ Sim (verificado no commit 6) | +| `/v3/fiscalInfo/services` | ✅ Sim (verificado no commit 2) | +| `/v3/myAccount/commercialInfo` | ✅ Sim (verificado no commit 13) | +| Notas fiscais (Invoice) | ⚠️ Não verificado a fundo, modelo é pré-existente | + +--- + +## Plano de ação (recomendado) + +**Imediato (este sessão):** +1. B-01 — fix NRE no multipart +2. B-08 — fix `Nossonumero` typo e completar BankSlip/Pix +3. B-05 — fix MobilePhoneRecharge (provider→operatorName, canBeCancelled, status enum) +4. B-02 — rewrite PaymentLimits +5. B-03 — rewrite SimulatePayment + SimulatedPayment +6. B-04 — rewrite Checkout/CreateCheckoutRequest +7. B-06 — rewrite PixAutomaticAuthorization (mais trabalhoso) +8. B-07 — rewrite AccountDocument + envelope +9. I-04 — Installment.Deleted bool? +10. I-02 — global replace WasSucessfull → WasSuccessful + +**Pós-revisão (próximo PR / no merge):** +- I-01, I-05, I-06, I-07, I-08, I-09 — qualidade +- P-01, P-02, P-03 — polimento diff --git a/REVIEW2.md b/REVIEW2.md new file mode 100644 index 0000000..459d908 --- /dev/null +++ b/REVIEW2.md @@ -0,0 +1,139 @@ +# Code Review 2 — branch `audit/asaas-api-conformance` + +> Gerado em 2026-05-24 (após primeira revisão fechar todos os 8 🔴 + 9 🟡 + 3 🟢) +> Fontes: (1) segundo subagent code-reviewer independente, (2) verificação manual de mais schemas via MCP + +--- + +## Sumário executivo + +A primeira revisão (REVIEW.md) foi efetivamente fechada. Esta segunda passada **encontrou 6 novos schemas errados** que não tinham sido verificados antes (Escrow + PixRecurring + 1 detalhe de bool), **1 inconsistência adicional** (`Subscription.Deleted` deixado para trás), e **stale na documentação**. Tudo foi corrigido. + +**1 falso-positivo** do subagent (assimetria `splits`/`split` no Checkout é por design da API Asaas, não bug). + +--- + +## 🔴 Bloqueantes — todos corrigidos + +### B-09 — `EscrowConfig` / `SaveEscrowConfigRequest` fields errados +**Arquivo:** [Models/Escrow/EscrowConfig.cs](Codout.Apis.Asaas/Models/Escrow/EscrowConfig.cs), [Models/Escrow/SaveEscrowConfigRequest.cs](Codout.Apis.Asaas/Models/Escrow/SaveEscrowConfigRequest.cs) +- Campo era `DaysUntilExpire`, API documenta **`DaysToExpire`** (obrigatório). +- Faltava `IsFeePayer`. + +### B-10 — `Escrow` (response): `Status` e `FinishReason` deveriam ser enums +**Arquivo:** [Models/Escrow/Escrow.cs](Codout.Apis.Asaas/Models/Escrow/Escrow.cs) +- `Status`: enum `EscrowStatus` (ACTIVE/DONE) +- `FinishReason`: enum `EscrowFinishReason` (CHARGEBACK/EXPIRED/INSUFFICIENT_BALANCE/PAYMENT_REFUNDED/REQUESTED_BY_CUSTOMER/CUSTOMER_CONFIG_DISABLED) + +### B-11 — `FinishPaymentEscrow` retorna `Payment`, não `Escrow` +**Arquivo:** [Managers/EscrowManager.cs](Codout.Apis.Asaas/Managers/EscrowManager.cs) +- API retorna `PaymentGetResponseDTO`, body é objeto vazio `{}`. +- Removido `FinishEscrowRequest` (não existe na API). + +### B-12 — `PixRecurringTransaction` muito errado +**Arquivo:** [Models/PixRecurring/PixRecurringTransaction.cs](Codout.Apis.Asaas/Models/PixRecurring/PixRecurringTransaction.cs) +- Campos inventados removidos: `Description`, `Periodicity`, `EndDate`, `DateCreated`. +- Adicionados conforme API: `Origin` (enum), `Frequency` (enum, não Periodicity!), `Quantity`, `FinishDate`, `CanBeCancelled`, `ExternalAccount`. +- Status convertido para enum `PixRecurringStatus` (5 valores). + +### B-13 — `PixRecurringItem` muito errado +**Arquivo:** [Models/PixRecurring/PixRecurringItem.cs](Codout.Apis.Asaas/Models/PixRecurring/PixRecurringItem.cs) +- Campos inventados removidos: `EffectiveDate`, `Description`. +- Adicionados: `CanBeCancelled`, `RecurrenceNumber`, `Quantity`, `RefusalReasonDescription`, `ExternalAccount`. +- Status convertido para enum `PixRecurringItemStatus`. + +### B-14 — `ListItems` (Pix Recurring) usa envelope `{data:[...]}` +**Arquivo:** [Managers/PixRecurringManager.cs](Codout.Apis.Asaas/Managers/PixRecurringManager.cs) +- A API retorna `{data:[...]}` sem `hasMore/totalCount/limit/offset`. +- Retorno mudou de `ResponseList` para `ResponseObject`. +- Mesma issue do B-07 (AccountDocument). + +### B-15 / I-12 — `RequestParameters.Add(bool?)` serializava como `"True"`/`"False"` +**Arquivo:** [Core/RequestParameters.cs](Codout.Apis.Asaas/Core/RequestParameters.cs) +- `bool.ToString()` retorna `"True"` (PascalCase). API Asaas espera `"true"`/`"false"` (lowercase). +- Sem o fix, filtros booleanos (`WebhookListFilter.Enabled`, `PaymentListFilter.Anticipated`, etc) eram silenciosamente ignorados pela API. +- Tests `RequestParametersTests.Add_BoolTrueValue_AddsStringTrue` e `_Add_BoolFalseValue_AddsStringFalse` assertavam o comportamento errado — corrigidos. + +--- + +## 🟡 Importantes — corrigidos + +### I-11 — `Subscription.Deleted` é `bool` (não `bool?`) +**Arquivo:** [Models/Subscription/Subscription.cs:43](Codout.Apis.Asaas/Models/Subscription/Subscription.cs#L43) +- Customer.Deleted, Payment.Deleted, Installment.Deleted foram convertidos durante o PR — Subscription ficou para trás. Convertido para `bool?`. + +### I-10 — README com ~12 referências stale ao 2.x +**Arquivo:** [README.md](README.md) +- `WasSucessfull()` → `WasSuccessful()` (4 ocorrências) +- Contagens: 20 managers / 400 testes → 27 managers / 492 testes +- `Transfer.Execute` → `TransferToAsaasAccount` / `TransferToBankAccount` +- `ReceivableAnticipation` → `Anticipation` +- `CustomerFiscalInfo` → `FiscalInfo` +- `MyAccount.Find` → `GetCommercialInfo` +- `Finance.Balance()` → `GetBalance()` (retorna objeto `{ Value }`) +- `Invoice.ListMunicipalServices` → `FiscalInfo.ListServices` +- Tabela de managers expandida para 27 entradas +- Adicionados exemplos de Webhook (CRUD), Sandbox, novo Transfer + +--- + +## 🟢 Polimento — adiado + +### P-04 — Sem testes de serialização para 10 domínios novos +**Arquivo:** [Tests/Serialization/SerializationTests.cs](Codout.Apis.Asaas.Tests/Serialization/SerializationTests.cs) +- Falta cobertura específica de serialização para: Chargeback, Escrow, Checkout, MobilePhoneRecharge, PixAutomatic, PixRecurring, AccountDocument, PaymentLimits, PaymentBillingInfo, SimulatedPayment. +- Enums já são exercitados indiretamente nos manager tests (ex: `ChargebackManagerTests` assert `ChargebackStatus.REQUESTED` from JSON). +- **Não bloqueia o release**. Recomendado como follow-up. + +--- + +## Falsos-positivos do subagent (não-issues) + +### C-01 (subagent) — `CreateCheckoutRequest.Splits` (plural) vs `Checkout.Split` (singular) +**Verdict:** ✅ Código atual está correto. O Asaas é assimétrico por design: +- `CheckoutSessionSaveRequestDTO.splits` (plural) +- `CheckoutSessionResponseDTO.split` (singular) + +Adicionado comentário no model para evitar "correções" futuras erradas: +```csharp +// Asaas usa "splits" (plural) no request e "split" (singular) no response. +// Nao "corrigir" essa assimetria — a API e assim por design. +public List Splits { get; set; } = []; +``` + +--- + +## Verificações que confirmaram não-issues + +| Verificação | Resultado | +|---|---| +| `_settings` migrado para `Settings` em todos os arquivos | ✅ Zero ocorrências de `_settings` remanescentes | +| `ReceivableAnticipation` rename completo | ✅ Apenas em docs (README+CHANGELOG), zero em .cs | +| File splitting do I-07 — duplicatas? | ✅ Nenhuma duplicata | +| Sandbox guard tests exercitam de fato `EnsureSandbox()`? | ✅ Sim — guard roda antes de `BuildHttpClient` | +| `WasSuccessful()` nos testes — ainda alguma chamada antiga? | ✅ Zero `WasSucessfull` em .cs (exceto o `[Obsolete]` alias) | +| `InternalsVisibleTo` para `SerializationTests`? | ✅ Configurado em csproj | +| `AnticipationStatusExtension` colocado junto do enum no mesmo arquivo? | ✅ Padrão pré-existente (BillPaymentStatus, PersonType, BillingType seguem o mesmo padrão) — não foi alvo do I-07 | +| `protected readonly Settings` naming compliance? | ✅ PascalCase é convenção .NET para protected field | + +--- + +## Estado final pós-REVIEW2 + +- ✅ **492 testes** passando em Release +- ✅ **Zero warnings CS0618** (`WasSucessfull` obsoleto não é mais chamado) +- ✅ **Zero Newtonsoft** — só `System.Text.Json` (confirmado) +- ✅ **Zero ocorrências de `True`/`False` (capital)** em query strings serializadas +- ✅ **Schemas verificados via MCP:** Customer, Payment, Subscription (parcial), FiscalInfo, Finance, Webhook, Anticipation (parcial), Pix, PixAutomatic, PixRecurring, MyAccount, AsaasAccount (parcial), Escrow, Checkout, MobilePhoneRecharge, Chargeback +- ⚠️ **Schemas NÃO verificados a fundo** (escopo limitado, modelos pré-existentes): Invoice detalhado, PaymentDunning detalhado, CreditBureauReport, BillPayment (parcial), CustomerFiscalInfo legacy (já renomeado para FiscalInfo) + +--- + +## Branch state + +``` +audit/asaas-api-conformance +~42 commits acima de master +``` + +Próximo passo: `git push -u origin audit/asaas-api-conformance` quando aprovado.