Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ examples/rede/credentials.php
*.p12
*.pfx
*.pem

# arquivos gerados pelos exemplos (carnês, boletos)
examples/*/*.pdf
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ AsaasGateway / EfiGateway ──implements──▶ <Gateway>Interface extends
│ cada método (customer/charge/pix/webhook/subscription) devolve um Resource novo
▼
Resources (Customer, Charge, Pix, Webhook, Subscription)
│ trait HasAsaasClient / HasEfiClient → PHPay\Http\HasHttpClient (get/post/put/patch/delete)
│ trait HasAsaasClient / HasEfiClient → PHPay\Http\HasHttpClient (get/post/put/patch/delete/download)
▼
Requests (validação estática dos payloads antes de qualquer chamada HTTP)
```
Expand Down Expand Up @@ -176,6 +176,11 @@ quebra a integração, cobra o valor errado.
## Particularidades por gateway

- **Asaas** — `$sandbox` troca a base URL. Chaves Pix próprias, porque é PSP (como Woovi e Efí).
Assinatura cobre os 14 endpoints da API. **O carnê responde PDF**, por isso usa
`HasHttpClient::download()`, que devolve o corpo cru — `request()` decodifica
JSON e devolveria `[]`. `destroy()` leva as cobranças pendentes e vencidas;
a pausa é `deactivate()`, e **reativar exige um novo `nextDueDate`**. `cycle` é
obrigatório na criação. Atualização de assinatura é `PUT`, como o resto do Asaas.
- **Efí** — **duas APIs com as mesmas credenciais**: Cobranças (`cobrancas.api...`,
trait `HasEfiClient`, boleto em `charge()`) e Pix (`pix.api...`, trait
`HasEfiPixClient`, **só por mTLS**). Cada API tem o seu token (`getToken()` e
Expand Down Expand Up @@ -232,8 +237,6 @@ quebra a integração, cobra o valor errado.

## O que ainda está em aberto

- `Subscription` só implementa `create()`. Listar, buscar, atualizar, cancelar,
carnê e NFe seguem pendentes na API do Asaas.
- Da API Pix da Efí ficaram de fora: Pix Automático pela jornada 1 (`solicrec`,
notificação no app do pagador), webhooks de recorrência e de cobrança recorrente
(`webhookrec`, `webhookcobr`), envio de Pix e split.
Expand Down
89 changes: 76 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,8 @@ todos está em [Conceitos](#conceitos).

### Asaas

O único com as cinco capacidades — é PSP, então emite chave Pix própria e
gerencia webhooks por API.
Um dos dois com as cinco capacidades, ao lado do Woovi. É PSP, então emite
chave Pix própria e gerencia webhooks por API.

```php
use PHPay\Asaas\AsaasGateway;
Expand Down Expand Up @@ -427,18 +427,83 @@ $phpay->customer()->restore($cliente['id']);

#### Assinaturas

A assinatura gera uma cobrança por ciclo, e cada uma é uma cobrança comum:
aparece em `getPayments()` e é tratada pelo recurso de cobrança.

```php
use PHPay\Asaas\Enums\SubscriptionCycleEnum;

$assinaturas = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription();

$assinatura = $assinaturas
->setCustomer($cliente) // ou setCustomerId('cus_...')
->setAmount(Money::reais('49,90'))
->setCycle(SubscriptionCycleEnum::MONTHLY)
->setSubscription([
'billingType' => 'BOLETO',
'nextDueDate' => '2026-10-10',
'description' => 'Plano mensal',
])
->create();
```

> O `create([...])` com o payload inteiro continua funcionando, e o array
> sobrescreve o que os setters montaram. O `cycle` é obrigatório: sem ele o
> Asaas recusa, e o PHPay barra antes.

Consulta e ciclo de vida:

```php
$assinaturas->setQueryParams(['customer' => 'cus_...', 'status' => 'ACTIVE'])->getAll();
$assinaturas->find($id);

/* muda as próximas cobranças; com updatePendingPayments, as pendentes também */
$assinaturas->update($id, ['description' => 'Plano anual', 'updatePendingPayments' => true]);

/* pausa: para de gerar cobranças e mantém as que existem */
$assinaturas->deactivate($id);
$assinaturas->reactivate($id, '2026-11-10'); // o Asaas exige um novo vencimento

/* remove: as cobranças pendentes e vencidas vão junto; as pagas ficam */
$assinaturas->destroy($id);
```

Cobranças, carnê e cartão:

```php
$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription();
$assinaturas->getPayments($id, ['status' => 'PENDING']);

/* o carnê vem como os bytes do PDF */
file_put_contents('carne.pdf', $assinaturas->paymentBook($id, month: 12, year: 2026));

/* troca o cartão sem cobrar — as cobranças pendentes passam para o novo */
$assinaturas->updateCreditCard($id, [
'creditCardToken' => $token, // ou creditCard + creditCardHolderInfo
'remoteIp' => $ipDoComprador,
]);
```

Nota fiscal emitida automaticamente para cada cobrança:

$phpay->setCustomer($cliente)->create([
'billingType' => 'BOLETO',
'value' => 100,
'nextDueDate' => '2026-04-09',
'cycle' => 'MONTHLY',
```php
$assinaturas->createInvoiceSettings($id, [
'municipalServiceName' => 'Desenvolvimento de software',
'effectiveDatePeriod' => 'ON_PAYMENT_CONFIRMATION',
'taxes' => [ // os sete são obrigatórios; 0 quando não houver
'retainIss' => false,
'iss' => 2,
'pis' => 0.65,
'cofins' => 3,
'csll' => 0,
'inss' => 0,
'ir' => 0,
],
]);

/* ou com um cliente existente */
$phpay->setCustomerId('cus_000006337812')->create([...]);
$assinaturas->getInvoiceSettings($id);
$assinaturas->updateInvoiceSettings($id, [...]);
$assinaturas->destroyInvoiceSettings($id);
$assinaturas->getInvoices($id); // as notas já emitidas
```

#### Webhooks e chaves Pix
Expand Down Expand Up @@ -1056,7 +1121,7 @@ Dois pontos merecem auditoria de quem vem da v1:

| Gateway | Cobranças | Clientes | Assinaturas | Webhooks | Pix |
| --- | :---: | :---: | :---: | :---: | :---: |
| **Asaas** | ✅ | ✅ | ✍️ | ✅ | ✅ |
| **Asaas** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Woovi/OpenPix** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Mercado Pago** | ✅ | ✅ | ✅ | — | ✅ |
| **PagBank** | ✅ | ✅ | ✅ | — | ✅ |
Expand All @@ -1068,8 +1133,6 @@ Dois pontos merecem auditoria de quem vem da v1:

**✅** pronto · **✍️** parcial · **🕥** planejado · **—** não existe na API do gateway

> Assinaturas do Asaas: criação pronta; listar, atualizar e cancelar pendentes.

---

## Contribuindo
Expand Down
58 changes: 42 additions & 16 deletions examples/asaas/subscriptions.php
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
<?php

use PHPay\Asaas\AsaasGateway;
use PHPay\Asaas\Enums\SubscriptionCycleEnum;
use PHPay\Asaas\Resources\Subscription\Subscription;
use PHPay\Exceptions\PHPayException;
use PHPay\PHPay;
use PHPay\Support\{Customer, Money};

require_once __DIR__ . '/../../vendor/autoload.php';

require_once __DIR__ . '/credentials.php';

$customer = [
'name' => NAME,
'cpfCnpj' => CPF_CNPJ,
];
$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX));

/**
* @var Subscription $phpay
* @var Subscription $subscriptions
*/
$phpay = PHPay::gateway(new AsaasGateway(TOKEN_ASAAS_SANDBOX))->subscription();
$subscriptions = $phpay->subscription();

try {
$subscriptionCreated = $phpay
->setCustomer($customer)
->create([
$subscription = $subscriptions
->setCustomer(Customer::make(NAME, CPF_CNPJ))
->setAmount(Money::reais('100,00'))
->setCycle(SubscriptionCycleEnum::MONTHLY)
->setSubscription([
'billingType' => 'BOLETO',
'value' => 100,
'nextDueDate' => date('Y-m-d', strtotime('+7 days')),
'discount' => [
'value' => 10,
Expand All @@ -38,23 +38,49 @@
'value' => 1,
'type' => 'FIXED', /* PERCENTAGE */
],
'cycle' => 'MONTHLY',
'description' => 'Teste de assinatura',
'maxPayments' => 12,
'externalReference' => '123456',
]);
])
->create();

$id = (string) $subscription['id'];

/* consulta */
$subscriptions->find($id);
$subscriptions->setQueryParams(['customer' => $subscription['customer']])->getAll();

print_r($subscriptionCreated);
/* as cobranças que a assinatura já gerou */
$subscriptions->getPayments($id);

/* para uma segunda assinatura do mesmo cliente, reaproveite o id */
$phpay
->setCustomerId($subscriptionCreated['customer'])
/* o carnê, em PDF */
file_put_contents(__DIR__ . '/carne.pdf', $subscriptions->paymentBook($id));

/* muda a descrição das próximas cobranças e das pendentes */
$subscriptions->update($id, [
'description' => 'Assinatura atualizada',
'updatePendingPayments' => true,
]);

/* pausa e reativa — reativar exige um novo vencimento */
$subscriptions->deactivate($id);
$subscriptions->reactivate($id, date('Y-m-d', strtotime('+30 days')));

/*
| para uma segunda assinatura do mesmo cliente, reaproveite o id — num
| recurso novo, porque o anterior guarda o que os setters montaram
*/
$phpay->subscription()
->setCustomerId((string) $subscription['customer'])
->create([
'billingType' => 'PIX',
'value' => 50,
'nextDueDate' => date('Y-m-d', strtotime('+7 days')),
'cycle' => 'MONTHLY',
]);

/* remove — as cobranças pendentes vão junto */
$subscriptions->destroy($id);
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
17 changes: 17 additions & 0 deletions src/Gateways/Asaas/Enums/SubscriptionCycleEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace PHPay\Asaas\Enums;

/**
* how often an Asaas subscription generates a charge.
*/
enum SubscriptionCycleEnum: string
{
case WEEKLY = 'WEEKLY';
case BIWEEKLY = 'BIWEEKLY';
case MONTHLY = 'MONTHLY';
case BIMONTHLY = 'BIMONTHLY';
case QUARTERLY = 'QUARTERLY';
case SEMIANNUALLY = 'SEMIANNUALLY';
case YEARLY = 'YEARLY';
}
Loading
Loading