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 @@ -7,6 +7,7 @@ examples/asaas/credentials.php
examples/efi/credentials.php
examples/mercadopago/credentials.php
examples/pagbank/credentials.php
examples/pagarme/credentials.php

# configurações locais do Claude Code (pessoais, não versionar)
.claude/settings.local.json
18 changes: 13 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,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** e **PagBank** (clientes, cobranças, assinaturas) e
**Efí** (cobranças).
capacidades), **Mercado Pago**, **PagBank** e **Pagar.me** (clientes, cobranças,
assinaturas) 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 +144,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`.
- **Pagar.me** — autenticação **Basic** (secret key como usuário, senha vazia), não
Bearer. Ambiente pelo prefixo `sk_test_`, host único, então sem `$sandbox`. Valores
em centavos. Cancelamento é `DELETE /charges/{id}` com valor opcional no corpo —
use `request('DELETE', ...)`, porque `delete()` do trait não manda corpo.
`webhookDeliveries()` é **extra do gateway concreto**, não capacidade: `/hooks` lê
entregas, não cadastra endpoints.
- **Mercado Pago** — **não tem URL de sandbox**: o ambiente vem do prefixo `TEST-` do
access token, então o construtor não recebe `$sandbox`. `POST /v1/payments` exige
`X-Idempotency-Key` (por isso `HasHttpClient::post()` aceita headers por requisição).
Expand All @@ -155,9 +161,11 @@ e rode `php examples/asaas/charges.php` (ou `make asaas resource=charges`).
carnê e NFe seguem pendentes na API do Asaas.
- A Efí só tem autorização e cobranças; `customer`, `webhook`, `pix` e `subscription`
lançam `NotImplementedException`.
- Nem o Mercado Pago nem o PagBank implementam `SupportsWebhooks` ou `SupportsPixKeys`,
e isso é correto: webhooks só têm configuração por painel ou `notification_url(s)` por
cobrança, e Pix nos dois é forma de pagamento. Não "resolva" isso criando stubs.
- Só o Asaas implementa `SupportsWebhooks` e `SupportsPixKeys`. Mercado Pago, PagBank e
Pagar.me registram endpoints por painel, e Pix neles é forma de pagamento. Não
"resolva" isso criando stubs — e não declare a capacidade por causa de uma API
parecida: o `/hooks` do Pagar.me lê entregas, é outra coisa, e por isso virou um
recurso fora do modelo.
- `Efi\Resources\Charge\Charge` tem `$items` e `$configuration` privados sem setter —
hoje sempre caem no fallback (`getItems()` monta um item a partir de
`description`/`value`; `getConfigurations()` usa fine 200 / interest 33).
Expand Down
107 changes: 96 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ O PHPay é uma biblioteca PHP que tem o objetivo tornar o trabalho de integraç
- Asaas (cobranças, clientes, webhooks, chaves Pix e assinaturas)
- Mercado Pago (cobranças, clientes e assinaturas)
- PagBank / PagSeguro (cobranças, assinantes e assinaturas)
- Pagar.me (cobranças, clientes e assinaturas)
- Efí (cobranças)

## ⬆️ Vindo da v1?
Expand Down Expand Up @@ -165,17 +166,22 @@ $phpay
Nem todo gateway oferece todo recurso. Cada gateway **declara** o que suporta
através de interfaces de capacidade, em vez de o contrato ser a união de tudo:

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

> Nem Mercado Pago nem PagBank expõem CRUD de webhooks por API: eles são
> registrados no painel, ou por cobrança através de `notification_url` /
> `notification_urls`.
| Capacidade | Interface | Asaas | Mercado Pago | PagBank | Pagar.me | Efí |
| --- | --- | :---: | :---: | :---: | :---: | :---: |
| Clientes | `SupportsCustomers` | ✅ | ✅ | ✅ | ✅ | — |
| Cobranças | `SupportsCharges` | ✅ | ✅ | ✅ | ✅ | ✅ |
| Webhooks | `SupportsWebhooks` | ✅ | — | — | — | — |
| Chaves Pix | `SupportsPixKeys` | ✅ | — | — | — | — |
| Assinaturas | `SupportsSubscriptions` | ✅ | ✅ | ✅ | ✅ | — |

> Só o Asaas expõe CRUD de webhooks por API. Nos outros, os endpoints são
> registrados no painel — a notificação vai por cobrança
> (`notification_url` / `notification_urls`), e o Pagar.me ainda deixa
> **consultar e reenviar entregas** via `webhookDeliveries()`, fora do modelo
> de capacidades.
>
> `SupportsPixKeys` significa gerenciar chaves e QR Code estático, o que só um
> PSP que emite chave própria oferece. Nos demais, Pix é forma de pagamento.

> `SupportsPixKeys` é mais estreito que "aceita Pix": ele significa gerenciar
> chaves e QR Code estático, algo que só um PSP que emite chave própria oferece.
Expand Down Expand Up @@ -379,6 +385,77 @@ Para conferir contra o sandbox de verdade:
PAGBANK_TOKEN='...' php examples/pagbank/sandbox-check.php
```

## 💠 Pagar.me

Autenticação Basic com a secret key, e ambiente pelo prefixo da chave — teste e
produção compartilham `api.pagar.me/core/v5`:

```php
use PHPay\PagarMe\PagarMeGateway;

$gateway = new PagarMeGateway(SECRET_KEY_PAGARME);

$gateway->isSandbox(); // true para chaves sk_test_
```

Valores em **centavos inteiros**, e Pix como forma de pagamento do pedido:

```php
$pedido = PHPay::gateway($gateway)->charge()
->setCustomer([
'name' => 'Mário Lucas',
'email' => 'fale@phpay.io',
'document' => '12345678901',
])
->addItem('Assinatura PHPay', 10050) // R$ 100,50
->setPix(1800) // expira em 30 minutos
->create();

$phpay->getPixCode($pedido['id']); // de charges[0].last_transaction.qr_code
```

O cancelamento é `DELETE`, com valor opcional para estorno parcial:

```php
$phpay->cancel($cobrancaId, 2500); // estorna R$ 25,00
$phpay->cancel($cobrancaId); // estorna tudo
```

Assinaturas aceitam um plano ou a recorrência no próprio payload:

```php
$phpay = PHPay::gateway($gateway)->subscription();

$plano = $phpay->createPlan([
'name' => 'Plano PHPay Mensal',
'interval' => 'month',
'interval_count' => 1,
'items' => [[
'name' => 'Mensalidade',
'quantity' => 1,
'pricing_scheme' => ['price' => 4990], // R$ 49,90
]],
]);

$phpay->setPlan($plano['id'])
->setCustomerId($customerId)
->create(['payment_method' => 'pix']);
```

### Consultando entregas de webhook

O Pagar.me deixa ler e reenviar os eventos que já despachou. Isso **não** é a
capacidade `SupportsWebhooks` — o cadastro dos endpoints é no dashboard — então
vive no gateway concreto, não na facade:

```php
$gateway->webhookDeliveries()->setFilter(['size' => 10])->getAll();
$gateway->webhookDeliveries()->resend($hookId);
```

É assim que o modelo de capacidades abre espaço para o que só um gateway
oferece: quem segura `PagarMeGateway` alcança, quem tipa uma capacidade não.

## 📝 Roadmap

- Definições de Arquitetura ✅
Expand Down Expand Up @@ -414,6 +491,14 @@ PAGBANK_TOKEN='...' php examples/pagbank/sandbox-check.php
- Webhook — sem CRUD por API
- Pix ✅ (como QR Code do pedido)

- Pagar.me.

- Cobranças ✅
- Clientes ✅ (com cartões salvos)
- Assinaturas ✅ (com ou sem plano)
- Webhook — leitura de entregas ✅, cadastro só no dashboard
- Pix ✅ (como forma de pagamento)

- Efí.

- Autorização ✅
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"PHPay\\Asaas\\": "src/Gateways/Asaas/",
"PHPay\\Efi\\": "src/Gateways/Efi/",
"PHPay\\MercadoPago\\": "src/Gateways/MercadoPago/",
"PHPay\\PagBank\\": "src/Gateways/PagBank/"
"PHPay\\PagBank\\": "src/Gateways/PagBank/",
"PHPay\\PagarMe\\": "src/Gateways/PagarMe/"
}
},
"autoload-dev": {
Expand Down
73 changes: 73 additions & 0 deletions examples/pagarme/charges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

use PHPay\Exceptions\PHPayException;
use PHPay\PagarMe\PagarMeGateway;
use PHPay\PagarMe\Resources\Charge\Charge;
use PHPay\PHPay;

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

require_once __DIR__ . '/credentials.php';

$gateway = new PagarMeGateway(SECRET_KEY_PAGARME);

/* o ambiente vem do prefixo da chave, não de uma url separada */
var_dump($gateway->isSandbox());

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

$customer = [
'name' => NAME,
'email' => EMAIL,
'document' => DOCUMENT,
'type' => 'individual',
];

try {
/*
| Pix é forma de pagamento do pedido. Todo valor é inteiro em CENTAVOS:
| R$ 100,50 é 10050.
*/
$pedido = $phpay
->setCustomer($customer)
->addItem('Assinatura PHPay', 10050)
->setPix(1800)
->create();

$pedidoId = (string) $pedido['id'];

/* copia-e-cola, que vem em charges[0].last_transaction.qr_code */
echo $phpay->getPixCode($pedidoId) . PHP_EOL;

$phpay->find($pedidoId);
$phpay->setQueryParams(['size' => 10])->getAll();

$cobrancaId = (string) $pedido['charges'][0]['id'];

echo $phpay->getStatus($cobrancaId) . PHP_EOL;

/* estorno parcial e total, em centavos, via DELETE */
$phpay->cancel($cobrancaId, 2500);
$phpay->cancel($cobrancaId);

/* boleto, reaproveitando um cliente que já existe */
PHPay::gateway($gateway)->charge()
->setCustomerId((string) $pedido['customer']['id'])
->addItem('Camiseta', 5990, 2)
->setBoleto(date('Y-m-d', strtotime('+5 days')), ['Não receber após o vencimento'])
->create();

/*
| Leitura de entregas de webhook. Não passa pela facade: é específico do
| Pagar.me, então vive no gateway concreto.
|
| O cadastro dos endpoints que recebem esses eventos é feito no dashboard,
| não pela API — por isso o gateway não declara SupportsWebhooks.
*/
$gateway->webhookDeliveries()->setFilter(['size' => 10])->getAll();
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
15 changes: 15 additions & 0 deletions examples/pagarme/credentials.example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/*
| Copie este arquivo para credentials.php e preencha com a sua secret key.
| credentials.php é ignorado pelo git — nunca commite credencial real.
|
| O Pagar.me não tem host de sandbox: teste e produção usam o mesmo endpoint,
| e o que decide o ambiente é o prefixo da chave (sk_test_ vs sk_live_).
*/

const SECRET_KEY_PAGARME = 'sk_test_';

const NAME = 'Mário Lucas';
const EMAIL = 'fale@phpay.io';
const DOCUMENT = '00000000000';
70 changes: 70 additions & 0 deletions examples/pagarme/subscriptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

use PHPay\Exceptions\PHPayException;
use PHPay\PagarMe\Enums\{IntervalEnum, PaymentMethodEnum};
use PHPay\PagarMe\PagarMeGateway;
use PHPay\PagarMe\Resources\Subscription\Subscription;
use PHPay\PHPay;

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

require_once __DIR__ . '/credentials.php';

$gateway = new PagarMeGateway(SECRET_KEY_PAGARME);

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

try {
/* preço do plano em CENTAVOS */
$plano = $phpay->createPlan([
'name' => 'Plano PHPay Mensal',
'interval' => IntervalEnum::MONTH->value,
'interval_count' => 1,
'payment_methods' => [PaymentMethodEnum::CREDIT_CARD->value, PaymentMethodEnum::PIX->value],
'items' => [[
'name' => 'Mensalidade',
'quantity' => 1,
'pricing_scheme' => ['price' => 4990],
]],
]);

$planoId = (string) $plano['id'];

/* o cliente pode nascer junto com a assinatura */
$assinatura = $phpay
->setPlan($planoId)
->setCustomer([
'name' => NAME,
'email' => EMAIL,
'document' => DOCUMENT,
'type' => 'individual',
])
->create(['payment_method' => PaymentMethodEnum::PIX->value]);

$assinaturaId = (string) $assinatura['id'];

$phpay->find($assinaturaId);
$phpay->setFilter(['size' => 10])->getAll();

/* assinatura sem plano: a recorrência vai no próprio payload */
PHPay::gateway($gateway)->subscription()
->setCustomerId((string) $assinatura['customer']['id'])
->create([
'payment_method' => PaymentMethodEnum::PIX->value,
'interval' => IntervalEnum::MONTH->value,
'interval_count' => 1,
'items' => [[
'name' => 'Avulso mensal',
'quantity' => 1,
'pricing_scheme' => ['price' => 2990],
]],
]);

$phpay->cancel($assinaturaId);
$phpay->destroyPlan($planoId);
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
9 changes: 9 additions & 0 deletions src/Gateways/PagarMe/Enums/CustomerTypeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace PHPay\PagarMe\Enums;

enum CustomerTypeEnum: string
{
case INDIVIDUAL = 'individual';
case COMPANY = 'company';
}
11 changes: 11 additions & 0 deletions src/Gateways/PagarMe/Enums/IntervalEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace PHPay\PagarMe\Enums;

enum IntervalEnum: string
{
case DAY = 'day';
case WEEK = 'week';
case MONTH = 'month';
case YEAR = 'year';
}
11 changes: 11 additions & 0 deletions src/Gateways/PagarMe/Enums/PaymentMethodEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace PHPay\PagarMe\Enums;

enum PaymentMethodEnum: string
{
case CREDIT_CARD = 'credit_card';
case DEBIT_CARD = 'debit_card';
case BOLETO = 'boleto';
case PIX = 'pix';
}
Loading
Loading