nxgate-sdk-python 1.0.0__py3-none-any.whl
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.
- nxgate/__init__.py +59 -0
- nxgate/auth.py +105 -0
- nxgate/client.py +251 -0
- nxgate/errors.py +50 -0
- nxgate/hmac_signer.py +56 -0
- nxgate/types.py +297 -0
- nxgate/webhook.py +48 -0
- nxgate_sdk_python-1.0.0.dist-info/METADATA +301 -0
- nxgate_sdk_python-1.0.0.dist-info/RECORD +12 -0
- nxgate_sdk_python-1.0.0.dist-info/WHEEL +5 -0
- nxgate_sdk_python-1.0.0.dist-info/licenses/LICENSE +21 -0
- nxgate_sdk_python-1.0.0.dist-info/top_level.txt +1 -0
nxgate/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""NXGATE PIX SDK for Python.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from nxgate import NXGate, NXGateWebhook
|
|
6
|
+
|
|
7
|
+
nx = NXGate(client_id="...", client_secret="...")
|
|
8
|
+
charge = nx.pix_generate(
|
|
9
|
+
valor=100.00,
|
|
10
|
+
nome_pagador="Joao da Silva",
|
|
11
|
+
documento_pagador="12345678901",
|
|
12
|
+
)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .client import NXGate
|
|
16
|
+
from .errors import (
|
|
17
|
+
NXGateAuthError,
|
|
18
|
+
NXGateError,
|
|
19
|
+
NXGateRetryError,
|
|
20
|
+
NXGateTimeoutError,
|
|
21
|
+
)
|
|
22
|
+
from .types import (
|
|
23
|
+
BalanceResponse,
|
|
24
|
+
CashInEvent,
|
|
25
|
+
CashInEventData,
|
|
26
|
+
CashOutEvent,
|
|
27
|
+
PixGenerateResponse,
|
|
28
|
+
PixWithdrawResponse,
|
|
29
|
+
SplitUser,
|
|
30
|
+
TokenResponse,
|
|
31
|
+
TransactionResponse,
|
|
32
|
+
WebhookEvent,
|
|
33
|
+
)
|
|
34
|
+
from .webhook import NXGateWebhook
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Client
|
|
38
|
+
"NXGate",
|
|
39
|
+
# Webhook
|
|
40
|
+
"NXGateWebhook",
|
|
41
|
+
# Errors
|
|
42
|
+
"NXGateError",
|
|
43
|
+
"NXGateAuthError",
|
|
44
|
+
"NXGateRetryError",
|
|
45
|
+
"NXGateTimeoutError",
|
|
46
|
+
# Types
|
|
47
|
+
"BalanceResponse",
|
|
48
|
+
"CashInEvent",
|
|
49
|
+
"CashInEventData",
|
|
50
|
+
"CashOutEvent",
|
|
51
|
+
"PixGenerateResponse",
|
|
52
|
+
"PixWithdrawResponse",
|
|
53
|
+
"SplitUser",
|
|
54
|
+
"TokenResponse",
|
|
55
|
+
"TransactionResponse",
|
|
56
|
+
"WebhookEvent",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
__version__ = "1.0.0"
|
nxgate/auth.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Token management for the NXGATE SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from .errors import NXGateAuthError
|
|
12
|
+
from .types import TokenResponse
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
_TOKEN_MARGIN_SECONDS = 60 # refresh token 60 s before it expires
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenManager:
|
|
21
|
+
"""Fetches and caches OAuth2 Bearer tokens.
|
|
22
|
+
|
|
23
|
+
The token is automatically refreshed when it is about to expire (with a
|
|
24
|
+
60-second safety margin).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: str,
|
|
30
|
+
client_id: str,
|
|
31
|
+
client_secret: str,
|
|
32
|
+
*,
|
|
33
|
+
timeout: int = 30,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._base_url = base_url.rstrip("/")
|
|
36
|
+
self._client_id = client_id
|
|
37
|
+
self._client_secret = client_secret
|
|
38
|
+
self._timeout = timeout
|
|
39
|
+
|
|
40
|
+
self._token: str | None = None
|
|
41
|
+
self._expires_at: float = 0.0
|
|
42
|
+
|
|
43
|
+
# ── public API ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def get_token(self) -> str:
|
|
46
|
+
"""Return a valid access token, refreshing when needed."""
|
|
47
|
+
if self._token is None or time.time() >= self._expires_at:
|
|
48
|
+
self._refresh()
|
|
49
|
+
assert self._token is not None
|
|
50
|
+
return self._token
|
|
51
|
+
|
|
52
|
+
def invalidate(self) -> None:
|
|
53
|
+
"""Force the next call to ``get_token`` to fetch a new token."""
|
|
54
|
+
self._token = None
|
|
55
|
+
self._expires_at = 0.0
|
|
56
|
+
|
|
57
|
+
# ── internals ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def _refresh(self) -> None:
|
|
60
|
+
url = f"{self._base_url}/oauth2/token"
|
|
61
|
+
payload = json.dumps(
|
|
62
|
+
{
|
|
63
|
+
"grant_type": "client_credentials",
|
|
64
|
+
"client_id": self._client_id,
|
|
65
|
+
"client_secret": self._client_secret,
|
|
66
|
+
}
|
|
67
|
+
).encode("utf-8")
|
|
68
|
+
|
|
69
|
+
req = urllib.request.Request(
|
|
70
|
+
url,
|
|
71
|
+
data=payload,
|
|
72
|
+
headers={"Content-Type": "application/json"},
|
|
73
|
+
method="POST",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
78
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
79
|
+
except urllib.error.HTTPError as exc:
|
|
80
|
+
try:
|
|
81
|
+
detail = json.loads(exc.read().decode("utf-8"))
|
|
82
|
+
except Exception:
|
|
83
|
+
detail = {}
|
|
84
|
+
err = detail.get("error", {})
|
|
85
|
+
if isinstance(err, str):
|
|
86
|
+
msg = err
|
|
87
|
+
else:
|
|
88
|
+
msg = err.get("description", str(exc))
|
|
89
|
+
raise NXGateAuthError(
|
|
90
|
+
f"Failed to obtain token: {msg}",
|
|
91
|
+
status_code=exc.code,
|
|
92
|
+
) from exc
|
|
93
|
+
except urllib.error.URLError as exc:
|
|
94
|
+
raise NXGateAuthError(
|
|
95
|
+
f"Failed to connect to {url}: {exc.reason}"
|
|
96
|
+
) from exc
|
|
97
|
+
|
|
98
|
+
token_resp = TokenResponse(
|
|
99
|
+
access_token=body["access_token"],
|
|
100
|
+
token_type=body.get("token_type", "Bearer"),
|
|
101
|
+
expires_in=int(body.get("expires_in", 3600)),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self._token = token_resp.access_token
|
|
105
|
+
self._expires_at = time.time() + token_resp.expires_in - _TOKEN_MARGIN_SECONDS
|
nxgate/client.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Main client for the NXGATE PIX SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .auth import TokenManager
|
|
13
|
+
from .errors import NXGateError, NXGateRetryError, NXGateTimeoutError
|
|
14
|
+
from .hmac_signer import HmacSigner
|
|
15
|
+
from .types import (
|
|
16
|
+
BalanceResponse,
|
|
17
|
+
PixGenerateRequest,
|
|
18
|
+
PixGenerateResponse,
|
|
19
|
+
PixWithdrawRequest,
|
|
20
|
+
PixWithdrawResponse,
|
|
21
|
+
SplitUser,
|
|
22
|
+
TransactionResponse,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_DEFAULT_BASE_URL = "https://api.nxgate.com.br"
|
|
26
|
+
_DEFAULT_TIMEOUT = 30
|
|
27
|
+
_MAX_RETRIES = 2
|
|
28
|
+
_RETRY_BASE_DELAY = 1.0 # seconds
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NXGate:
|
|
32
|
+
"""Synchronous client for the NXGATE PIX API.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
client_id: Your NXGATE ``client_id``.
|
|
36
|
+
client_secret: Your NXGATE ``client_secret``.
|
|
37
|
+
hmac_secret: Optional HMAC secret. When provided every request is
|
|
38
|
+
automatically signed with HMAC-SHA256 headers.
|
|
39
|
+
base_url: API base URL (defaults to ``https://api.nxgate.com.br``).
|
|
40
|
+
timeout: HTTP request timeout in seconds (default ``30``).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
client_id: str,
|
|
46
|
+
client_secret: str,
|
|
47
|
+
hmac_secret: str | None = None,
|
|
48
|
+
*,
|
|
49
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
50
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._base_url = base_url.rstrip("/")
|
|
53
|
+
self._timeout = timeout
|
|
54
|
+
self._client_id = client_id
|
|
55
|
+
|
|
56
|
+
self._token_manager = TokenManager(
|
|
57
|
+
base_url=self._base_url,
|
|
58
|
+
client_id=client_id,
|
|
59
|
+
client_secret=client_secret,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self._signer: HmacSigner | None = None
|
|
64
|
+
if hmac_secret:
|
|
65
|
+
self._signer = HmacSigner(client_id, hmac_secret)
|
|
66
|
+
|
|
67
|
+
# ── Public API ────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def pix_generate(
|
|
70
|
+
self,
|
|
71
|
+
valor: float,
|
|
72
|
+
nome_pagador: str,
|
|
73
|
+
documento_pagador: str,
|
|
74
|
+
*,
|
|
75
|
+
forcar_pagador: bool | None = None,
|
|
76
|
+
email_pagador: str | None = None,
|
|
77
|
+
celular: str | None = None,
|
|
78
|
+
descricao: str | None = None,
|
|
79
|
+
webhook: str | None = None,
|
|
80
|
+
magic_id: str | None = None,
|
|
81
|
+
api_key: str | None = None,
|
|
82
|
+
split_users: list[dict[str, Any]] | None = None,
|
|
83
|
+
) -> PixGenerateResponse:
|
|
84
|
+
"""Generate a PIX charge (cash-in) and return a QR Code.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
:class:`PixGenerateResponse` with payment code and transaction id.
|
|
88
|
+
"""
|
|
89
|
+
parsed_splits: list[SplitUser] | None = None
|
|
90
|
+
if split_users is not None:
|
|
91
|
+
parsed_splits = [
|
|
92
|
+
SplitUser(username=s["username"], percentage=s["percentage"])
|
|
93
|
+
for s in split_users
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
req = PixGenerateRequest(
|
|
97
|
+
valor=valor,
|
|
98
|
+
nome_pagador=nome_pagador,
|
|
99
|
+
documento_pagador=documento_pagador,
|
|
100
|
+
forcar_pagador=forcar_pagador,
|
|
101
|
+
email_pagador=email_pagador,
|
|
102
|
+
celular=celular,
|
|
103
|
+
descricao=descricao,
|
|
104
|
+
webhook=webhook,
|
|
105
|
+
magic_id=magic_id,
|
|
106
|
+
api_key=api_key,
|
|
107
|
+
split_users=parsed_splits,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
data = self._request("POST", "/pix/gerar", body=req.to_dict())
|
|
111
|
+
return PixGenerateResponse.from_dict(data)
|
|
112
|
+
|
|
113
|
+
def pix_withdraw(
|
|
114
|
+
self,
|
|
115
|
+
valor: float,
|
|
116
|
+
chave_pix: str,
|
|
117
|
+
tipo_chave: str,
|
|
118
|
+
*,
|
|
119
|
+
documento: str | None = None,
|
|
120
|
+
webhook: str | None = None,
|
|
121
|
+
magic_id: str | None = None,
|
|
122
|
+
api_key: str | None = None,
|
|
123
|
+
) -> PixWithdrawResponse:
|
|
124
|
+
"""Execute a PIX withdrawal (cash-out).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
:class:`PixWithdrawResponse` with status and internal reference.
|
|
128
|
+
"""
|
|
129
|
+
req = PixWithdrawRequest(
|
|
130
|
+
valor=valor,
|
|
131
|
+
chave_pix=chave_pix,
|
|
132
|
+
tipo_chave=tipo_chave,
|
|
133
|
+
documento=documento,
|
|
134
|
+
webhook=webhook,
|
|
135
|
+
magic_id=magic_id,
|
|
136
|
+
api_key=api_key,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
data = self._request("POST", "/pix/sacar", body=req.to_dict())
|
|
140
|
+
return PixWithdrawResponse.from_dict(data)
|
|
141
|
+
|
|
142
|
+
def get_balance(self) -> BalanceResponse:
|
|
143
|
+
"""Check current account balance.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
:class:`BalanceResponse` with balance, blocked and available.
|
|
147
|
+
"""
|
|
148
|
+
data = self._request("GET", "/v1/balance")
|
|
149
|
+
return BalanceResponse.from_dict(data)
|
|
150
|
+
|
|
151
|
+
def get_transaction(
|
|
152
|
+
self,
|
|
153
|
+
type: str,
|
|
154
|
+
txid: str,
|
|
155
|
+
) -> TransactionResponse:
|
|
156
|
+
"""Retrieve a specific transaction.
|
|
157
|
+
|
|
158
|
+
Parameters:
|
|
159
|
+
type: ``"cash-in"`` or ``"cash-out"``.
|
|
160
|
+
txid: The transaction identifier.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
:class:`TransactionResponse` with transaction details.
|
|
164
|
+
"""
|
|
165
|
+
params = urllib.parse.urlencode({"type": type, "txid": txid})
|
|
166
|
+
data = self._request("GET", f"/v1/transactions?{params}")
|
|
167
|
+
return TransactionResponse.from_dict(data)
|
|
168
|
+
|
|
169
|
+
# ── Internals ─────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def _request(
|
|
172
|
+
self,
|
|
173
|
+
method: str,
|
|
174
|
+
path: str,
|
|
175
|
+
*,
|
|
176
|
+
body: dict | None = None,
|
|
177
|
+
) -> dict:
|
|
178
|
+
"""Execute an authenticated HTTP request with retry on 503."""
|
|
179
|
+
token = self._token_manager.get_token()
|
|
180
|
+
|
|
181
|
+
body_bytes: bytes | None = None
|
|
182
|
+
body_str = ""
|
|
183
|
+
if body is not None:
|
|
184
|
+
body_str = json.dumps(body)
|
|
185
|
+
body_bytes = body_str.encode("utf-8")
|
|
186
|
+
|
|
187
|
+
# Resolve full URL - path may already contain query params
|
|
188
|
+
url = f"{self._base_url}{path}"
|
|
189
|
+
|
|
190
|
+
last_exc: Exception | None = None
|
|
191
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
192
|
+
headers: dict[str, str] = {
|
|
193
|
+
"Authorization": f"Bearer {token}",
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"Accept": "application/json",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if self._signer is not None:
|
|
199
|
+
# Sign using only the path portion (before query string)
|
|
200
|
+
sign_path = path.split("?")[0]
|
|
201
|
+
hmac_headers = self._signer.sign(method, sign_path, body_str)
|
|
202
|
+
headers.update(hmac_headers)
|
|
203
|
+
|
|
204
|
+
req = urllib.request.Request(
|
|
205
|
+
url,
|
|
206
|
+
data=body_bytes,
|
|
207
|
+
headers=headers,
|
|
208
|
+
method=method,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
213
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
214
|
+
except urllib.error.HTTPError as exc:
|
|
215
|
+
if exc.code == 503 and attempt < _MAX_RETRIES:
|
|
216
|
+
last_exc = exc
|
|
217
|
+
time.sleep(_RETRY_BASE_DELAY * (2 ** attempt))
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Try to parse structured error
|
|
221
|
+
try:
|
|
222
|
+
detail = json.loads(exc.read().decode("utf-8"))
|
|
223
|
+
except Exception:
|
|
224
|
+
detail = {}
|
|
225
|
+
|
|
226
|
+
err = detail.get("error", {})
|
|
227
|
+
if isinstance(err, str):
|
|
228
|
+
raise NXGateError(
|
|
229
|
+
err,
|
|
230
|
+
status_code=exc.code,
|
|
231
|
+
) from exc
|
|
232
|
+
raise NXGateError(
|
|
233
|
+
err.get("description", str(exc)),
|
|
234
|
+
title=err.get("title"),
|
|
235
|
+
code=err.get("code"),
|
|
236
|
+
description=err.get("description"),
|
|
237
|
+
status_code=exc.code,
|
|
238
|
+
) from exc
|
|
239
|
+
except urllib.error.URLError as exc:
|
|
240
|
+
if "timed out" in str(exc.reason):
|
|
241
|
+
raise NXGateTimeoutError(
|
|
242
|
+
f"Request timed out after {self._timeout}s"
|
|
243
|
+
) from exc
|
|
244
|
+
raise NXGateError(
|
|
245
|
+
f"Connection error: {exc.reason}"
|
|
246
|
+
) from exc
|
|
247
|
+
|
|
248
|
+
raise NXGateRetryError(
|
|
249
|
+
f"All {_MAX_RETRIES + 1} attempts failed for {method} {path}",
|
|
250
|
+
status_code=503,
|
|
251
|
+
)
|
nxgate/errors.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Exceptions for the NXGATE SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NXGateError(Exception):
|
|
7
|
+
"""Base exception for all NXGATE SDK errors.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
title: Short error title (e.g. ``"Unauthorized"``).
|
|
11
|
+
code: Machine-readable error code returned by the API.
|
|
12
|
+
description: Human-readable explanation of the error.
|
|
13
|
+
status_code: HTTP status code, when available.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
*,
|
|
20
|
+
title: str | None = None,
|
|
21
|
+
code: str | None = None,
|
|
22
|
+
description: str | None = None,
|
|
23
|
+
status_code: int | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.title = title
|
|
27
|
+
self.code = code
|
|
28
|
+
self.description = description
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
parts = [f"NXGateError({str(self)!r}"]
|
|
33
|
+
if self.status_code is not None:
|
|
34
|
+
parts.append(f", status_code={self.status_code}")
|
|
35
|
+
if self.code is not None:
|
|
36
|
+
parts.append(f", code={self.code!r}")
|
|
37
|
+
parts.append(")")
|
|
38
|
+
return "".join(parts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NXGateAuthError(NXGateError):
|
|
42
|
+
"""Raised when authentication / token retrieval fails."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NXGateTimeoutError(NXGateError):
|
|
46
|
+
"""Raised when a request exceeds the configured timeout."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NXGateRetryError(NXGateError):
|
|
50
|
+
"""Raised when all retry attempts have been exhausted."""
|
nxgate/hmac_signer.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HMAC request signing for the NXGATE SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HmacSigner:
|
|
13
|
+
"""Generates HMAC-SHA256 headers for API request authentication.
|
|
14
|
+
|
|
15
|
+
The signature is computed over the canonical string::
|
|
16
|
+
|
|
17
|
+
METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nBODY
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client_id: str, hmac_secret: str) -> None:
|
|
21
|
+
self._client_id = client_id
|
|
22
|
+
self._secret = hmac_secret.encode("utf-8")
|
|
23
|
+
|
|
24
|
+
def sign(
|
|
25
|
+
self,
|
|
26
|
+
method: str,
|
|
27
|
+
path: str,
|
|
28
|
+
body: str = "",
|
|
29
|
+
) -> dict[str, str]:
|
|
30
|
+
"""Return the HMAC headers for a single request.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
method: HTTP method (e.g. ``"POST"``).
|
|
34
|
+
path: Request path (e.g. ``"/pix/gerar"``).
|
|
35
|
+
body: Serialised request body (empty string for GET).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A dict with the four required headers.
|
|
39
|
+
"""
|
|
40
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
41
|
+
nonce = uuid.uuid4().hex
|
|
42
|
+
|
|
43
|
+
canonical = f"{method}\n{path}\n{timestamp}\n{nonce}\n{body}"
|
|
44
|
+
signature = hmac.new(
|
|
45
|
+
self._secret,
|
|
46
|
+
canonical.encode("utf-8"),
|
|
47
|
+
hashlib.sha256,
|
|
48
|
+
).digest()
|
|
49
|
+
signature_b64 = base64.b64encode(signature).decode("ascii")
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"X-Client-ID": self._client_id,
|
|
53
|
+
"X-HMAC-Signature": signature_b64,
|
|
54
|
+
"X-HMAC-Timestamp": timestamp,
|
|
55
|
+
"X-HMAC-Nonce": nonce,
|
|
56
|
+
}
|
nxgate/types.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Data classes for the NXGATE SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ── Auth ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class TokenResponse:
|
|
12
|
+
"""Represents the response from ``POST /oauth2/token``."""
|
|
13
|
+
|
|
14
|
+
access_token: str
|
|
15
|
+
token_type: str
|
|
16
|
+
expires_in: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── PIX Cash-in ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class SplitUser:
|
|
23
|
+
"""Describes a split payment participant."""
|
|
24
|
+
|
|
25
|
+
username: str
|
|
26
|
+
percentage: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class PixGenerateRequest:
|
|
31
|
+
"""Payload for ``POST /pix/gerar``."""
|
|
32
|
+
|
|
33
|
+
valor: float
|
|
34
|
+
nome_pagador: str
|
|
35
|
+
documento_pagador: str
|
|
36
|
+
forcar_pagador: bool | None = None
|
|
37
|
+
email_pagador: str | None = None
|
|
38
|
+
celular: str | None = None
|
|
39
|
+
descricao: str | None = None
|
|
40
|
+
webhook: str | None = None
|
|
41
|
+
magic_id: str | None = None
|
|
42
|
+
api_key: str | None = None
|
|
43
|
+
split_users: list[SplitUser] | None = None
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
data: dict = {
|
|
47
|
+
"valor": self.valor,
|
|
48
|
+
"nome_pagador": self.nome_pagador,
|
|
49
|
+
"documento_pagador": self.documento_pagador,
|
|
50
|
+
}
|
|
51
|
+
if self.forcar_pagador is not None:
|
|
52
|
+
data["forcar_pagador"] = self.forcar_pagador
|
|
53
|
+
if self.email_pagador is not None:
|
|
54
|
+
data["email_pagador"] = self.email_pagador
|
|
55
|
+
if self.celular is not None:
|
|
56
|
+
data["celular"] = self.celular
|
|
57
|
+
if self.descricao is not None:
|
|
58
|
+
data["descricao"] = self.descricao
|
|
59
|
+
if self.webhook is not None:
|
|
60
|
+
data["webhook"] = self.webhook
|
|
61
|
+
if self.magic_id is not None:
|
|
62
|
+
data["magic_id"] = self.magic_id
|
|
63
|
+
if self.api_key is not None:
|
|
64
|
+
data["api_key"] = self.api_key
|
|
65
|
+
if self.split_users is not None:
|
|
66
|
+
data["split_users"] = [
|
|
67
|
+
{"username": s.username, "percentage": s.percentage}
|
|
68
|
+
for s in self.split_users
|
|
69
|
+
]
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class PixGenerateResponse:
|
|
75
|
+
"""Response from ``POST /pix/gerar``."""
|
|
76
|
+
|
|
77
|
+
status: str
|
|
78
|
+
message: str
|
|
79
|
+
paymentCode: str
|
|
80
|
+
idTransaction: str
|
|
81
|
+
paymentCodeBase64: str
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: dict) -> PixGenerateResponse:
|
|
85
|
+
return cls(
|
|
86
|
+
status=data.get("status", ""),
|
|
87
|
+
message=data.get("message", ""),
|
|
88
|
+
paymentCode=data.get("paymentCode", ""),
|
|
89
|
+
idTransaction=data.get("idTransaction", ""),
|
|
90
|
+
paymentCodeBase64=data.get("paymentCodeBase64", ""),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── PIX Cash-out ──────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
TIPO_CHAVE_VALUES = {"CPF", "CNPJ", "PHONE", "EMAIL", "RANDOM"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class PixWithdrawRequest:
|
|
101
|
+
"""Payload for ``POST /pix/sacar``."""
|
|
102
|
+
|
|
103
|
+
valor: float
|
|
104
|
+
chave_pix: str
|
|
105
|
+
tipo_chave: str # CPF | CNPJ | PHONE | EMAIL | RANDOM
|
|
106
|
+
documento: str | None = None
|
|
107
|
+
webhook: str | None = None
|
|
108
|
+
magic_id: str | None = None
|
|
109
|
+
api_key: str | None = None
|
|
110
|
+
|
|
111
|
+
def __post_init__(self) -> None:
|
|
112
|
+
if self.tipo_chave not in TIPO_CHAVE_VALUES:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"tipo_chave must be one of {TIPO_CHAVE_VALUES}, "
|
|
115
|
+
f"got {self.tipo_chave!r}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict:
|
|
119
|
+
data: dict = {
|
|
120
|
+
"valor": self.valor,
|
|
121
|
+
"chave_pix": self.chave_pix,
|
|
122
|
+
"tipo_chave": self.tipo_chave,
|
|
123
|
+
}
|
|
124
|
+
if self.documento is not None:
|
|
125
|
+
data["documento"] = self.documento
|
|
126
|
+
if self.webhook is not None:
|
|
127
|
+
data["webhook"] = self.webhook
|
|
128
|
+
if self.magic_id is not None:
|
|
129
|
+
data["magic_id"] = self.magic_id
|
|
130
|
+
if self.api_key is not None:
|
|
131
|
+
data["api_key"] = self.api_key
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class PixWithdrawResponse:
|
|
137
|
+
"""Response from ``POST /pix/sacar``."""
|
|
138
|
+
|
|
139
|
+
status: str
|
|
140
|
+
message: str
|
|
141
|
+
internalreference: str
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_dict(cls, data: dict) -> PixWithdrawResponse:
|
|
145
|
+
return cls(
|
|
146
|
+
status=data.get("status", ""),
|
|
147
|
+
message=data.get("message", ""),
|
|
148
|
+
internalreference=data.get("internalreference", ""),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── Balance ───────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class BalanceResponse:
|
|
156
|
+
"""Response from ``GET /v1/balance``."""
|
|
157
|
+
|
|
158
|
+
balance: float
|
|
159
|
+
blocked: float
|
|
160
|
+
available: float
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_dict(cls, data: dict) -> BalanceResponse:
|
|
164
|
+
return cls(
|
|
165
|
+
balance=float(data.get("balance", 0)),
|
|
166
|
+
blocked=float(data.get("blocked", 0)),
|
|
167
|
+
available=float(data.get("available", 0)),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Transactions ──────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class TransactionResponse:
|
|
175
|
+
"""Response from ``GET /v1/transactions``."""
|
|
176
|
+
|
|
177
|
+
idTransaction: str
|
|
178
|
+
status: str
|
|
179
|
+
amount: float
|
|
180
|
+
paidAt: str | None
|
|
181
|
+
endToEnd: str | None
|
|
182
|
+
raw: dict = field(default_factory=dict)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_dict(cls, data: dict) -> TransactionResponse:
|
|
186
|
+
return cls(
|
|
187
|
+
idTransaction=data.get("idTransaction", ""),
|
|
188
|
+
status=data.get("status", ""),
|
|
189
|
+
amount=float(data.get("amount", 0)),
|
|
190
|
+
paidAt=data.get("paidAt"),
|
|
191
|
+
endToEnd=data.get("endToEnd"),
|
|
192
|
+
raw=data,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── Webhook Events ───────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
@dataclass(frozen=True)
|
|
199
|
+
class CashInEventData:
|
|
200
|
+
"""Payload inside a cash-in webhook event."""
|
|
201
|
+
|
|
202
|
+
amount: float
|
|
203
|
+
status: str
|
|
204
|
+
worked: bool
|
|
205
|
+
tag: str
|
|
206
|
+
tx_id: str
|
|
207
|
+
end_to_end: str
|
|
208
|
+
payment_date: str
|
|
209
|
+
debtor_name: str
|
|
210
|
+
debtor_document: str
|
|
211
|
+
type_document: str
|
|
212
|
+
magic_id: str
|
|
213
|
+
fee: float
|
|
214
|
+
raw: dict = field(default_factory=dict)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_dict(cls, data: dict) -> CashInEventData:
|
|
218
|
+
return cls(
|
|
219
|
+
amount=float(data.get("amount", 0)),
|
|
220
|
+
status=data.get("status", ""),
|
|
221
|
+
worked=bool(data.get("worked", False)),
|
|
222
|
+
tag=data.get("tag", ""),
|
|
223
|
+
tx_id=data.get("tx_id", ""),
|
|
224
|
+
end_to_end=data.get("end_to_end", ""),
|
|
225
|
+
payment_date=data.get("payment_date", ""),
|
|
226
|
+
debtor_name=data.get("debtor_name", ""),
|
|
227
|
+
debtor_document=data.get("debtor_document", ""),
|
|
228
|
+
type_document=data.get("type_document", ""),
|
|
229
|
+
magic_id=data.get("magic_id", ""),
|
|
230
|
+
fee=float(data.get("fee", 0)),
|
|
231
|
+
raw=data,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class CashInEvent:
|
|
237
|
+
"""Cash-in webhook event."""
|
|
238
|
+
|
|
239
|
+
type: str # QR_CODE_COPY_AND_PASTE_PAID | QR_CODE_COPY_AND_PASTE_REFUNDED
|
|
240
|
+
data: CashInEventData
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def from_dict(cls, payload: dict) -> CashInEvent:
|
|
244
|
+
return cls(
|
|
245
|
+
type=payload["type"],
|
|
246
|
+
data=CashInEventData.from_dict(payload.get("data", {})),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass(frozen=True)
|
|
251
|
+
class CashOutEvent:
|
|
252
|
+
"""Cash-out webhook event."""
|
|
253
|
+
|
|
254
|
+
type: str # PIX_CASHOUT_SUCCESS | PIX_CASHOUT_ERROR | PIX_CASHOUT_REFUNDED
|
|
255
|
+
worked: bool
|
|
256
|
+
status: str
|
|
257
|
+
idTransaction: str
|
|
258
|
+
amount: float
|
|
259
|
+
key: str
|
|
260
|
+
end_to_end: str
|
|
261
|
+
payment_date: str
|
|
262
|
+
magic_id: str
|
|
263
|
+
fee: float
|
|
264
|
+
error: str | None
|
|
265
|
+
raw: dict = field(default_factory=dict)
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def from_dict(cls, payload: dict) -> CashOutEvent:
|
|
269
|
+
return cls(
|
|
270
|
+
type=payload["type"],
|
|
271
|
+
worked=bool(payload.get("worked", False)),
|
|
272
|
+
status=payload.get("status", ""),
|
|
273
|
+
idTransaction=payload.get("idTransaction", ""),
|
|
274
|
+
amount=float(payload.get("amount", 0)),
|
|
275
|
+
key=payload.get("key", ""),
|
|
276
|
+
end_to_end=payload.get("end_to_end", ""),
|
|
277
|
+
payment_date=payload.get("payment_date", ""),
|
|
278
|
+
magic_id=payload.get("magic_id", ""),
|
|
279
|
+
fee=float(payload.get("fee", 0)),
|
|
280
|
+
error=payload.get("error"),
|
|
281
|
+
raw=payload,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Union-like alias for webhook events
|
|
286
|
+
WebhookEvent = CashInEvent | CashOutEvent
|
|
287
|
+
|
|
288
|
+
CASHIN_TYPES = {
|
|
289
|
+
"QR_CODE_COPY_AND_PASTE_PAID",
|
|
290
|
+
"QR_CODE_COPY_AND_PASTE_REFUNDED",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
CASHOUT_TYPES = {
|
|
294
|
+
"PIX_CASHOUT_SUCCESS",
|
|
295
|
+
"PIX_CASHOUT_ERROR",
|
|
296
|
+
"PIX_CASHOUT_REFUNDED",
|
|
297
|
+
}
|
nxgate/webhook.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Webhook event parser for the NXGATE SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .errors import NXGateError
|
|
6
|
+
from .types import (
|
|
7
|
+
CASHIN_TYPES,
|
|
8
|
+
CASHOUT_TYPES,
|
|
9
|
+
CashInEvent,
|
|
10
|
+
CashOutEvent,
|
|
11
|
+
WebhookEvent,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NXGateWebhook:
|
|
16
|
+
"""Stateless helper to parse incoming webhook payloads."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def parse(payload: dict) -> WebhookEvent:
|
|
20
|
+
"""Parse a raw webhook JSON payload into a typed event.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
payload: The decoded JSON body sent by NXGATE to your webhook
|
|
24
|
+
endpoint.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A :class:`CashInEvent` or :class:`CashOutEvent`.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
NXGateError: If the ``type`` field is missing or unrecognised.
|
|
31
|
+
"""
|
|
32
|
+
event_type = payload.get("type")
|
|
33
|
+
if not event_type:
|
|
34
|
+
raise NXGateError(
|
|
35
|
+
"Webhook payload is missing the 'type' field.",
|
|
36
|
+
code="INVALID_WEBHOOK",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if event_type in CASHIN_TYPES:
|
|
40
|
+
return CashInEvent.from_dict(payload)
|
|
41
|
+
|
|
42
|
+
if event_type in CASHOUT_TYPES:
|
|
43
|
+
return CashOutEvent.from_dict(payload)
|
|
44
|
+
|
|
45
|
+
raise NXGateError(
|
|
46
|
+
f"Unknown webhook event type: {event_type!r}",
|
|
47
|
+
code="UNKNOWN_WEBHOOK_TYPE",
|
|
48
|
+
)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nxgate-sdk-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SDK oficial da NXGATE para integração com a API PIX
|
|
5
|
+
Author-email: NXGATE <suporte@nxgate.com.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nxgate/nxgate-sdk-python
|
|
8
|
+
Project-URL: Documentation, https://github.com/nxgate/nxgate-sdk-python#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/nxgate/nxgate-sdk-python
|
|
10
|
+
Keywords: nxgate,pix,pagamentos,qrcode,sdk
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# NXGATE PIX SDK para Python
|
|
28
|
+
|
|
29
|
+
SDK oficial da NXGATE para integração com a API PIX. Permite gerar cobranças (cash-in), realizar saques (cash-out), consultar saldo e transações, tudo com tipagem completa e zero dependências externas.
|
|
30
|
+
|
|
31
|
+
## Requisitos
|
|
32
|
+
|
|
33
|
+
- Python 3.10 ou superior
|
|
34
|
+
- Sem dependências externas (usa apenas a biblioteca padrão)
|
|
35
|
+
|
|
36
|
+
## Instalação
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nxgate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Ou instale diretamente do repositório:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install git+https://github.com/nxgate/sdk-python.git
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Início Rápido
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from nxgate import NXGate, NXGateWebhook
|
|
52
|
+
|
|
53
|
+
nx = NXGate(
|
|
54
|
+
client_id="nxgate_xxx",
|
|
55
|
+
client_secret="secret",
|
|
56
|
+
hmac_secret="opcional", # opcional - quando informado, todas as requisições são assinadas com HMAC-SHA256
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Funcionalidades
|
|
61
|
+
|
|
62
|
+
### Gerar cobrança PIX (Cash-in)
|
|
63
|
+
|
|
64
|
+
Gera uma cobrança PIX e retorna o QR Code para pagamento.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
charge = nx.pix_generate(
|
|
68
|
+
valor=100.00,
|
|
69
|
+
nome_pagador="João da Silva",
|
|
70
|
+
documento_pagador="12345678901",
|
|
71
|
+
webhook="https://meusite.com/webhook",
|
|
72
|
+
descricao="Pedido #1234",
|
|
73
|
+
email_pagador="joao@email.com",
|
|
74
|
+
celular="11999999999",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(charge.status) # "ACTIVE"
|
|
78
|
+
print(charge.paymentCode) # código copia-e-cola
|
|
79
|
+
print(charge.idTransaction) # ID da transação
|
|
80
|
+
print(charge.paymentCodeBase64) # QR Code em base64
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Cobrança com split de pagamento
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
charge = nx.pix_generate(
|
|
87
|
+
valor=200.00,
|
|
88
|
+
nome_pagador="Maria Souza",
|
|
89
|
+
documento_pagador="98765432100",
|
|
90
|
+
split_users=[
|
|
91
|
+
{"username": "loja_a", "percentage": 70.0},
|
|
92
|
+
{"username": "loja_b", "percentage": 30.0},
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Saque PIX (Cash-out)
|
|
98
|
+
|
|
99
|
+
Realiza uma transferência PIX para uma chave destino.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
withdrawal = nx.pix_withdraw(
|
|
103
|
+
valor=50.00,
|
|
104
|
+
chave_pix="joao@email.com",
|
|
105
|
+
tipo_chave="EMAIL", # CPF | CNPJ | PHONE | EMAIL | RANDOM
|
|
106
|
+
webhook="https://meusite.com/webhook",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
print(withdrawal.status) # "PROCESSING"
|
|
110
|
+
print(withdrawal.internalreference) # referência interna
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Consultar saldo
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
balance = nx.get_balance()
|
|
117
|
+
|
|
118
|
+
print(balance.balance) # saldo total
|
|
119
|
+
print(balance.blocked) # saldo bloqueado
|
|
120
|
+
print(balance.available) # saldo disponível
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Consultar transação
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
tx = nx.get_transaction(type="cash-in", txid="px_xxx")
|
|
127
|
+
|
|
128
|
+
print(tx.idTransaction) # ID da transação
|
|
129
|
+
print(tx.status) # status atual
|
|
130
|
+
print(tx.amount) # valor
|
|
131
|
+
print(tx.paidAt) # data do pagamento
|
|
132
|
+
print(tx.endToEnd) # identificador end-to-end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Webhooks
|
|
136
|
+
|
|
137
|
+
O SDK fornece um parser para eventos recebidos via webhook.
|
|
138
|
+
|
|
139
|
+
### Recebendo eventos (exemplo com Flask)
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from flask import Flask, request, jsonify
|
|
143
|
+
from nxgate import NXGateWebhook, NXGateError
|
|
144
|
+
from nxgate.types import CashInEvent, CashOutEvent
|
|
145
|
+
|
|
146
|
+
app = Flask(__name__)
|
|
147
|
+
|
|
148
|
+
@app.route("/webhook", methods=["POST"])
|
|
149
|
+
def webhook():
|
|
150
|
+
try:
|
|
151
|
+
event = NXGateWebhook.parse(request.json)
|
|
152
|
+
except NXGateError as e:
|
|
153
|
+
return jsonify({"error": str(e)}), 400
|
|
154
|
+
|
|
155
|
+
if isinstance(event, CashInEvent):
|
|
156
|
+
print(f"Cash-in: {event.type}")
|
|
157
|
+
print(f" Valor: R$ {event.data.amount:.2f}")
|
|
158
|
+
print(f" Pagador: {event.data.debtor_name}")
|
|
159
|
+
print(f" TX ID: {event.data.tx_id}")
|
|
160
|
+
print(f" Status: {event.data.status}")
|
|
161
|
+
|
|
162
|
+
elif isinstance(event, CashOutEvent):
|
|
163
|
+
print(f"Cash-out: {event.type}")
|
|
164
|
+
print(f" Valor: R$ {event.amount:.2f}")
|
|
165
|
+
print(f" Chave: {event.key}")
|
|
166
|
+
if event.error:
|
|
167
|
+
print(f" Erro: {event.error}")
|
|
168
|
+
|
|
169
|
+
return jsonify({"ok": True})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Tipos de evento
|
|
173
|
+
|
|
174
|
+
**Cash-in:**
|
|
175
|
+
- `QR_CODE_COPY_AND_PASTE_PAID` - pagamento confirmado
|
|
176
|
+
- `QR_CODE_COPY_AND_PASTE_REFUNDED` - pagamento devolvido
|
|
177
|
+
|
|
178
|
+
**Cash-out:**
|
|
179
|
+
- `PIX_CASHOUT_SUCCESS` - saque realizado com sucesso
|
|
180
|
+
- `PIX_CASHOUT_ERROR` - erro no saque
|
|
181
|
+
- `PIX_CASHOUT_REFUNDED` - saque devolvido
|
|
182
|
+
|
|
183
|
+
## Assinatura HMAC
|
|
184
|
+
|
|
185
|
+
Quando o parâmetro `hmac_secret` é informado na inicialização do cliente, todas as requisições são automaticamente assinadas com HMAC-SHA256. Os seguintes headers são adicionados:
|
|
186
|
+
|
|
187
|
+
| Header | Descrição |
|
|
188
|
+
|--------|-----------|
|
|
189
|
+
| `X-Client-ID` | Seu `client_id` |
|
|
190
|
+
| `X-HMAC-Signature` | Assinatura HMAC-SHA256 em base64 |
|
|
191
|
+
| `X-HMAC-Timestamp` | Timestamp ISO 8601 |
|
|
192
|
+
| `X-HMAC-Nonce` | Valor único por requisição |
|
|
193
|
+
|
|
194
|
+
A assinatura é gerada sobre a string canônica:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Gerenciamento de Token
|
|
201
|
+
|
|
202
|
+
O SDK gerencia automaticamente o token OAuth2:
|
|
203
|
+
|
|
204
|
+
- O token é obtido na primeira requisição
|
|
205
|
+
- É mantido em cache enquanto válido
|
|
206
|
+
- É renovado automaticamente 60 segundos antes de expirar
|
|
207
|
+
- Em caso de falha na autenticação, `NXGateAuthError` é lançado
|
|
208
|
+
|
|
209
|
+
## Tratamento de Erros
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from nxgate import NXGate, NXGateError
|
|
213
|
+
from nxgate.errors import NXGateAuthError, NXGateTimeoutError, NXGateRetryError
|
|
214
|
+
|
|
215
|
+
nx = NXGate(client_id="xxx", client_secret="yyy")
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
charge = nx.pix_generate(
|
|
219
|
+
valor=100.00,
|
|
220
|
+
nome_pagador="Teste",
|
|
221
|
+
documento_pagador="00000000000",
|
|
222
|
+
)
|
|
223
|
+
except NXGateAuthError as e:
|
|
224
|
+
print(f"Erro de autenticação: {e}")
|
|
225
|
+
print(f"Status HTTP: {e.status_code}")
|
|
226
|
+
except NXGateTimeoutError as e:
|
|
227
|
+
print(f"Timeout: {e}")
|
|
228
|
+
except NXGateRetryError as e:
|
|
229
|
+
print(f"Todas as tentativas falharam: {e}")
|
|
230
|
+
except NXGateError as e:
|
|
231
|
+
print(f"Erro da API: {e}")
|
|
232
|
+
print(f"Título: {e.title}")
|
|
233
|
+
print(f"Código: {e.code}")
|
|
234
|
+
print(f"Descrição: {e.description}")
|
|
235
|
+
print(f"Status HTTP: {e.status_code}")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Hierarquia de exceções
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
Exception
|
|
242
|
+
└── NXGateError
|
|
243
|
+
├── NXGateAuthError # falha na autenticação
|
|
244
|
+
├── NXGateTimeoutError # timeout na requisição
|
|
245
|
+
└── NXGateRetryError # tentativas esgotadas (503)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Retry Automático
|
|
249
|
+
|
|
250
|
+
Requisições que retornam HTTP 503 são automaticamente retentadas com backoff exponencial:
|
|
251
|
+
|
|
252
|
+
- Máximo de 2 retentativas (3 tentativas no total)
|
|
253
|
+
- Delay entre tentativas: 1s, 2s
|
|
254
|
+
- Se todas as tentativas falharem, `NXGateRetryError` é lançado
|
|
255
|
+
|
|
256
|
+
## Referência da API
|
|
257
|
+
|
|
258
|
+
### `NXGate(client_id, client_secret, hmac_secret=None, *, base_url, timeout)`
|
|
259
|
+
|
|
260
|
+
| Parâmetro | Tipo | Obrigatório | Descrição |
|
|
261
|
+
|-----------|------|-------------|-----------|
|
|
262
|
+
| `client_id` | `str` | Sim | Seu client_id NXGATE |
|
|
263
|
+
| `client_secret` | `str` | Sim | Seu client_secret NXGATE |
|
|
264
|
+
| `hmac_secret` | `str \| None` | Não | Secret para assinatura HMAC |
|
|
265
|
+
| `base_url` | `str` | Não | URL base da API (padrão: `https://api.nxgate.com.br`) |
|
|
266
|
+
| `timeout` | `int` | Não | Timeout em segundos (padrão: `30`) |
|
|
267
|
+
|
|
268
|
+
### Métodos
|
|
269
|
+
|
|
270
|
+
| Método | Retorno | Descrição |
|
|
271
|
+
|--------|---------|-----------|
|
|
272
|
+
| `pix_generate(...)` | `PixGenerateResponse` | Gera cobrança PIX |
|
|
273
|
+
| `pix_withdraw(...)` | `PixWithdrawResponse` | Realiza saque PIX |
|
|
274
|
+
| `get_balance()` | `BalanceResponse` | Consulta saldo |
|
|
275
|
+
| `get_transaction(type, txid)` | `TransactionResponse` | Consulta transação |
|
|
276
|
+
|
|
277
|
+
## Desenvolvimento
|
|
278
|
+
|
|
279
|
+
### Executar testes
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
pip install pytest
|
|
283
|
+
pytest
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Estrutura do projeto
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
nxgate/
|
|
290
|
+
├── __init__.py # exports públicos
|
|
291
|
+
├── client.py # classe NXGate
|
|
292
|
+
├── auth.py # gerenciamento de token
|
|
293
|
+
├── hmac_signer.py # assinatura HMAC
|
|
294
|
+
├── webhook.py # parser de webhook
|
|
295
|
+
├── errors.py # exceções
|
|
296
|
+
└── types.py # dataclasses tipadas
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Licença
|
|
300
|
+
|
|
301
|
+
MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
nxgate/__init__.py,sha256=RXcUkCzrlJyS0iIh_YjbsVtyX7VI2VEB5FuJseUJjrY,1120
|
|
2
|
+
nxgate/auth.py,sha256=EsND1FEl1jemmWNuIZLL1yhmJAdEpKHV3TBcvgRQ7_o,3406
|
|
3
|
+
nxgate/client.py,sha256=Cl-pJibhAOi06TweebsvqZbKXM3caiDATPfiCzSD5tQ,8259
|
|
4
|
+
nxgate/errors.py,sha256=ae5m9N7meE2voTafcEqc6rhVb9sc-5yP14YvVPWGMFY,1439
|
|
5
|
+
nxgate/hmac_signer.py,sha256=kYcY977WnqC2gn1GEnbVXNQ0KswGSE_nA0ijjciJybQ,1557
|
|
6
|
+
nxgate/types.py,sha256=sMl1pZU5lnIOpmdSSvUjyFNZsnENnFcXUHEnGEuRrrY,9059
|
|
7
|
+
nxgate/webhook.py,sha256=hT_UtJ8WDToimqYiB9kPHuR2w93ltEfg-rf45cY2Y3s,1285
|
|
8
|
+
nxgate_sdk_python-1.0.0.dist-info/licenses/LICENSE,sha256=rzvC6KhelMbCpKgYRDIbrqN8m4J6XwiWHNih4NTFf7U,1063
|
|
9
|
+
nxgate_sdk_python-1.0.0.dist-info/METADATA,sha256=JbiQ-u2JqzsgrfkdIMAlhf-g_w968ecm9pO0_USFBSQ,8555
|
|
10
|
+
nxgate_sdk_python-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
nxgate_sdk_python-1.0.0.dist-info/top_level.txt,sha256=Kem3KARfoXYGgH_omjOBK8axGMfqIhj8DgIe5NGeGVQ,7
|
|
12
|
+
nxgate_sdk_python-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NXGATE
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nxgate
|