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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules/
.idea/

# credenciais dos exemplos (nunca versionar)
examples/abacatepay/credentials.php
examples/asaas/credentials.php
examples/efi/credentials.php
examples/mercadopago/credentials.php
Expand Down
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Orientações para o Claude Code trabalhar neste repositório.
PHPay (`phpay-io/phpay`) é uma **biblioteca PHP** (não uma aplicação) que padroniza a
integração com gateways de pagamento brasileiros. Hoje suporta **Asaas** (as cinco
capacidades), **Mercado Pago**, **PagBank** e **Pagar.me** (clientes, cobranças,
assinaturas), **Cielo** (cobranças e recorrência), **Rede** e **Efí** (cobranças).
assinaturas), **Cielo** (cobranças e recorrência), **AbacatePay** (clientes e
cobranças), **Rede** e **Efí** (cobranças).

Requisitos: PHP `^8.1` para consumir a lib; `^8.2` para rodar o ambiente de dev
(Pest 3 e Termwind 2 exigem 8.2+). Dependências de runtime: `ext-curl`, `ext-json`,
Expand Down Expand Up @@ -144,6 +145,12 @@ e rode `php examples/asaas/charges.php` (ou `make asaas resource=charges`).
inteiro em centavos** — os validadores recusam decimal, porque mandar `10.50` onde
se espera `1050` cobra onze centavos. Pix é `qr_codes` do pedido (um só por pedido,
copia-e-cola em `qr_codes[0].text`), não uma `charge`.
- **AbacatePay** — host único e **sem prefixo de chave**: não dá para derivar o
ambiente da credencial, então **não existe `isSandbox()`** — inventar convenção aqui
seria mentira. A resposta da cobrança traz `devMode`, e é isso que `isDevMode()` lê.
Cobrança é montada por **produtos**, não por valor; preço em centavos com mínimo de
100. `frequency` só aceita `ONE_TIME`, por isso sem assinaturas. Cupons são extra do
gateway concreto, como o `webhookDeliveries()` do Pagar.me.
- **Rede** — **host de OAuth separado do host de API**, e o caminho do token muda por
ambiente (`oauth2/token` vs `redelabs/oauth2/token`) — está em `RedeEnvironment`,
fora do trait, para o gateway ler sem puxar os verbos HTTP. **O token expira**: é o
Expand Down
83 changes: 69 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [Pagar.me](#pagarme)
- [Cielo](#cielo)
- [Rede](#rede)
- [AbacatePay](#abacatepay)
- [Efí](#efí)
- [Exemplos executáveis](#exemplos-executáveis)
- [Migrando da v1](#migrando-da-v1)
Expand Down Expand Up @@ -69,13 +70,13 @@ Trocar de gateway é trocar a linha do construtor.

## Gateways suportados

| Capacidade | Interface | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | Rede | Efí |
| --- | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Clientes | `SupportsCustomers` | ✅ | ✅ | ✅ | ✅ | — | — | — |
| Cobranças | `SupportsCharges` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Assinaturas | `SupportsSubscriptions` | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
| Webhooks | `SupportsWebhooks` | ✅ | — | — | — | — | — | — |
| Chaves Pix | `SupportsPixKeys` | ✅ | — | — | — | — | — | — |
| Capacidade | Interface | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | Rede | Abacate | Efí |
| --- | --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Clientes | `SupportsCustomers` | ✅ | ✅ | ✅ | ✅ | — | — | ✅ | — |
| Cobranças | `SupportsCharges` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Assinaturas | `SupportsSubscriptions` | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — |
| Webhooks | `SupportsWebhooks` | ✅ | — | — | — | — | — | — | — |
| Chaves Pix | `SupportsPixKeys` | ✅ | — | — | — | — | — | — | — |

Duas colunas merecem explicação, porque a ausência de ✅ **não** quer dizer que o
gateway não aceita Pix ou não manda webhook:
Expand Down Expand Up @@ -239,6 +240,7 @@ que cada um faz em vez de inventar um padrão:
| **Rede** | `$sandbox` no construtor — troca **as duas** URLs e o caminho do token |
| **Mercado Pago** | Prefixo do token (`TEST-`); host único, sem `$sandbox` |
| **Pagar.me** | Prefixo da chave (`sk_test_`); host único, sem `$sandbox` |
| **AbacatePay** | Pela chave usada; host único, **sem prefixo** — a cobrança informa em `devMode` |

Nos dois últimos, `isSandbox()` diz em qual ambiente você está:

Expand All @@ -263,6 +265,7 @@ em vez de falhar:
| **Pagar.me** | Centavos (inteiro) | `10050` |
| **Cielo** | Centavos (inteiro) | `10050` |
| **Rede** | Centavos (inteiro) | `10050` |
| **AbacatePay** | Centavos (inteiro, mín. 100) | `10050` |
| **Efí** | Centavos (inteiro) | `10050` |

Nos gateways que usam centavos, o PHPay **recusa valor decimal na validação**,
Expand Down Expand Up @@ -654,6 +657,58 @@ $gateway->authorization()->hasValidToken();
$gateway->authorization()->forget();
```

### AbacatePay

Pix nativo, e **mesmo assim não declara `SupportsPixKeys`** — aquela capacidade
é sobre gerenciar chaves e QR Code estático, coisa de PSP. Aqui Pix é o método
de pagamento da cobrança, e o único aceito.

Também não declara assinaturas: a API documenta `ONE_TIME` como a única
frequência aceita.

A cobrança é um **link de pagamento montado a partir de produtos**, não de um
valor solto — o total vem calculado em `amount`. Preço em centavos, com
**mínimo de 100** (R$ 1,00) por produto.

```php
use PHPay\AbacatePay\AbacatePayGateway;

$gateway = new AbacatePayGateway(ABACATEPAY_TOKEN);

$cobranca = PHPay::gateway($gateway)->charge()
->setCustomer([
'name' => 'Mário Lucas',
'email' => 'fale@phpay.io',
'cellphone' => '(11) 4002-8922',
'taxId' => '12345678901',
])
->addProduct('prod-1234', 'Assinatura PHPay', 2000) // R$ 20,00
->setUrls(
completionUrl: 'https://exemplo.test/obrigado',
returnUrl: 'https://exemplo.test/loja'
)
->create();

$phpay->getPaymentUrl($cobranca); // o link para onde mandar o cliente
$phpay->isDevMode($cobranca); // em qual ambiente a cobrança nasceu
```

O `externalId` do produto é o id **no seu sistema** — o AbacatePay cria o
produto do lado dele a partir dele, então precisa ser único.

#### Cupons de desconto

Nenhum outro gateway da biblioteca tem isso, então não é capacidade: vive no
gateway concreto, como o `webhookDeliveries()` do Pagar.me.

```php
$gateway->coupons()->create([
'code' => 'PHPAY10',
'discountKind' => 'PERCENTAGE',
'discount' => 10,
]);
```

### Efí

Só cobranças, por enquanto. O gateway **não faz chamada de rede no construtor**
Expand Down Expand Up @@ -730,13 +785,13 @@ Dois pontos merecem auditoria de quem vem da v1:

### Cobertura por gateway

| | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | Rede | Efí |
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Cobranças | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Clientes | ✅ | ✅ | ✅ | ✅ | — | — | 🕥 |
| Assinaturas | ✍️ | ✅ | ✅ | ✅ | ✅ | — | 🕥 |
| Webhooks | ✅ | — | — | leitura ✅ | — | — | 🕥 |
| Pix | ✅ | ✅ | ✅ | ✅ | ✅ | 🕥 | 🕥 |
| | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | Rede | Abacate | Efí |
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Cobranças | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Clientes | ✅ | ✅ | ✅ | ✅ | — | — | ✅ | 🕥 |
| Assinaturas | ✍️ | ✅ | ✅ | ✅ | ✅ | — | — | 🕥 |
| Webhooks | ✅ | — | — | leitura ✅ | — | — | — | 🕥 |
| Pix | ✅ | ✅ | ✅ | ✅ | ✅ | 🕥 | ✅ | 🕥 |

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

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"PHPay\\PagBank\\": "src/Gateways/PagBank/",
"PHPay\\PagarMe\\": "src/Gateways/PagarMe/",
"PHPay\\Cielo\\": "src/Gateways/Cielo/",
"PHPay\\Rede\\": "src/Gateways/Rede/"
"PHPay\\Rede\\": "src/Gateways/Rede/",
"PHPay\\AbacatePay\\": "src/Gateways/AbacatePay/"
}
},
"autoload-dev": {
Expand Down
79 changes: 79 additions & 0 deletions examples/abacatepay/charges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

use PHPay\AbacatePay\AbacatePayGateway;
use PHPay\AbacatePay\Resources\Charge\Charge;
use PHPay\Exceptions\PHPayException;
use PHPay\PHPay;

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

require_once __DIR__ . '/credentials.php';

$gateway = new AbacatePayGateway(ABACATEPAY_TOKEN);

/**
* @var Charge $phpay
*/
$phpay = PHPay::gateway($gateway)->charge();

$cliente = [
'name' => NAME,
'email' => EMAIL,
'cellphone' => CELLPHONE,
'taxId' => TAX_ID,
];

try {
/*
| A cobrança é um link de pagamento montado a partir de PRODUTOS, não de um
| valor solto — o total vem calculado na resposta, em `amount`.
|
| Preço em CENTAVOS, com mínimo de 100 (R$ 1,00) por produto.
| O externalId é o id do produto no SEU sistema, e precisa ser único.
*/
$cobranca = $phpay
->setCustomer($cliente)
->addProduct('prod-1234', 'Assinatura PHPay', 2000) /* R$ 20,00 */
->addProduct('prod-5678', 'Camiseta', 5990, 2, 'Tamanho M') /* R$ 59,90 cada */
->setUrls(
completionUrl: 'https://exemplo.test/obrigado',
returnUrl: 'https://exemplo.test/loja'
)
->create();

/* o link para onde você manda o cliente */
echo $phpay->getPaymentUrl($cobranca) . PHP_EOL;

/*
| Como a URL é única para os dois ambientes, é a cobrança que diz em qual
| deles ela nasceu.
*/
var_dump($phpay->isDevMode($cobranca));

/* listagem */
$phpay->setQueryParams(['limit' => 10])->getAll();

/* cliente avulso, para reaproveitar em cobranças futuras */
$criado = PHPay::gateway($gateway)->customer($cliente)->create();

PHPay::gateway($gateway)->charge()
->setCustomerId((string) ($criado['data']['id'] ?? ''))
->addProduct('prod-1234', 'Assinatura PHPay', 2000)
->setUrls('https://exemplo.test/obrigado', 'https://exemplo.test/loja')
->create();

/*
| Cupons de desconto não passam pela facade: nenhum outro gateway da
| biblioteca tem isso, então vive no gateway concreto.
*/
$gateway->coupons()->create([
'code' => 'PHPAY10',
'discountKind' => 'PERCENTAGE',
'discount' => 10,
'maxRedeems' => 100,
]);

$gateway->coupons()->getAll();
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
17 changes: 17 additions & 0 deletions examples/abacatepay/credentials.example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/*
| Copie este arquivo para credentials.php e preencha com a sua chave de API.
| credentials.php é ignorado pelo git — nunca commite credencial real.
|
| O AbacatePay tem uma URL só. O ambiente depende de a chave ter sido criada
| em dev mode ou em produção, e a chave não traz prefixo que diferencie: a
| cobrança criada informa isso no campo devMode.
*/

const ABACATEPAY_TOKEN = '';

const NAME = 'Mário Lucas';
const EMAIL = 'fale@phpay.io';
const CELLPHONE = '(11) 4002-8922';
const TAX_ID = '00000000000';
70 changes: 70 additions & 0 deletions src/Gateways/AbacatePay/AbacatePayGateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace PHPay\AbacatePay;

use GuzzleHttp\Client;
use PHPay\AbacatePay\Interface\AbacatePayGatewayInterface;
use PHPay\AbacatePay\Resources\Charge\Charge;
use PHPay\AbacatePay\Resources\Coupon\Coupon;
use PHPay\AbacatePay\Resources\Customer\Customer;

class AbacatePayGateway implements AbacatePayGatewayInterface
{
/**
* construct
*
* there is no $sandbox flag. AbacatePay serves one host, and which
* environment answers depends on whether the key was created in dev mode
* or in production — the key carries no prefix that tells them apart, so
* the library does not guess. A created billing reports it in `devMode`.
*
* @param string $token
* @param Client|null $client injected http client, mainly for tests
*/
public function __construct(
private string $token,
private ?Client $client = null,
) {
}

/**
* gateway name
*
* @return string
*/
public function name(): string
{
return 'AbacatePay';
}

/**
* customer
*
* @param array<mixed> $customer
* @return Customer
*/
public function customer(array $customer = []): Customer
{
return new Customer($this->token, $customer, $this->client);
}

/**
* charge
*
* @return Charge
*/
public function charge(): Charge
{
return new Charge($this->token, $this->client);
}

/**
* discount coupons
*
* @return Coupon
*/
public function coupons(): Coupon
{
return new Coupon($this->token, $this->client);
}
}
14 changes: 14 additions & 0 deletions src/Gateways/AbacatePay/Enums/BillingFrequencyEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace PHPay\AbacatePay\Enums;

/**
* how often a billing charges.
*
* the API documents ONE_TIME as the only accepted value, which is why the
* gateway does not declare SupportsSubscriptions.
*/
enum BillingFrequencyEnum: string
{
case ONE_TIME = 'ONE_TIME';
}
14 changes: 14 additions & 0 deletions src/Gateways/AbacatePay/Enums/BillingMethodEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace PHPay\AbacatePay\Enums;

/**
* payment methods a billing accepts.
*
* only Pix today, and the API takes exactly one — the enum exists so the
* value is not a loose string, and so a second method has a place to land.
*/
enum BillingMethodEnum: string
{
case PIX = 'PIX';
}
12 changes: 12 additions & 0 deletions src/Gateways/AbacatePay/Enums/BillingStatusEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace PHPay\AbacatePay\Enums;

enum BillingStatusEnum: string
{
case PENDING = 'PENDING';
case EXPIRED = 'EXPIRED';
case CANCELLED = 'CANCELLED';
case PAID = 'PAID';
case REFUNDED = 'REFUNDED';
}
Loading
Loading