@isnouu/zpay-sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -2
- package/examples/create-pix.mjs +18 -0
- package/index.cjs +159 -0
- package/index.d.ts +206 -0
- package/index.mjs +2 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SDK Z.PAY · JavaScript / TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cliente para servidores Node.js 18+, com `import`, `require` e tipos TypeScript. Sem dependências de execução. Versão inicial: `0.1.0`.
|
|
4
|
+
|
|
5
|
+
## Instalar
|
|
6
|
+
|
|
7
|
+
Instale pelo npm:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @isnouu/zpay-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Para instalar a partir do arquivo distribuído com esta versão:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install ./zpay-sdk-0.1.0.tgz
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Também é possível instalar a pasta local: `npm install ./sdk/javascript`. O nome importado é `@isnouu/zpay-sdk`.
|
|
20
|
+
|
|
21
|
+
## Configurar e criar Pix
|
|
22
|
+
|
|
23
|
+
Gere as credenciais no painel da Z.PAY e configure `ZPAY_CLIENT_ID` e `ZPAY_CLIENT_SECRET` no ambiente do seu servidor.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { ZPay, ZPayError } from '@isnouu/zpay-sdk';
|
|
27
|
+
|
|
28
|
+
const zpay = new ZPay({
|
|
29
|
+
clientId: process.env.ZPAY_CLIENT_ID,
|
|
30
|
+
clientSecret: process.env.ZPAY_CLIENT_SECRET,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const payment = await zpay.payments.create({
|
|
34
|
+
amount: 19.90, // reais
|
|
35
|
+
payerName: 'Cliente',
|
|
36
|
+
description: 'Pedido 123',
|
|
37
|
+
idempotencyKey: 'pedido-123', // salve junto do pedido
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(payment.paymentId, payment.copyPaste);
|
|
41
|
+
const current = await zpay.payments.get(payment.paymentId);
|
|
42
|
+
console.log(current.status);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Em CommonJS: `const { ZPay, ZPayError } = require('@isnouu/zpay-sdk');`. Nunca importe o cliente com credenciais em páginas, componentes de navegador ou bots distribuídos aos clientes. A aplicação do lojista deve chamar seu próprio backend.
|
|
46
|
+
|
|
47
|
+
## Recursos
|
|
48
|
+
|
|
49
|
+
| Recurso | Métodos |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| Conta e tarifas | `account.stats()`, `account.fees()` |
|
|
52
|
+
| Cobranças Pix | `payments.create()`, `payments.get(id)`, `payments.payer(id)` |
|
|
53
|
+
| Doações | `donations.list(filtros)`, `donations.top(filtros)` |
|
|
54
|
+
| Checkout hospedado | `checkouts.create(dados)`, `checkouts.get(id)` |
|
|
55
|
+
| Saque para chave cadastrada | `withdrawals.create(dados)`, `withdrawals.get(id)`, `withdrawals.list(filtros)` |
|
|
56
|
+
| Envio Pix para chave ou copia e cola | `cashouts.create(dados)`, `cashouts.get(id)` |
|
|
57
|
+
| Reembolso Pix | `refunds.create({ transactionId })` |
|
|
58
|
+
| Transferência interna | `transfers.recipient(destino)`, `transfers.create(dados)` |
|
|
59
|
+
| Emissão de boleto | `boletos.create(dados)` |
|
|
60
|
+
| Pagamento de boleto | `billPayments.preview(dados)`, `create(dados)`, `get(id)`, `list(filtros)` |
|
|
61
|
+
| MEDs | `meds.list(filtros)`, `meds.get(id)` |
|
|
62
|
+
| Cripto pela Purin | `crypto.createPayment(dados)`, `getPayment(id)`, `balance()`, `summary()`, `operation(id)`, `swap(dados)`, `withdraw(dados)` |
|
|
63
|
+
| Subcontas | `subaccounts.create(dados)`, `list()`, `get(id)`, `update(id, dados)`, `block(id)`, `unblock(id)`, `delete(id)`, `transactions(id, filtros)`, `fund(id, dados)`, `transfer(id, dados)`, `payout(id, dados)`, `withdraw(id, dados)` |
|
|
64
|
+
| Split | `splits.create(dados)`, `splits.pix(dados)`, `splits.get(id)` |
|
|
65
|
+
|
|
66
|
+
Os nomes abreviados na tabela pertencem ao mesmo grupo da primeira chamada. Tipos dos parâmetros estão em `index.d.ts`. O SDK devolve a resposta da API sem transformar `pending` em confirmação de pagamento. Recursos dependem da disponibilidade e das permissões da conta na API.
|
|
67
|
+
|
|
68
|
+
Valores monetários são em reais, salvo quantidade LTC e campos explicitamente em centavos, como `splits.create({ value, amount })` no modo `fixed`. A precisão LTC é de oito casas decimais.
|
|
69
|
+
|
|
70
|
+
## Receber LTC e fazer swap
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const charge = await zpay.crypto.createPayment({
|
|
74
|
+
requestId: 'ltc-pedido-123',
|
|
75
|
+
amount: 50, // BRL; a Purin informa a quantidade exata LTC
|
|
76
|
+
currency: 'brl',
|
|
77
|
+
payCurrency: 'ltc',
|
|
78
|
+
payerName: 'Cliente',
|
|
79
|
+
});
|
|
80
|
+
console.log(charge.payAddress, charge.payAmount);
|
|
81
|
+
const balance = await zpay.crypto.balance(); // LTC e saldo legado separados
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
O swap converte saldo em reais para USDT BEP20, enviando à carteira informada. Não converte saldo LTC. Execute apenas após a confirmação do usuário:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const swap = await zpay.crypto.swap({
|
|
88
|
+
requestId: 'swap-pedido-456',
|
|
89
|
+
amount: 100, // reais a debitar
|
|
90
|
+
address: carteiraConfirmadaPeloUsuario,
|
|
91
|
+
confirmNotCoinbase: true,
|
|
92
|
+
acceptProviderRate: true, // cotação definida pela Purin no envio
|
|
93
|
+
// totpCode: codigo2faDaConta,
|
|
94
|
+
});
|
|
95
|
+
console.log(swap.id, swap.status, swap.receiveUSDT);
|
|
96
|
+
const updated = await zpay.crypto.operation(swap.id);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A API com os novos endpoints Purin precisa estar publicada. Recebimento LTC exige confirmação das taxas pelo administrador; o SDK não configura tarifas nem ignora regras de KYC, telefone, 2FA ou limites do provedor. Novas cobranças USDT não estão disponíveis: recebimentos novos usam LTC, e USDT está disponível no swap BRL → USDT BEP20.
|
|
100
|
+
|
|
101
|
+
## Paginação
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const page = await zpay.donations.list({
|
|
105
|
+
page: 1,
|
|
106
|
+
limit: 20,
|
|
107
|
+
status: 'paid',
|
|
108
|
+
date: '2026-10-27', // horário de Brasília
|
|
109
|
+
when: 'paid',
|
|
110
|
+
});
|
|
111
|
+
console.log(page.items, page.pagination);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
O SDK não percorre páginas nem consulta status em loop automaticamente. Respeite os limites da API e use webhooks para acompanhar os pagamentos.
|
|
115
|
+
|
|
116
|
+
## Falhas e referências
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
try {
|
|
120
|
+
await zpay.withdrawals.create({ amount: 50, idempotencyKey: 'saque-pedido-789' });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof ZPayError)) throw error;
|
|
123
|
+
console.error(error.status, error.code);
|
|
124
|
+
if (error.outcomeUnknown) {
|
|
125
|
+
// Consulte a referência original; não gere outra operação.
|
|
126
|
+
const original = await zpay.withdrawals.get('saque-pedido-789');
|
|
127
|
+
console.log(original.status);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`ZPayError` inclui `status`, `code`, `details`, `retryAfter` e `outcomeUnknown`. Uma falha de conexão após despacho, timeout, 429 ou resposta 5xx pode deixar o resultado desconhecido. Nenhuma requisição é repetida automaticamente. Guarde `idempotencyKey` ou `requestId` antes de enviar e reutilize a referência somente para a mesma operação e os mesmos dados.
|
|
133
|
+
|
|
134
|
+
O SDK exige chave explícita nos métodos cujos endpoints suportam esse campo. Checkouts e splits não oferecem essa garantia nesta versão da API; nunca repita a criação automaticamente. Reembolsos usam a transação original. Uma resposta HTTP 202 pode indicar processamento ou revisão, sem confirmar pagamento. Para cripto, salve o `id` da operação assim que recebido para consultar `crypto.operation(id)`.
|
|
135
|
+
|
|
136
|
+
## Timeout e cancelamento
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const client = new ZPay({
|
|
140
|
+
clientId: process.env.ZPAY_CLIENT_ID,
|
|
141
|
+
clientSecret: process.env.ZPAY_CLIENT_SECRET,
|
|
142
|
+
timeoutMs: 30000,
|
|
143
|
+
// baseUrl: 'http://localhost:3000/api/v1', // desenvolvimento local
|
|
144
|
+
});
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const pending = client.payments.get('payment-id', { signal: controller.signal });
|
|
147
|
+
// controller.abort();
|
|
148
|
+
const result = await pending;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Cancelar a requisição não cancela o pagamento ou saque na Z.PAY. `baseUrl` é a URL completa incluindo `/api/v1`; o padrão é `https://zpaysolution.com/api/v1`. Redirecionamentos são recusados para não encaminhar credenciais a outro destino. Você pode fornecer `fetch` compatível para testes ou instrumentação no servidor.
|
|
152
|
+
|
|
153
|
+
## Desenvolvimento e pacote
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
npm ci
|
|
157
|
+
npm test
|
|
158
|
+
npm run test:types
|
|
159
|
+
npm pack
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Os testes usam respostas simuladas, sem chaves reais ou movimentação financeira. O empacotamento inclui somente cliente, tipos, README e exemplos. A publicação de novas versões requer acesso ao escopo `@isnouu` no npm.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ZPay, ZPayError } from '@isnouu/zpay-sdk';
|
|
2
|
+
|
|
3
|
+
// Run with a stable order reference, e.g. node create-pix.mjs pedido-123.
|
|
4
|
+
const reference = process.argv[2];
|
|
5
|
+
if (!reference) throw new Error('Informe a referência persistida do pedido.');
|
|
6
|
+
const zpay = new ZPay({ clientId: process.env.ZPAY_CLIENT_ID, clientSecret: process.env.ZPAY_CLIENT_SECRET });
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const payment = await zpay.payments.create({
|
|
10
|
+
amount: 19.90, payerName: 'Cliente', description: `Pedido ${reference}`, idempotencyKey: reference,
|
|
11
|
+
});
|
|
12
|
+
console.log(JSON.stringify({ paymentId: payment.paymentId, status: payment.status, copyPaste: payment.copyPaste }, null, 2));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (!(error instanceof ZPayError)) throw error;
|
|
15
|
+
console.error(error.status, error.code, error.message);
|
|
16
|
+
if (error.outcomeUnknown) console.error('Resultado incerto. Consulte a referência original antes de qualquer nova tentativa.');
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
package/index.cjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BASE_URL = 'https://zpaysolution.com/api/v1';
|
|
4
|
+
const VERSION = '0.1.0';
|
|
5
|
+
|
|
6
|
+
class ZPayError extends Error {
|
|
7
|
+
constructor(message, { status = 0, code = 'ZPAY_ERROR', details = null, retryAfter = null, outcomeUnknown = false } = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ZPayError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.details = details;
|
|
13
|
+
this.retryAfter = retryAfter;
|
|
14
|
+
this.outcomeUnknown = outcomeUnknown;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function identifier(value) {
|
|
19
|
+
if (typeof value !== 'string' || !value.trim() || value === '.' || value === '..') throw new TypeError('Informe um identificador válido.');
|
|
20
|
+
return encodeURIComponent(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function keyed(body, field = 'idempotencyKey') {
|
|
24
|
+
const key = body?.[field];
|
|
25
|
+
if (typeof key !== 'string' || !key.trim() || key.length > 120) throw new TypeError(`Informe ${field} estável, com até 120 caracteres. Reutilize-o para a mesma operação.`);
|
|
26
|
+
if (field === 'requestId' && !/^[a-zA-Z0-9_:.-]{8,120}$/.test(key)) throw new TypeError('requestId deve ter de 8 a 120 caracteres: letras, números, ponto, dois-pontos, hífen ou sublinhado.');
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ZPay {
|
|
31
|
+
#clientId;
|
|
32
|
+
#clientSecret;
|
|
33
|
+
#fetch;
|
|
34
|
+
#baseUrl;
|
|
35
|
+
#timeoutMs;
|
|
36
|
+
|
|
37
|
+
constructor({ clientId, clientSecret, baseUrl = DEFAULT_BASE_URL, timeoutMs = 30000, fetch: fetchImpl = globalThis.fetch } = {}) {
|
|
38
|
+
if (typeof window !== 'undefined') throw new TypeError('Use o SDK no servidor; as credenciais da Z.PAY são privadas.');
|
|
39
|
+
for (const [name, value] of Object.entries({ clientId, clientSecret })) {
|
|
40
|
+
if (typeof value !== 'string' || !value.trim() || /[\r\n]/.test(value)) throw new TypeError(`Informe ${name} válido.`);
|
|
41
|
+
}
|
|
42
|
+
const url = new URL(baseUrl);
|
|
43
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
44
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))) {
|
|
45
|
+
throw new TypeError('baseUrl deve usar HTTPS, sem credenciais, query ou fragmento. HTTP é permitido apenas em localhost.');
|
|
46
|
+
}
|
|
47
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new TypeError('timeoutMs deve ser um inteiro positivo de até 2147483647.');
|
|
48
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('Use Node.js 18+ ou informe uma implementação de fetch.');
|
|
49
|
+
this.#clientId = clientId;
|
|
50
|
+
this.#clientSecret = clientSecret;
|
|
51
|
+
this.#fetch = fetchImpl;
|
|
52
|
+
this.#baseUrl = url.href.replace(/\/+$/, '');
|
|
53
|
+
this.#timeoutMs = timeoutMs;
|
|
54
|
+
|
|
55
|
+
const get = (path, query, options) => this.#request('GET', path, { ...options, query });
|
|
56
|
+
const post = (path, body, options) => this.#request('POST', path, { ...options, body });
|
|
57
|
+
this.account = Object.freeze({ stats: (query, options) => get('/stats', query, options), fees: options => get('/fees', undefined, options) });
|
|
58
|
+
this.payments = Object.freeze({
|
|
59
|
+
create: (body, options) => post('/payments', keyed(body), options),
|
|
60
|
+
get: (id, options) => get(`/payments/${identifier(id)}`, undefined, options),
|
|
61
|
+
payer: (id, options) => get(`/payments/${identifier(id)}/payer`, undefined, options),
|
|
62
|
+
});
|
|
63
|
+
this.donations = Object.freeze({ list: (query, options) => get('/donations', query, options), top: (query, options) => get('/top-donates', query, options) });
|
|
64
|
+
this.checkouts = Object.freeze({ create: (body, options) => post('/checkouts', body, options), get: (id, options) => get(`/checkouts/${identifier(id)}`, undefined, options) });
|
|
65
|
+
this.withdrawals = Object.freeze({
|
|
66
|
+
create: (body, options) => post('/withdraw', keyed(body), options),
|
|
67
|
+
get: (id, options) => get(`/withdrawals/${identifier(id)}`, undefined, options),
|
|
68
|
+
list: (query, options) => get('/withdrawals', query, options),
|
|
69
|
+
});
|
|
70
|
+
this.cashouts = Object.freeze({ create: (body, options) => post('/cashout', keyed(body), options), get: (id, options) => get(`/cashout/${identifier(id)}`, undefined, options) });
|
|
71
|
+
this.refunds = Object.freeze({ create: (body, options) => post('/refund', body, options) });
|
|
72
|
+
this.transfers = Object.freeze({ recipient: (to, options) => get('/transfers/internal/recipient', { to }, options), create: (body, options) => post('/transfers/internal', keyed(body), options) });
|
|
73
|
+
this.boletos = Object.freeze({ create: (body, options) => post('/boletos', keyed(body), options) });
|
|
74
|
+
this.billPayments = Object.freeze({
|
|
75
|
+
preview: (body, options) => post('/bill-payments/preview', body, options),
|
|
76
|
+
create: (body, options) => post('/bill-payments', keyed(body), options),
|
|
77
|
+
get: (id, options) => get(`/bill-payments/${identifier(id)}`, undefined, options),
|
|
78
|
+
list: (query, options) => get('/bill-payments', query, options),
|
|
79
|
+
});
|
|
80
|
+
this.meds = Object.freeze({ list: (query, options) => get('/meds', query, options), get: (id, options) => get(`/meds/${identifier(id)}`, undefined, options) });
|
|
81
|
+
this.crypto = Object.freeze({
|
|
82
|
+
createPayment: (body, options) => post('/payments/crypto', keyed(body, 'requestId'), options),
|
|
83
|
+
getPayment: (id, options) => get(`/payments/crypto/${identifier(id)}`, undefined, options),
|
|
84
|
+
balance: options => get('/crypto/balance', undefined, options),
|
|
85
|
+
summary: options => get('/crypto/purin', undefined, options),
|
|
86
|
+
operation: (id, options) => get(`/crypto/purin/operations/${identifier(id)}`, undefined, options),
|
|
87
|
+
swap: (body, options) => post('/crypto/swap', keyed(body, 'requestId'), options),
|
|
88
|
+
withdraw: (body, options) => post('/crypto/purin/withdraw', keyed(body, 'requestId'), options),
|
|
89
|
+
});
|
|
90
|
+
this.subaccounts = Object.freeze({
|
|
91
|
+
create: (body, options) => post('/subaccounts', body, options),
|
|
92
|
+
list: options => get('/subaccounts', undefined, options),
|
|
93
|
+
get: (id, options) => get(`/subaccounts/${identifier(id)}`, undefined, options),
|
|
94
|
+
update: (id, body, options) => this.#request('PATCH', `/subaccounts/${identifier(id)}`, { ...options, body }),
|
|
95
|
+
block: (id, options) => post(`/subaccounts/${identifier(id)}/block`, {}, options),
|
|
96
|
+
unblock: (id, options) => post(`/subaccounts/${identifier(id)}/unblock`, {}, options),
|
|
97
|
+
delete: (id, options) => this.#request('DELETE', `/subaccounts/${identifier(id)}`, options),
|
|
98
|
+
transactions: (id, query, options) => get(`/subaccounts/${identifier(id)}/transactions`, query, options),
|
|
99
|
+
fund: (id, body, options) => post(`/subaccounts/${identifier(id)}/fund`, keyed(body), options),
|
|
100
|
+
transfer: (id, body, options) => post(`/subaccounts/${identifier(id)}/transfer`, keyed(body), options),
|
|
101
|
+
payout: (id, body, options) => post(`/subaccounts/${identifier(id)}/payout`, keyed(body), options),
|
|
102
|
+
withdraw: (id, body, options) => post(`/subaccounts/${identifier(id)}/withdraw`, keyed(body), options),
|
|
103
|
+
});
|
|
104
|
+
this.splits = Object.freeze({ create: (body, options) => post('/split/payment', body, options), pix: (body, options) => post('/split/pix', body, options), get: (id, options) => get(`/split/payments/${identifier(id)}`, undefined, options) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #request(method, path, { body, query, signal } = {}) {
|
|
108
|
+
const url = new URL(this.#baseUrl + path);
|
|
109
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
110
|
+
if (value == null) continue;
|
|
111
|
+
if (!['string', 'number', 'boolean'].includes(typeof value) || (typeof value === 'number' && !Number.isFinite(value))) throw new TypeError(`Filtro inválido: ${key}.`);
|
|
112
|
+
url.searchParams.set(key, String(value));
|
|
113
|
+
}
|
|
114
|
+
// Serialize before dispatch so a local serialization error isn't reported as
|
|
115
|
+
// an uncertain financial outcome.
|
|
116
|
+
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
117
|
+
if (signal?.aborted) throw new ZPayError('Requisição cancelada antes do envio.', { code: 'REQUEST_ABORTED' });
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const abort = () => controller.abort();
|
|
120
|
+
let timedOut = false;
|
|
121
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
122
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, this.#timeoutMs);
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await this.#fetch(url.href, {
|
|
126
|
+
method, redirect: 'error', signal: controller.signal,
|
|
127
|
+
headers: { accept: 'application/json', 'content-type': 'application/json',
|
|
128
|
+
'client-id': this.#clientId, 'client-secret': this.#clientSecret, 'user-agent': `zpay-sdk-node/${VERSION}` },
|
|
129
|
+
...(payload === undefined ? {} : { body: payload }),
|
|
130
|
+
});
|
|
131
|
+
const text = await response.text();
|
|
132
|
+
let data = null;
|
|
133
|
+
if (text) {
|
|
134
|
+
try { data = JSON.parse(text); }
|
|
135
|
+
catch (_) { throw new ZPayError('A Z.PAY retornou uma resposta que não é JSON.', { status: response.status, code: 'INVALID_RESPONSE', outcomeUnknown: method !== 'GET' }); }
|
|
136
|
+
}
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const message = typeof data?.error === 'string' ? data.error : `Erro HTTP ${response.status} na Z.PAY.`;
|
|
139
|
+
// Do not expose credentials even if an upstream error echoes them.
|
|
140
|
+
const redact = value => typeof value === 'string' ? value.split(this.#clientSecret).join('[redacted]').split(this.#clientId).join('[redacted]') : value;
|
|
141
|
+
const safe = data == null ? null : JSON.parse(JSON.stringify(data, (_key, value) => redact(value)));
|
|
142
|
+
throw new ZPayError(redact(message), { status: response.status, code: redact(typeof data?.code === 'string' ? data.code : 'API_ERROR'),
|
|
143
|
+
details: safe, retryAfter: response.headers.get('retry-after'), outcomeUnknown: method !== 'GET' && (response.status >= 500 || [408, 409, 429].includes(response.status)) });
|
|
144
|
+
}
|
|
145
|
+
if (data == null || typeof data !== 'object' || Array.isArray(data)) throw new ZPayError('Formato de resposta inválido da Z.PAY.', { status: response.status, code: 'INVALID_RESPONSE', outcomeUnknown: method !== 'GET' });
|
|
146
|
+
return data; // 202 is an acknowledgement; it does not mean funds arrived.
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof ZPayError) throw error;
|
|
149
|
+
throw new ZPayError(timedOut ? 'Tempo de resposta da Z.PAY esgotado.' : signal?.aborted ? 'Requisição cancelada.' : 'Falha de conexão com a Z.PAY.', {
|
|
150
|
+
status: response?.status || 0, code: timedOut ? 'REQUEST_TIMEOUT' : signal?.aborted ? 'REQUEST_ABORTED' : 'NETWORK_ERROR', outcomeUnknown: method !== 'GET',
|
|
151
|
+
});
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
signal?.removeEventListener('abort', abort);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { ZPay, ZPayError, DEFAULT_BASE_URL, VERSION };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL: string;
|
|
2
|
+
export const VERSION: string;
|
|
3
|
+
export interface RequestOptions { signal?: AbortSignal }
|
|
4
|
+
export interface ZPayOptions {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
/** URL completa da API, incluindo /api/v1. */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Padrão: 30000 ms. Não provoca reenvio automático. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
}
|
|
13
|
+
export class ZPayError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
code: string;
|
|
16
|
+
details: unknown;
|
|
17
|
+
retryAfter: string | null;
|
|
18
|
+
/** A operação pode ter sido enviada; consulte antes de tentar novamente. */
|
|
19
|
+
outcomeUnknown: boolean;
|
|
20
|
+
constructor(message: string, options?: { status?: number; code?: string; details?: unknown; retryAfter?: string | null; outcomeUnknown?: boolean });
|
|
21
|
+
}
|
|
22
|
+
export interface ApiObject { [key: string]: unknown }
|
|
23
|
+
export interface Page<T> extends ApiObject {
|
|
24
|
+
items: T[];
|
|
25
|
+
pagination: { page: number; limit: number; total: number; totalPages: number };
|
|
26
|
+
}
|
|
27
|
+
export interface TagFilter { tag?: string; tagSlug?: string }
|
|
28
|
+
export interface ListQuery extends TagFilter {
|
|
29
|
+
page?: number; limit?: number; status?: string;
|
|
30
|
+
date?: string; hour?: number; from?: string; to?: string;
|
|
31
|
+
when?: 'created' | 'paid' | 'completed';
|
|
32
|
+
}
|
|
33
|
+
export interface Keyed { idempotencyKey: string }
|
|
34
|
+
export interface Payer {
|
|
35
|
+
payerName: string; payerDocument?: string; payerEmail?: string; payerPhone?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface CreatePayment extends Payer, Keyed {
|
|
38
|
+
/** Valor em reais, não centavos. */
|
|
39
|
+
amount: number;
|
|
40
|
+
description?: string; tag?: string; subaccountId?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Payment extends ApiObject {
|
|
43
|
+
paymentId: string; amount: number; status: string;
|
|
44
|
+
copyPaste: string | null; qrCodeBase64: string | null; qrcodeUrl?: string | null;
|
|
45
|
+
paymentMethod?: string; idempotencyKey?: string | null; subaccountId?: string | null;
|
|
46
|
+
paidAt?: string | null; replayed?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface Donation extends ApiObject {
|
|
49
|
+
paymentId: string; amount: number; payerName: string; description: string;
|
|
50
|
+
status: string; paidAt: string | null; createdAt: string;
|
|
51
|
+
}
|
|
52
|
+
export interface CreateCheckout {
|
|
53
|
+
amount: number; name: string; description: string; payerName?: string;
|
|
54
|
+
payerDocument?: string; payerEmail?: string; payerPhone?: string;
|
|
55
|
+
methods?: Array<'pix' | 'card' | 'boleto'>; maxInstallments?: number; tag?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Checkout extends ApiObject {
|
|
58
|
+
checkoutId: string; paymentId: string; amount: number; status: string;
|
|
59
|
+
checkoutUrl?: string; expiresAt: string; methods: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface Withdrawal extends ApiObject {
|
|
62
|
+
id: string; transactionId: string; status: string; amount: number; fee: number;
|
|
63
|
+
netAmount: number; clientTransactionId: string; kind: string;
|
|
64
|
+
createdAt: string; completedAt: string | null;
|
|
65
|
+
}
|
|
66
|
+
export interface CreateWithdrawal extends Keyed { amount: number; subaccountId?: string }
|
|
67
|
+
export type PixKeyType = 'CPF' | 'CNPJ' | 'EMAIL' | 'TELEFONE' | 'CHAVE_ALEATORIA';
|
|
68
|
+
export type CreateCashout = Keyed & { description?: string; subaccountId?: string } & (
|
|
69
|
+
{ amount: number; pixKey: string; pixKeyType: PixKeyType; qrCode?: never } |
|
|
70
|
+
{ qrCode: string; amount?: number; pixKey?: never; pixKeyType?: never }
|
|
71
|
+
);
|
|
72
|
+
export interface Transfer extends Keyed { to: string; amount: number; description?: string }
|
|
73
|
+
export interface CryptoFees extends ApiObject {
|
|
74
|
+
receivePercent: number | null; receiveFixedLtc: number | null;
|
|
75
|
+
receiveFeeConfirmed: boolean; withdrawFixedLtc: number | null;
|
|
76
|
+
usdtNetwork: 'BEP20'; usdtRate: 'provider_at_submission';
|
|
77
|
+
}
|
|
78
|
+
export interface CryptoOperation extends ApiObject {
|
|
79
|
+
id: string; kind: 'deposit' | 'swap' | 'withdraw'; provider: 'purincash';
|
|
80
|
+
status: 'created' | 'dispatching' | 'pending' | 'paid' | 'failed' | 'review';
|
|
81
|
+
amountBrl: number; currency: 'LTC' | 'USDT'; payCurrency: 'ltc' | 'usdt';
|
|
82
|
+
payAmount: number | null; fee: number | null; address: string | null;
|
|
83
|
+
network: 'Litecoin' | 'BEP20'; receiveUSDT: number | null; rateBRLPerUSDT: number | null;
|
|
84
|
+
txHash: string | null; donateId: string | null; createdAt: string;
|
|
85
|
+
paidAt?: string | null; expiresAt?: string | null;
|
|
86
|
+
}
|
|
87
|
+
export interface CryptoPayment extends CryptoOperation {
|
|
88
|
+
paymentId: string; amount: number; payAddress: string | null;
|
|
89
|
+
}
|
|
90
|
+
export interface CreateCryptoPayment {
|
|
91
|
+
requestId: string; amount: number; currency?: 'brl'; payCurrency?: 'ltc';
|
|
92
|
+
payerName?: string; description?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface CreateSwap {
|
|
95
|
+
requestId: string; amount: number; address: string;
|
|
96
|
+
confirmNotCoinbase: true; acceptProviderRate: true; totpCode?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface CreateCryptoWithdrawal {
|
|
99
|
+
requestId: string;
|
|
100
|
+
/** Quantidade em LTC, até oito casas decimais. */
|
|
101
|
+
amount: number;
|
|
102
|
+
address: string; totpCode?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface CryptoBalance extends ApiObject {
|
|
105
|
+
currency: 'ltc'; balance: number; provider: 'purincash'; fees: CryptoFees;
|
|
106
|
+
legacy: { currency: string; balance: number; pendingWithdrawal: number };
|
|
107
|
+
}
|
|
108
|
+
export interface SubaccountInput {
|
|
109
|
+
name: string; externalId: string; email?: string;
|
|
110
|
+
metadata?: Record<string, unknown>; feePercent?: number; feeFixed?: number;
|
|
111
|
+
payoutFeePercent?: number; payoutFeeFixed?: number;
|
|
112
|
+
}
|
|
113
|
+
export interface Subaccount extends ApiObject {
|
|
114
|
+
id: string; name: string; externalId: string; balance: number; status: string;
|
|
115
|
+
}
|
|
116
|
+
export interface SubaccountMove extends Keyed { amount: number; description?: string }
|
|
117
|
+
export interface CreateBoleto extends CreatePayment { payerEmail: string; payerDocument: string; dueDays?: number }
|
|
118
|
+
export interface BillPaymentPreview { line: string; amount?: number }
|
|
119
|
+
export interface CreateBillPayment extends BillPaymentPreview, Keyed { description?: string }
|
|
120
|
+
export interface Stats extends ApiObject {
|
|
121
|
+
balance?: number; totalEarnedToday: number; donationsToday: number;
|
|
122
|
+
totalEarnedThisMonth: number; donationsThisMonth: number; totalAllTime: number;
|
|
123
|
+
totalWithdrawn?: number;
|
|
124
|
+
}
|
|
125
|
+
export interface Fees extends ApiObject {
|
|
126
|
+
depositFee: number; depositRate: number; withdrawFee: number; cashoutFee: number;
|
|
127
|
+
minWithdraw: number; maxCashin: number; maxCashout: number;
|
|
128
|
+
highValueDepositThreshold: number; highValueDepositRate: number; accountType: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Cliente para Node.js 18+. Mantenha clientSecret exclusivamente no servidor. */
|
|
132
|
+
export class ZPay {
|
|
133
|
+
constructor(options: ZPayOptions);
|
|
134
|
+
readonly account: {
|
|
135
|
+
stats(query?: TagFilter, options?: RequestOptions): Promise<Stats>;
|
|
136
|
+
fees(options?: RequestOptions): Promise<Fees>;
|
|
137
|
+
};
|
|
138
|
+
readonly payments: {
|
|
139
|
+
create(body: CreatePayment, options?: RequestOptions): Promise<Payment>;
|
|
140
|
+
get(id: string, options?: RequestOptions): Promise<Payment>;
|
|
141
|
+
payer(id: string, options?: RequestOptions): Promise<ApiObject>;
|
|
142
|
+
};
|
|
143
|
+
readonly donations: {
|
|
144
|
+
list(query?: ListQuery, options?: RequestOptions): Promise<Page<Donation>>;
|
|
145
|
+
top(query?: TagFilter & { limit?: number }, options?: RequestOptions): Promise<ApiObject>;
|
|
146
|
+
};
|
|
147
|
+
readonly checkouts: {
|
|
148
|
+
create(body: CreateCheckout, options?: RequestOptions): Promise<Checkout>;
|
|
149
|
+
get(id: string, options?: RequestOptions): Promise<Checkout>;
|
|
150
|
+
};
|
|
151
|
+
readonly withdrawals: {
|
|
152
|
+
create(body: CreateWithdrawal, options?: RequestOptions): Promise<Withdrawal>;
|
|
153
|
+
get(id: string, options?: RequestOptions): Promise<Withdrawal>;
|
|
154
|
+
list(query?: ListQuery & { kind?: 'withdraw' | 'cashout' | 'refund' }, options?: RequestOptions): Promise<Page<Withdrawal>>;
|
|
155
|
+
};
|
|
156
|
+
readonly cashouts: {
|
|
157
|
+
create(body: CreateCashout, options?: RequestOptions): Promise<Withdrawal>;
|
|
158
|
+
get(id: string, options?: RequestOptions): Promise<Withdrawal>;
|
|
159
|
+
};
|
|
160
|
+
readonly refunds: { create(body: { transactionId: string }, options?: RequestOptions): Promise<ApiObject> };
|
|
161
|
+
readonly transfers: {
|
|
162
|
+
recipient(to: string, options?: RequestOptions): Promise<ApiObject>;
|
|
163
|
+
create(body: Transfer, options?: RequestOptions): Promise<ApiObject>;
|
|
164
|
+
};
|
|
165
|
+
readonly boletos: { create(body: CreateBoleto, options?: RequestOptions): Promise<Payment> };
|
|
166
|
+
readonly billPayments: {
|
|
167
|
+
preview(body: BillPaymentPreview, options?: RequestOptions): Promise<ApiObject>;
|
|
168
|
+
create(body: CreateBillPayment, options?: RequestOptions): Promise<ApiObject>;
|
|
169
|
+
list(query?: Pick<ListQuery, 'page' | 'limit'>, options?: RequestOptions): Promise<ApiObject>;
|
|
170
|
+
get(id: string, options?: RequestOptions): Promise<ApiObject>;
|
|
171
|
+
};
|
|
172
|
+
readonly meds: {
|
|
173
|
+
list(query?: ListQuery, options?: RequestOptions): Promise<ApiObject>;
|
|
174
|
+
get(id: string, options?: RequestOptions): Promise<ApiObject>;
|
|
175
|
+
};
|
|
176
|
+
readonly crypto: {
|
|
177
|
+
createPayment(body: CreateCryptoPayment, options?: RequestOptions): Promise<CryptoPayment>;
|
|
178
|
+
/** Cobranças novas LTC; consultas antigas podem devolver o formato legado. */
|
|
179
|
+
getPayment(id: string, options?: RequestOptions): Promise<CryptoPayment | ApiObject>;
|
|
180
|
+
balance(options?: RequestOptions): Promise<CryptoBalance>;
|
|
181
|
+
summary(options?: RequestOptions): Promise<ApiObject & { balance: number; fees: CryptoFees; operations: CryptoOperation[] }>;
|
|
182
|
+
operation(id: string, options?: RequestOptions): Promise<CryptoOperation>;
|
|
183
|
+
swap(body: CreateSwap, options?: RequestOptions): Promise<CryptoOperation>;
|
|
184
|
+
withdraw(body: CreateCryptoWithdrawal, options?: RequestOptions): Promise<CryptoOperation>;
|
|
185
|
+
};
|
|
186
|
+
readonly subaccounts: {
|
|
187
|
+
create(body: SubaccountInput, options?: RequestOptions): Promise<{ ok: boolean; subaccount: Subaccount }>;
|
|
188
|
+
list(options?: RequestOptions): Promise<{ subaccounts: Subaccount[] }>;
|
|
189
|
+
get(id: string, options?: RequestOptions): Promise<{ subaccount: Subaccount }>;
|
|
190
|
+
update(id: string, body: Partial<Pick<SubaccountInput, 'name' | 'externalId' | 'email' | 'metadata'>>, options?: RequestOptions): Promise<{ ok: boolean; subaccount: Subaccount }>;
|
|
191
|
+
block(id: string, options?: RequestOptions): Promise<{ ok: boolean; subaccount: Subaccount }>;
|
|
192
|
+
unblock(id: string, options?: RequestOptions): Promise<{ ok: boolean; subaccount: Subaccount }>;
|
|
193
|
+
delete(id: string, options?: RequestOptions): Promise<ApiObject>;
|
|
194
|
+
transactions(id: string, query?: ListQuery, options?: RequestOptions): Promise<ApiObject>;
|
|
195
|
+
fund(id: string, body: SubaccountMove, options?: RequestOptions): Promise<ApiObject>;
|
|
196
|
+
transfer(id: string, body: SubaccountMove & { to: string }, options?: RequestOptions): Promise<ApiObject>;
|
|
197
|
+
payout(id: string, body: SubaccountMove, options?: RequestOptions): Promise<ApiObject>;
|
|
198
|
+
withdraw(id: string, body: SubaccountMove, options?: RequestOptions): Promise<ApiObject>;
|
|
199
|
+
};
|
|
200
|
+
readonly splits: {
|
|
201
|
+
/** value em centavos; amount é percentual inteiro ou valor em centavos. */
|
|
202
|
+
create(body: { value: number; type: 'percentage' | 'fixed'; amount: number; split_account: string; payerName?: string; description?: string }, options?: RequestOptions): Promise<ApiObject>;
|
|
203
|
+
pix(body: ApiObject & { value: number; splits: ApiObject[] }, options?: RequestOptions): Promise<ApiObject>;
|
|
204
|
+
get(id: string, options?: RequestOptions): Promise<ApiObject>;
|
|
205
|
+
};
|
|
206
|
+
}
|
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isnouu/zpay-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK JavaScript e TypeScript da API Z.PAY para servidores Node.js",
|
|
5
|
+
"main": "./index.cjs",
|
|
6
|
+
"module": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"require": "./index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.cjs",
|
|
17
|
+
"index.mjs",
|
|
18
|
+
"index.d.ts",
|
|
19
|
+
"README.md",
|
|
20
|
+
"examples"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test test/*.test.cjs",
|
|
27
|
+
"test:types": "tsc -p tsconfig.json",
|
|
28
|
+
"prepack": "npm test && npm run test:types"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"zpay",
|
|
35
|
+
"pix",
|
|
36
|
+
"payments",
|
|
37
|
+
"sdk",
|
|
38
|
+
"typescript"
|
|
39
|
+
],
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|