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

# configurações locais do Claude Code (pessoais, não versionar)
.claude/settings.local.json
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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) e **Efí** (cobranças).
assinaturas), **Cielo** (cobranças e recorrência) 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,13 @@ 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`.
- **Cielo** — **dois hosts separados por tipo de operação**, não por domínio: escritas
em `api.cieloecommerce...`, consultas em `apiquery.cieloecommerce...`. O **mesmo
recurso** usa os dois, por isso `HasHttpClient::request()` aceita um client opcional
e o trait expõe `queryGet()`. Autenticação por headers `MerchantId`/`MerchantKey`.
Valores em centavos. Recorrência **não tem endpoint de criação**: nasce de uma venda
com bloco `RecurrentPayment`. Os endpoints de update da recorrência recebem um valor
JSON puro no corpo (`19900`, `"Monthly"`), não um objeto — daí o `putValue()`.
- **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 —
Expand Down
93 changes: 79 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Mercado Pago](#mercado-pago)
- [PagBank](#pagbank)
- [Pagar.me](#pagarme)
- [Cielo](#cielo)
- [Efí](#efí)
- [Exemplos executáveis](#exemplos-executáveis)
- [Migrando da v1](#migrando-da-v1)
Expand Down Expand Up @@ -67,13 +68,13 @@ Trocar de gateway é trocar a linha do construtor.

## Gateways suportados

| Capacidade | Interface | Asaas | Mercado Pago | PagBank | Pagar.me | Efí |
| --- | --- | :---: | :---: | :---: | :---: | :---: |
| Clientes | `SupportsCustomers` | ✅ | ✅ | ✅ | ✅ | — |
| Cobranças | `SupportsCharges` | ✅ | ✅ | ✅ | ✅ | ✅ |
| Assinaturas | `SupportsSubscriptions` | ✅ | ✅ | ✅ | ✅ | — |
| Webhooks | `SupportsWebhooks` | ✅ | — | — | — | — |
| Chaves Pix | `SupportsPixKeys` | ✅ | — | — | — | — |
| Capacidade | Interface | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | 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 @@ -232,6 +233,7 @@ que cada um faz em vez de inventar um padrão:
| --- | --- |
| **Asaas** | `$sandbox` no construtor — troca a URL |
| **PagBank** | `$sandbox` no construtor — troca a URL |
| **Cielo** | `$sandbox` no construtor — troca **as duas** URLs |
| **Efí** | `$sandbox` no construtor — troca a URL |
| **Mercado Pago** | Prefixo do token (`TEST-`); host único, sem `$sandbox` |
| **Pagar.me** | Prefixo da chave (`sk_test_`); host único, sem `$sandbox` |
Expand All @@ -257,6 +259,7 @@ em vez de falhar:
| **Mercado Pago** | Reais (decimal) | `100.50` |
| **PagBank** | Centavos (inteiro) | `10050` |
| **Pagar.me** | Centavos (inteiro) | `10050` |
| **Cielo** | Centavos (inteiro) | `10050` |
| **Efí** | Centavos (inteiro) | `10050` |

Nos gateways que usam centavos, o PHPay **recusa valor decimal na validação**,
Expand Down Expand Up @@ -538,6 +541,68 @@ $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.

### Cielo

A primeira **adquirente** da biblioteca, e a forma mostra: não há recurso de
cliente — ele é um campo da venda. Daí as duas capacidades.

A particularidade é que a Cielo separa **dois hosts por tipo de operação**:
escritas vão para `api.cieloecommerce...`, consultas para
`apiquery.cieloecommerce...`. O mesmo recurso usa os dois, e o PHPay roteia
sozinho — `create()` vai num, `find()` no outro.

```php
use PHPay\Cielo\CieloGateway;

$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->charge();

$venda = $phpay
->setOrderId('pedido-1')
->setCustomer(['Name' => 'Mário Lucas'])
->setPix(15700) // R$ 157,00
->setRequestId('pedido-1') // idempotência
->create();

$phpay->getPixCode($venda['Payment']['PaymentId']);
```

Cartão em duas etapas — autoriza agora, captura depois:

```php
$phpay
->setCustomer(['Name' => 'Mário Lucas'])
->setCreditCard(15700, $cartao, installments: 3) // capture: false por padrão
->create();

$phpay->capture($paymentId);
$phpay->cancel($paymentId, 2500); // estorna R$ 25,00
```

#### Recorrência

A Cielo **não tem endpoint de criar assinatura**: a recorrência nasce de uma
venda com um bloco `RecurrentPayment`, e só então ganha um `RecurrentPaymentId`
próprio. Sempre cobra cartão.

```php
use PHPay\Cielo\Enums\RecurrentIntervalEnum;

$phpay = PHPay::gateway(new CieloGateway(MERCHANT_ID, MERCHANT_KEY))->subscription();

$recorrencia = $phpay
->setCustomer(['Name' => 'Mário Lucas'])
->setCard($cartao)
->setInterval(RecurrentIntervalEnum::MONTHLY)
->setEndDate('2027-12-31')
->create(15700);

$id = $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId'];

$phpay->updateAmount($id, 19900);
$phpay->deactivate($id);
$phpay->reactivate($id);
```

### Efí

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

### Cobertura por gateway

| | Asaas | Mercado Pago | PagBank | Pagar.me | Efí |
| --- | :---: | :---: | :---: | :---: | :---: |
| Cobranças | ✅ | ✅ | ✅ | ✅ | ✅ |
| Clientes | ✅ | ✅ | ✅ | ✅ | 🕥 |
| Assinaturas | ✍️ | ✅ | ✅ | ✅ | 🕥 |
| Webhooks | ✅ | — | — | leitura ✅ | 🕥 |
| Pix | ✅ | ✅ | ✅ | ✅ | 🕥 |
| | Asaas | Mercado Pago | PagBank | Pagar.me | Cielo | 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 @@ -25,7 +25,8 @@
"PHPay\\Efi\\": "src/Gateways/Efi/",
"PHPay\\MercadoPago\\": "src/Gateways/MercadoPago/",
"PHPay\\PagBank\\": "src/Gateways/PagBank/",
"PHPay\\PagarMe\\": "src/Gateways/PagarMe/"
"PHPay\\PagarMe\\": "src/Gateways/PagarMe/",
"PHPay\\Cielo\\": "src/Gateways/Cielo/"
}
},
"autoload-dev": {
Expand Down
71 changes: 71 additions & 0 deletions examples/cielo/charges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

use PHPay\Cielo\CieloGateway;
use PHPay\Cielo\Enums\SaleStatusEnum;
use PHPay\Cielo\Resources\Charge\Charge;
use PHPay\Exceptions\PHPayException;
use PHPay\PHPay;

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

require_once __DIR__ . '/credentials.php';

/**
* @var Charge $phpay
*/
$phpay = PHPay::gateway(new CieloGateway(CIELO_MERCHANT_ID, CIELO_MERCHANT_KEY))->charge();

try {
/*
| Venda com Pix. Todo valor é inteiro em CENTAVOS: R$ 157,00 é 15700.
|
| A criação vai para o host de escrita; as consultas abaixo vão para o de
| query. O PHPay roteia sozinho.
*/
$venda = $phpay
->setOrderId('pedido-' . time())
->setCustomer(['Name' => NAME])
->setPix(15700)
/* use uma chave estável do seu domínio para tornar o retry seguro */
->setRequestId('pedido-123456')
->create();

$paymentId = (string) $venda['Payment']['PaymentId'];

/* copia-e-cola do Pix */
echo $phpay->getPixCode($paymentId) . PHP_EOL;

/* status, como enum */
$status = $phpay->getStatus($paymentId);

if ($status !== null) {
echo SaleStatusEnum::from($status)->name . PHP_EOL;
}

/* consulta pelas vendas de um pedido do seu sistema */
$phpay->findByOrderId('pedido-123456');

/*
| Venda com cartão em duas etapas: autoriza agora, captura depois.
| Passe capture: true para autorizar e capturar de uma vez.
*/
$comCartao = PHPay::gateway(new CieloGateway(CIELO_MERCHANT_ID, CIELO_MERCHANT_KEY))
->charge()
->setCustomer(['Name' => NAME])
->setCreditCard(15700, [
'CardNumber' => '0000000000000001',
'Holder' => 'Mario Lucas',
'ExpirationDate' => '12/2030',
'SecurityCode' => '123',
'Brand' => 'Visa',
], installments: 3)
->create();

$cartaoId = (string) $comCartao['Payment']['PaymentId'];

$phpay->capture($cartaoId); /* captura total */
$phpay->cancel($cartaoId, 2500); /* estorna R$ 25,00 */
$phpay->cancel($cartaoId); /* estorna o restante */
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
15 changes: 15 additions & 0 deletions examples/cielo/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 as credenciais de
| sandbox. credentials.php é ignorado pelo git — nunca commite credencial real.
|
| O sandbox da Cielo é self-service: o cadastro devolve MerchantId e MerchantKey
| sem exigir afiliação comercial.
| https://cadastrosandbox.cieloecommerce.cielo.com.br/
*/

const CIELO_MERCHANT_ID = '';
const CIELO_MERCHANT_KEY = '';

const NAME = 'Mário Lucas';
52 changes: 52 additions & 0 deletions examples/cielo/subscriptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

use PHPay\Cielo\CieloGateway;
use PHPay\Cielo\Enums\RecurrentIntervalEnum;
use PHPay\Cielo\Resources\Subscription\Subscription;
use PHPay\Exceptions\PHPayException;
use PHPay\PHPay;

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

require_once __DIR__ . '/credentials.php';

/**
* A Cielo não tem endpoint de "criar assinatura": a recorrência nasce de uma
* venda que carrega um bloco RecurrentPayment, e só então ganha um
* RecurrentPaymentId próprio para ser gerenciada. Sempre cobra cartão.
*
* @var Subscription $phpay
*/
$phpay = PHPay::gateway(new CieloGateway(CIELO_MERCHANT_ID, CIELO_MERCHANT_KEY))->subscription();

try {
$recorrencia = $phpay
->setOrderId('assinatura-' . time())
->setCustomer(['Name' => NAME])
->setCard([
'CardNumber' => '0000000000000001',
'Holder' => 'Mario Lucas',
'ExpirationDate' => '12/2030',
'SecurityCode' => '123',
'Brand' => 'Visa',
])
->setInterval(RecurrentIntervalEnum::MONTHLY)
->setEndDate('2027-12-31')
->create(15700); /* R$ 157,00 */

$recorrenciaId = (string) $recorrencia['Payment']['RecurrentPayment']['RecurrentPaymentId'];

$phpay->find($recorrenciaId);

/* reajuste e mudança de periodicidade */
$phpay->updateAmount($recorrenciaId, 19900);
$phpay->updateInterval($recorrenciaId, RecurrentIntervalEnum::ANNUAL);
$phpay->updateNextPaymentDate($recorrenciaId, date('Y-m-d', strtotime('+30 days')));
$phpay->updateEndDate($recorrenciaId, '2028-01-31');

/* suspender e retomar */
$phpay->deactivate($recorrenciaId);
$phpay->reactivate($recorrenciaId);
} catch (PHPayException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
71 changes: 71 additions & 0 deletions src/Gateways/Cielo/CieloGateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace PHPay\Cielo;

use GuzzleHttp\Client;
use PHPay\Cielo\Interface\CieloGatewayInterface;
use PHPay\Cielo\Resources\Charge\Charge;
use PHPay\Cielo\Resources\Subscription\Subscription;

class CieloGateway implements CieloGatewayInterface
{
/**
* construct
*
* @param string $merchantId
* @param string $merchantKey
* @param bool $sandbox
* @param Client|null $client injected write client, mainly for tests
* @param Client|null $queryClient injected query client, mainly for tests
*/
public function __construct(
private string $merchantId,
private string $merchantKey,
private bool $sandbox = true,
private ?Client $client = null,
private ?Client $queryClient = null,
) {
}

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

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

/**
* subscription
*
* @return Subscription
*/
public function subscription(): Subscription
{
return new Subscription(
$this->merchantId,
$this->merchantKey,
$this->sandbox,
$this->client,
$this->queryClient
);
}
}
Loading
Loading