pyXRocketAPI 0.1.0__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyXRocketAPI
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python client for the xRocket Pay API
5
5
  Author: Badiboy
6
6
  License-Expression: MIT
@@ -17,7 +17,11 @@ License-File: LICENSE
17
17
  Requires-Dist: requests>=2.28
18
18
  Dynamic: license-file
19
19
 
20
- # pyXRocketAPI
20
+ [![PyPi Package Version](https://img.shields.io/pypi/v/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
21
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
22
+ [![PyPi downloads](https://img.shields.io/pypi/dm/pyXRocketAPI.svg)](https://pypi.org/project/pyXRocketAPI/)
23
+
24
+ # <p align="center">pyXRocketAPI</p>
21
25
 
22
26
  **pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
23
27
 
@@ -1,4 +1,8 @@
1
- # pyXRocketAPI
1
+ [![PyPi Package Version](https://img.shields.io/pypi/v/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
2
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
3
+ [![PyPi downloads](https://img.shields.io/pypi/dm/pyXRocketAPI.svg)](https://pypi.org/project/pyXRocketAPI/)
4
+
5
+ # <p align="center">pyXRocketAPI</p>
2
6
 
3
7
  **pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
4
8
 
@@ -1,3 +1,5 @@
1
1
  """Public API for pyXRocketAPI."""
2
2
 
3
3
  from .api import *
4
+ from .exceptions import *
5
+ from .webhooks import *
@@ -20,3 +20,19 @@ class xRocketAPIException(Exception):
20
20
  self.instance = instance
21
21
  self.kind = kind
22
22
  self.info = info
23
+
24
+
25
+ class xRocketWebhookException(ValueError):
26
+ """Base class for errors while validating or parsing an xRocket webhook."""
27
+
28
+
29
+ class xRocketWebhookParseException(xRocketWebhookException):
30
+ """Webhook body is not valid JSON or does not have the expected structure."""
31
+
32
+
33
+ class xRocketWebhookSignatureException(xRocketWebhookException):
34
+ """Webhook signature is absent or does not match the supplied secret."""
35
+
36
+
37
+ class xRocketWebhookUnsupportedVersionException(xRocketWebhookSignatureException):
38
+ """Webhook uses a signature scheme version unsupported by this library."""
@@ -68,8 +68,8 @@ class App(xRocketObject):
68
68
 
69
69
  API: https://docs.xrocket.exchange/api/pay/reference/http/app-controller-get-app
70
70
 
71
- :ivar id: No description is provided.
72
- :ivar name: Name of current app.
71
+ :param id: No description is provided.
72
+ :param name: Name of current app.
73
73
  """
74
74
  def __init__(self):
75
75
  self.id = None
@@ -87,10 +87,10 @@ class Balance(xRocketObject):
87
87
 
88
88
  API: https://docs.xrocket.exchange/api/pay/reference/http/app-controller-get-app-balances
89
89
 
90
- :ivar asset: Balance asset.
91
- :ivar balance: Asset balance.
92
- :ivar available: Available balance.
93
- :ivar holds: Holds balance.
90
+ :param asset: Balance asset.
91
+ :param balance: Asset balance.
92
+ :param available: Available balance.
93
+ :param holds: Holds balance.
94
94
  """
95
95
  def __init__(self):
96
96
  self.asset = None
@@ -110,9 +110,9 @@ class InvoiceLinks(xRocketObject):
110
110
 
111
111
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice
112
112
 
113
- :ivar telegramBotLink: Invoice telegram bot link.
114
- :ivar telegramMiniAppLink: Invoice telegram mini app link.
115
- :ivar webLink: Invoice web link.
113
+ :param telegramBotLink: Invoice telegram bot link.
114
+ :param telegramMiniAppLink: Invoice telegram mini app link.
115
+ :param webLink: Invoice web link.
116
116
  """
117
117
  def __init__(self):
118
118
  self.telegramBotLink = None
@@ -131,21 +131,21 @@ class Invoice(xRocketObject):
131
131
 
132
132
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice
133
133
 
134
- :ivar id: Invoice id.
135
- :ivar priceAmount: Invoice price amount.
136
- :ivar minPayment: Minimum payment amount (for open-amount invoices).
137
- :ivar priceCurrency: Invoice price currency (crypto or fiat).
138
- :ivar payCurrencies: Currencies which can be used to pay the invoice (crypto or fiat).
139
- :ivar clientInvoiceId: Your unique identifier for this invoice to link with your internal system (e.g., order ID).
140
- :ivar description: Description for invoice.
141
- :ivar expiresIn: Invoice expires in milliseconds (from creation time).
142
- :ivar createdAt: Invoice creation time.
143
- :ivar expiresAt: Invoice expiration time (null if no expiration).
144
- :ivar status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
145
- :ivar callback: No description is provided.
146
- :ivar url: No description is provided.
147
- :ivar customer: No description is provided.
148
- :ivar links: No description is provided.
134
+ :param id: Invoice id.
135
+ :param priceAmount: Invoice price amount.
136
+ :param minPayment: Minimum payment amount (for open-amount invoices).
137
+ :param priceCurrency: Invoice price currency (crypto or fiat).
138
+ :param payCurrencies: Currencies which can be used to pay the invoice (crypto or fiat).
139
+ :param clientInvoiceId: Your unique identifier for this invoice to link with your internal system (e.g., order ID).
140
+ :param description: Description for invoice.
141
+ :param expiresIn: Invoice expires in milliseconds (from creation time).
142
+ :param createdAt: Invoice creation time.
143
+ :param expiresAt: Invoice expiration time (null if no expiration).
144
+ :param status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
145
+ :param callback: No description is provided.
146
+ :param url: No description is provided.
147
+ :param customer: No description is provided.
148
+ :param links: No description is provided.
149
149
  """
150
150
  def __init__(self):
151
151
  self.id = None
@@ -179,9 +179,9 @@ class ChequeLinks(xRocketObject):
179
179
 
180
180
  API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheque
181
181
 
182
- :ivar telegramBotLink: Cheque activation telegram bot link.
183
- :ivar telegramMiniAppLink: Cheque activation telegram mini app link (soon).
184
- :ivar webLink: Cheque activation web link (soon).
182
+ :param telegramBotLink: Cheque activation telegram bot link.
183
+ :param telegramMiniAppLink: Cheque activation telegram mini app link (soon).
184
+ :param webLink: Cheque activation web link (soon).
185
185
  """
186
186
  def __init__(self):
187
187
  self.telegramBotLink = None
@@ -200,17 +200,17 @@ class Cheque(xRocketObject):
200
200
 
201
201
  API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheque
202
202
 
203
- :ivar chequeId: Cheque ID.
204
- :ivar clientChequeId: Unique cheque ID in your system to prevent double spends.
205
- :ivar asset: Currency of transfer.
206
- :ivar description: Description for cheque.
207
- :ivar targetType: Target type for cheque.
208
- :ivar target: Target for cheque.
209
- :ivar links: Cheque activation links.
210
- :ivar state: Cheque state.
211
- :ivar deleted: Cheque is cancelled and the reserved funds are returned to the application balance.
212
- :ivar callback: No description is provided.
213
- :ivar url: No description is provided.
203
+ :param chequeId: Cheque ID.
204
+ :param clientChequeId: Unique cheque ID in your system to prevent double spends.
205
+ :param asset: Currency of transfer.
206
+ :param description: Description for cheque.
207
+ :param targetType: Target type for cheque.
208
+ :param target: Target for cheque.
209
+ :param links: Cheque activation links.
210
+ :param state: Cheque state.
211
+ :param deleted: Cheque is cancelled and the reserved funds are returned to the application balance.
212
+ :param callback: No description is provided.
213
+ :param url: No description is provided.
214
214
  """
215
215
  def __init__(self):
216
216
  self.chequeId = None
@@ -240,15 +240,15 @@ class Payout(xRocketObject):
240
240
 
241
241
  API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-get-payout
242
242
 
243
- :ivar payoutId: Payout ID.
244
- :ivar clientPayoutId: Unique payout ID in your system to prevent double spends.
245
- :ivar target: Target.
246
- :ivar targetType: Target type.
247
- :ivar asset: Asset of payout.
248
- :ivar amount: Payout amount.
249
- :ivar description: Payout description.
250
- :ivar status: Payout status.
251
- :ivar callback: Webhook settings of this payout.
243
+ :param payoutId: Payout ID.
244
+ :param clientPayoutId: Unique payout ID in your system to prevent double spends.
245
+ :param target: Target.
246
+ :param targetType: Target type.
247
+ :param asset: Asset of payout.
248
+ :param amount: Payout amount.
249
+ :param description: Payout description.
250
+ :param status: Payout status.
251
+ :param callback: Webhook settings of this payout.
252
252
  """
253
253
  def __init__(self):
254
254
  self.payoutId = None
@@ -273,16 +273,16 @@ class Withdrawal(xRocketObject):
273
273
 
274
274
  API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawal
275
275
 
276
- :ivar withdrawalId: Unique withdrawal ID in your system to prevent double spends.
277
- :ivar network: Network code.
278
- :ivar address: Withdrawal address.
279
- :ivar asset: Asset code.
280
- :ivar amount: Withdrawal amount. 9 decimal places, others cut off.
281
- :ivar status: Withdrawal status.
282
- :ivar comment: Withdrawal comment.
283
- :ivar txHash: Withdrawal TX hash. Provided only after withdrawal.
284
- :ivar txLink: Withdrawal TX link. Provided only after withdrawal.
285
- :ivar callback: Webhook settings of this withdrawal.
276
+ :param withdrawalId: Unique withdrawal ID in your system to prevent double spends.
277
+ :param network: Network code.
278
+ :param address: Withdrawal address.
279
+ :param asset: Asset code.
280
+ :param amount: Withdrawal amount. 9 decimal places, others cut off.
281
+ :param status: Withdrawal status.
282
+ :param comment: Withdrawal comment.
283
+ :param txHash: Withdrawal TX hash. Provided only after withdrawal.
284
+ :param txLink: Withdrawal TX link. Provided only after withdrawal.
285
+ :param callback: Webhook settings of this withdrawal.
286
286
  """
287
287
  def __init__(self):
288
288
  self.withdrawalId = None
@@ -308,7 +308,7 @@ class CurrencyNetwork(xRocketObject):
308
308
 
309
309
  API: https://docs.xrocket.exchange/api/pay/reference/http/currencies-controller-get-currencies
310
310
 
311
- :ivar code: Network code.
311
+ :param code: Network code.
312
312
  """
313
313
  def __init__(self):
314
314
  self.code = None
@@ -325,10 +325,10 @@ class Currency(xRocketObject):
325
325
 
326
326
  API: https://docs.xrocket.exchange/api/pay/reference/http/currencies-controller-get-currencies
327
327
 
328
- :ivar code: No description is provided.
329
- :ivar title: No description is provided.
330
- :ivar kind: No description is provided.
331
- :ivar networks: No description is provided.
328
+ :param code: No description is provided.
329
+ :param title: No description is provided.
330
+ :param kind: No description is provided.
331
+ :param networks: No description is provided.
332
332
  """
333
333
  def __init__(self):
334
334
  self.code = None
@@ -350,8 +350,8 @@ class Rate(xRocketObject):
350
350
 
351
351
  API: https://docs.xrocket.exchange/api/pay/reference/http/rate-controller-get-rates
352
352
 
353
- :ivar currency: No description is provided.
354
- :ivar rate: Current rate.
353
+ :param currency: No description is provided.
354
+ :param rate: Current rate.
355
355
  """
356
356
  def __init__(self):
357
357
  self.currency = None
@@ -369,8 +369,8 @@ class xPage(xRocketObject):
369
369
 
370
370
  This is an SDK base model; the API has no standalone page endpoint.
371
371
 
372
- :ivar items: No description is provided.
373
- :ivar pagination: No description is provided.
372
+ :param items: No description is provided.
373
+ :param pagination: No description is provided.
374
374
  """
375
375
  def __init__(self):
376
376
  self.items = []
@@ -388,8 +388,8 @@ class CursorPagination(xRocketObject):
388
388
 
389
389
  This shared response fragment has no standalone API endpoint.
390
390
 
391
- :ivar total: Total quantity of campaigns.
392
- :ivar next: Cursor for the next page of results.
391
+ :param total: Total quantity of campaigns.
392
+ :param next: Cursor for the next page of results.
393
393
  """
394
394
  def __init__(self):
395
395
  self.total = None
@@ -407,7 +407,7 @@ class InvoicePaymentsPagination(xRocketObject):
407
407
 
408
408
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
409
409
 
410
- :ivar next: Cursor for the next page of results.
410
+ :param next: Cursor for the next page of results.
411
411
  """
412
412
  def __init__(self):
413
413
  self.next = None
@@ -424,14 +424,14 @@ class InvoicePayment(xRocketObject):
424
424
 
425
425
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
426
426
 
427
- :ivar id: Unique payment id. Use it to match webhook events with the invoice payments endpoint.
428
- :ivar status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
429
- :ivar finalizedAt: When payment reached final state (null for in-progress payments).
430
- :ivar payAmount: Gross amount payer sent in total across all transactions of this payment (before fees).
431
- :ivar payCurrency: Currency payer used.
432
- :ivar receiveAmount: Net amount merchant receives after fees, summed across all transactions of this payment (in invoice priceCurrency).
433
- :ivar receiveCurrency: Currency merchant receives (= invoice priceCurrency).
434
- :ivar transactions: No description is provided.
427
+ :param id: Unique payment id. Use it to match webhook events with the invoice payments endpoint.
428
+ :param status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
429
+ :param finalizedAt: When payment reached final state (null for in-progress payments).
430
+ :param payAmount: Gross amount payer sent in total across all transactions of this payment (before fees).
431
+ :param payCurrency: Currency payer used.
432
+ :param receiveAmount: Net amount merchant receives after fees, summed across all transactions of this payment (in invoice priceCurrency).
433
+ :param receiveCurrency: Currency merchant receives (= invoice priceCurrency).
434
+ :param transactions: No description is provided.
435
435
  """
436
436
  def __init__(self):
437
437
  self.id = None
@@ -455,8 +455,8 @@ class InvoicesList(xPage):
455
455
 
456
456
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoices
457
457
 
458
- :ivar items: No description is provided.
459
- :ivar pagination: No description is provided.
458
+ :param items: No description is provided.
459
+ :param pagination: No description is provided.
460
460
  """
461
461
  @classmethod
462
462
  def de_json(cls, json_dict):
@@ -474,8 +474,8 @@ class InvoicePaymentsList(xPage):
474
474
 
475
475
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
476
476
 
477
- :ivar items: No description is provided.
478
- :ivar pagination: No description is provided.
477
+ :param items: No description is provided.
478
+ :param pagination: No description is provided.
479
479
  """
480
480
  @classmethod
481
481
  def de_json(cls, json_dict):
@@ -493,8 +493,8 @@ class ChequesList(xPage):
493
493
 
494
494
  API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheques
495
495
 
496
- :ivar items: No description is provided.
497
- :ivar pagination: No description is provided.
496
+ :param items: No description is provided.
497
+ :param pagination: No description is provided.
498
498
  """
499
499
  @classmethod
500
500
  def de_json(cls, json_dict):
@@ -512,8 +512,8 @@ class PayoutsList(xPage):
512
512
 
513
513
  API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-get-list-payouts
514
514
 
515
- :ivar items: No description is provided.
516
- :ivar pagination: No description is provided.
515
+ :param items: No description is provided.
516
+ :param pagination: No description is provided.
517
517
  """
518
518
  @classmethod
519
519
  def de_json(cls, json_dict):
@@ -531,8 +531,8 @@ class WithdrawalsList(xPage):
531
531
 
532
532
  API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawals
533
533
 
534
- :ivar items: No description is provided.
535
- :ivar pagination: No description is provided.
534
+ :param items: No description is provided.
535
+ :param pagination: No description is provided.
536
536
  """
537
537
  @classmethod
538
538
  def de_json(cls, json_dict):
@@ -550,11 +550,11 @@ class InvoicePaymentAddress(xRocketObject):
550
550
 
551
551
  API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-payment-controller-create-invoice-payment-address
552
552
 
553
- :ivar address: Deposit address.
554
- :ivar payCurrency: Currency code.
555
- :ivar payNetwork: Network code.
556
- :ivar expiresAt: Payment expired at.
557
- :ivar minAmount: Minimum deposit amount (invoice has no fixed amount).
553
+ :param address: Deposit address.
554
+ :param payCurrency: Currency code.
555
+ :param payNetwork: Network code.
556
+ :param expiresAt: Payment expired at.
557
+ :param minAmount: Minimum deposit amount (invoice has no fixed amount).
558
558
  """
559
559
  def __init__(self):
560
560
  self.address = None
@@ -575,10 +575,10 @@ class WithdrawalQuotas(xRocketObject):
575
575
 
576
576
  API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawal-fees
577
577
 
578
- :ivar withdrawMinSize: Minimum withdrawal amount.
579
- :ivar withdrawFee: Withdrawal fee.
580
- :ivar withdrawFeeAsset: Withdrawal fee asset.
581
- :ivar precision: Floating point precision.
578
+ :param withdrawMinSize: Minimum withdrawal amount.
579
+ :param withdrawFee: Withdrawal fee.
580
+ :param withdrawFeeAsset: Withdrawal fee asset.
581
+ :param precision: Floating point precision.
582
582
  """
583
583
  def __init__(self):
584
584
  self.withdrawMinSize = None
@@ -598,9 +598,9 @@ class WithdrawalLink(xRocketObject):
598
598
 
599
599
  API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-links-controller-create-withdrawal-link
600
600
 
601
- :ivar telegramBotLink: Withdrawal telegram bot link.
602
- :ivar telegramMiniAppLink: Withdrawal telegram mini app link (soon).
603
- :ivar webLink: Withdrawal web link (soon).
601
+ :param telegramBotLink: Withdrawal telegram bot link.
602
+ :param telegramMiniAppLink: Withdrawal telegram mini app link (soon).
603
+ :param webLink: Withdrawal web link (soon).
604
604
  """
605
605
  def __init__(self):
606
606
  self.telegramBotLink = None
@@ -619,12 +619,12 @@ class MassPayoutReason(xRocketObject):
619
619
 
620
620
  API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
621
621
 
622
- :ivar type: A URI reference that identifies the problem type.
623
- :ivar title: A short, human-readable summary of the problem type.
624
- :ivar status: HTTP status code.
625
- :ivar detail: A human-readable explanation specific to this occurrence of the problem.
626
- :ivar instance: A URI reference that identifies the specific occurrence of the problem.
627
- :ivar kind: Problem category for easier error handling.
622
+ :param type: A URI reference that identifies the problem type.
623
+ :param title: A short, human-readable summary of the problem type.
624
+ :param status: HTTP status code.
625
+ :param detail: A human-readable explanation specific to this occurrence of the problem.
626
+ :param instance: A URI reference that identifies the specific occurrence of the problem.
627
+ :param kind: Problem category for easier error handling.
628
628
  """
629
629
  def __init__(self):
630
630
  self.type = None
@@ -646,12 +646,12 @@ class MassPayoutError(xRocketObject):
646
646
 
647
647
  API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
648
648
 
649
- :ivar target: Target.
650
- :ivar targetType: Target type (only TelegramUserId is supported for mass payouts).
651
- :ivar amount: Payout amount.
652
- :ivar clientPayoutId: Unique payout ID in your system to prevent double spends.
653
- :ivar description: Payout description.
654
- :ivar reason: Payout error reason.
649
+ :param target: Target.
650
+ :param targetType: Target type (only TelegramUserId is supported for mass payouts).
651
+ :param amount: Payout amount.
652
+ :param clientPayoutId: Unique payout ID in your system to prevent double spends.
653
+ :param description: Payout description.
654
+ :param reason: Payout error reason.
655
655
  """
656
656
  def __init__(self):
657
657
  self.target = None
@@ -676,8 +676,8 @@ class MassPayouts(xRocketObject):
676
676
 
677
677
  API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
678
678
 
679
- :ivar successPayouts: Successful payouts.
680
- :ivar errorPayouts: Error payouts.
679
+ :param successPayouts: Successful payouts.
680
+ :param errorPayouts: Error payouts.
681
681
  """
682
682
  def __init__(self):
683
683
  self.successPayouts = []
@@ -1,3 +1,3 @@
1
1
  """Package version, read by the build configuration."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -0,0 +1,480 @@
1
+ """Verification and parsing of xRocket Pay webhooks.
2
+
3
+ API: https://docs.xrocket.exchange/api/pay/pay-api-overview
4
+
5
+ This module does not receive HTTP requests and does not depend on Flask,
6
+ FastAPI, Django, or another web framework. Pass the request body and headers
7
+ from the HTTP layer that your application already uses.
8
+
9
+ Always verify a webhook before parsing or acting on it. The verifier requires
10
+ the original request body, preferably as ``bytes``. Do not pass a JSON object
11
+ that was decoded and serialized again: whitespace, field order, or escaping
12
+ can change and invalidate the signature.
13
+
14
+ For applications using the current xRocket Pay API, xRocket sends these
15
+ headers:
16
+
17
+ * ``Signature-Timestamp``: Unix timestamp in milliseconds when the webhook was sent.
18
+ * ``Signature-Version``: Signature scheme version. The current value is ``v1``.
19
+ * ``Signature``: hexadecimal HMAC-SHA-256 signature.
20
+
21
+ For version ``v1``, the signed byte sequence is::
22
+ {Signature-Timestamp}.{raw request body}
23
+
24
+ The application's webhook secret is the HMAC key without transformations.
25
+ Header names are compared case-insensitively. The legacy
26
+ ``Rocket-Pay-Signature`` header is intentionally unsupported because it does
27
+ not use the current signature scheme.
28
+
29
+ Use :func:`parse_and_verify_webhook` for the usual case::
30
+ event = parse_and_verify_webhook(
31
+ raw_body=request.body,
32
+ headers=request.headers,
33
+ webhook_secret=WEBHOOK_SECRET,
34
+ )
35
+
36
+ The returned object is one of :class:`InvoiceStatusChangedWebhook`,
37
+ :class:`PaymentStatusChangedWebhook`, :class:`ChequeWebhook`,
38
+ :class:`PayoutWebhook`, :class:`WithdrawalWebhook`, or
39
+ :class:`UnknownWebhook`.
40
+
41
+ The top-level webhook ``id`` identifies a delivery and remains stable across
42
+ retries. Persist and deduplicate this value in application storage before an
43
+ irreversible business action. Idempotency storage is deliberately outside this
44
+ library because its implementation depends on the application's database and
45
+ transaction rules.
46
+
47
+ xRocket can add new event and status values without notice. Unknown webhook
48
+ types or invoice event values become :class:`UnknownWebhook` instead of an
49
+ exception, so an application can log and safely ignore them. Invalid JSON,
50
+ missing required envelope fields, or an invalid signature are errors.
51
+
52
+ When a payment completes an invoice, xRocket sends both
53
+ ``payment_status_changed`` and ``invoice_status_changed``. Delivery order is
54
+ not guaranteed, including after retries. Process each event independently.
55
+ Track successful payments only when ``event`` is
56
+ ``payment_status_changed`` and ``event.data.payment.status`` is ``paid``;
57
+ track invoice completion only when ``event`` is
58
+ ``invoice_status_changed`` and ``event.data.invoice.status`` is ``paid``.
59
+ Payment finality is indicated by ``event.data.payment.finalizedAt`` not being
60
+ ``None``, rather than by relying on one fixed status value.
61
+
62
+ AI generated. Manually validated.
63
+ """
64
+
65
+ import hashlib
66
+ import hmac
67
+ import json
68
+ from collections.abc import Mapping
69
+ from typing import Any
70
+
71
+ from .exceptions import (
72
+ xRocketWebhookParseException,
73
+ xRocketWebhookSignatureException,
74
+ xRocketWebhookUnsupportedVersionException,
75
+ )
76
+ from .models import Cheque, Invoice, InvoicePayment, Payout, Withdrawal, xRocketObject
77
+
78
+
79
+ # noinspection method-overriding
80
+ class xRocketWebhook(xRocketObject):
81
+ """Base model for an xRocket webhook delivery.
82
+
83
+ API: https://docs.xrocket.exchange/api/pay/pay-api-overview
84
+
85
+ :param id: Unique webhook delivery id, can be used by the receiver for
86
+ idempotency.
87
+ :param timestamp: When webhook was sent.
88
+ :param type: Webhook type.
89
+ :param data: Typed webhook payload.
90
+ """
91
+
92
+ def __init__(self):
93
+ self.id = None
94
+ self.timestamp = None
95
+ self.type = None
96
+ self.data = None
97
+
98
+ @classmethod
99
+ def de_json(cls, json_dict):
100
+ """Create a webhook model from its parsed envelope dictionary.
101
+
102
+ :param json_dict: Parsed webhook envelope.
103
+ :return: ``xRocketWebhook``.
104
+ """
105
+ data = cls.check_json(json_dict)
106
+ return super(xRocketWebhook, cls).de_json(data, process_mode=2)
107
+
108
+
109
+ # noinspection method-overriding
110
+ class InvoiceStatusChangedWebhookData(xRocketObject):
111
+ """Payload of an ``invoice_status_changed`` webhook.
112
+
113
+ API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
114
+
115
+ :param event: Event type. This value is ``invoice_status_changed``.
116
+ :param invoice: Updated invoice.
117
+ """
118
+
119
+ def __init__(self):
120
+ self.event = None
121
+ self.invoice = None
122
+
123
+ @classmethod
124
+ def de_json(cls, json_dict):
125
+ """Create the payload from a parsed webhook dictionary.
126
+
127
+ :param json_dict: Parsed invoice-status-changed payload.
128
+ :return: ``InvoiceStatusChangedWebhookData``.
129
+ """
130
+ data = cls.check_json(json_dict)
131
+ instance = super(InvoiceStatusChangedWebhookData, cls).de_json(data, process_mode=2)
132
+ instance.invoice = Invoice.de_json(instance.invoice)
133
+ return instance
134
+
135
+
136
+ # noinspection method-overriding
137
+ class PaymentStatusChangedWebhookData(xRocketObject):
138
+ """Payload of a ``payment_status_changed`` webhook.
139
+
140
+ API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
141
+
142
+ :param event: Event type. This value is ``payment_status_changed``.
143
+ :param invoice: Updated invoice.
144
+ :param payment: Payment received for the invoice or whose status changed.
145
+ """
146
+
147
+ def __init__(self):
148
+ self.event = None
149
+ self.invoice = None
150
+ self.payment = None
151
+
152
+ @classmethod
153
+ def de_json(cls, json_dict):
154
+ """Create the payload from a parsed webhook dictionary.
155
+
156
+ :param json_dict: Parsed payment-status-changed payload.
157
+ :return: ``PaymentStatusChangedWebhookData``.
158
+ """
159
+ data = cls.check_json(json_dict)
160
+ instance = super(PaymentStatusChangedWebhookData, cls).de_json(data, process_mode=2)
161
+ instance.invoice = Invoice.de_json(instance.invoice)
162
+ instance.payment = InvoicePayment.de_json(instance.payment)
163
+ return instance
164
+
165
+
166
+ # noinspection method-overriding
167
+ class InvoiceStatusChangedWebhook(xRocketWebhook):
168
+ """Invoice webhook reporting an invoice status transition.
169
+
170
+ API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
171
+
172
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
173
+ :param timestamp: When webhook was sent.
174
+ :param type: Webhook type. This value is ``invoice``.
175
+ :param data: ``InvoiceStatusChangedWebhookData`` payload.
176
+ """
177
+
178
+ @classmethod
179
+ def de_json(cls, json_dict):
180
+ """Create the webhook from a parsed envelope dictionary.
181
+
182
+ :param json_dict: Parsed invoice webhook envelope.
183
+ :return: ``InvoiceStatusChangedWebhook``.
184
+ """
185
+ data = cls.check_json(json_dict)
186
+ instance = super(InvoiceStatusChangedWebhook, cls).de_json(data)
187
+ instance.data = InvoiceStatusChangedWebhookData.de_json(instance.data)
188
+ return instance
189
+
190
+
191
+ # noinspection method-overriding
192
+ class PaymentStatusChangedWebhook(xRocketWebhook):
193
+ """Invoice webhook reporting a received payment or payment status change.
194
+
195
+ API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
196
+
197
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
198
+ :param timestamp: When webhook was sent.
199
+ :param type: Webhook type. This value is ``invoice``.
200
+ :param data: ``PaymentStatusChangedWebhookData`` payload.
201
+ """
202
+
203
+ @classmethod
204
+ def de_json(cls, json_dict):
205
+ """Create the webhook from a parsed envelope dictionary.
206
+
207
+ :param json_dict: Parsed invoice webhook envelope.
208
+ :return: ``PaymentStatusChangedWebhook``.
209
+ """
210
+ data = cls.check_json(json_dict)
211
+ instance = super(PaymentStatusChangedWebhook, cls).de_json(data)
212
+ instance.data = PaymentStatusChangedWebhookData.de_json(instance.data)
213
+ return instance
214
+
215
+
216
+ class ChequeWebhook(xRocketWebhook):
217
+ """Webhook reporting an update to a cheque.
218
+
219
+ API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-create-cheque
220
+
221
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
222
+ :param timestamp: When webhook was sent.
223
+ :param type: Webhook type. This value is ``cheque``.
224
+ :param data: Updated ``Cheque``.
225
+ """
226
+
227
+ @classmethod
228
+ def de_json(cls, json_dict):
229
+ """Create the webhook from a parsed envelope dictionary.
230
+
231
+ :param json_dict: Parsed cheque webhook envelope.
232
+ :return: ``ChequeWebhook``.
233
+ """
234
+ data = cls.check_json(json_dict)
235
+ instance = super(ChequeWebhook, cls).de_json(data)
236
+ instance.data = Cheque.de_json(instance.data)
237
+ return instance
238
+
239
+
240
+ class PayoutWebhook(xRocketWebhook):
241
+ """Webhook reporting a payout status update.
242
+
243
+ API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-payout
244
+
245
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
246
+ :param timestamp: When webhook was sent.
247
+ :param type: Webhook type. This value is ``payout``.
248
+ :param data: Updated ``Payout``.
249
+ """
250
+
251
+ @classmethod
252
+ def de_json(cls, json_dict):
253
+ """Create the webhook from a parsed envelope dictionary.
254
+
255
+ :param json_dict: Parsed payout webhook envelope.
256
+ :return: ``PayoutWebhook``.
257
+ """
258
+ data = cls.check_json(json_dict)
259
+ instance = super(PayoutWebhook, cls).de_json(data)
260
+ instance.data = Payout.de_json(instance.data)
261
+ return instance
262
+
263
+
264
+ class WithdrawalWebhook(xRocketWebhook):
265
+ """Webhook reporting a withdrawal status update.
266
+
267
+ API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-create-withdrawal
268
+
269
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
270
+ :param timestamp: When webhook was sent.
271
+ :param type: Webhook type. This value is ``withdrawal``.
272
+ :param data: Updated ``Withdrawal``.
273
+ """
274
+
275
+ @classmethod
276
+ def de_json(cls, json_dict):
277
+ """Create the webhook from a parsed envelope dictionary.
278
+
279
+ :param json_dict: Parsed withdrawal webhook envelope.
280
+ :return: ``WithdrawalWebhook``.
281
+ """
282
+ data = cls.check_json(json_dict)
283
+ instance = super(WithdrawalWebhook, cls).de_json(data)
284
+ instance.data = Withdrawal.de_json(instance.data)
285
+ return instance
286
+
287
+
288
+ class UnknownWebhook(xRocketWebhook):
289
+ """Webhook whose type or invoice event is not known to this library.
290
+
291
+ xRocket may extend type and event values. This model retains the original
292
+ envelope instead of failing, so an application can log and ignore it.
293
+
294
+ :param id: Unique webhook delivery id, can be used by the receiver for idempotency.
295
+ :param timestamp: When webhook was sent.
296
+ :param type: Unknown webhook type, or ``invoice`` with an unknown event.
297
+ :param data: Original untyped webhook payload.
298
+ """
299
+
300
+
301
+ def verify_webhook_signature(raw_body: bytes | bytearray | str, headers: Mapping[str, Any], webhook_secret: str) -> None:
302
+ """Verify the current xRocket Pay ``v1`` webhook signature.
303
+
304
+ API: https://docs.xrocket.exchange/api/pay/pay-api-overview
305
+
306
+ ``raw_body`` must be the original request body. ``bytes`` is preferred;
307
+ a string is accepted only when it is the unchanged request body text.
308
+ A parsed dictionary is intentionally rejected because serializing it again
309
+ can alter the signed representation.
310
+
311
+ :param raw_body: Original request body as bytes, bytearray, or unchanged
312
+ text.
313
+ :param headers: HTTP request headers. Header names are case-insensitive.
314
+ :param webhook_secret: Application webhook secret used as the HMAC key
315
+ without transformations.
316
+ :return: ``None`` when the signature is valid.
317
+ :raises xRocketWebhookUnsupportedVersionException: If
318
+ ``Signature-Version`` is not ``v1``.
319
+ :raises xRocketWebhookSignatureException: If a required header is absent,
320
+ arguments are invalid, or the signature does not match.
321
+ """
322
+ body = _get_raw_body(raw_body)
323
+ if not isinstance(headers, Mapping):
324
+ raise xRocketWebhookSignatureException("headers must be a mapping.")
325
+ if not isinstance(webhook_secret, str) or not webhook_secret:
326
+ raise xRocketWebhookSignatureException("webhook_secret must be a non-empty string.")
327
+
328
+ timestamp = _get_header(headers, "Signature-Timestamp")
329
+ version = _get_header(headers, "Signature-Version")
330
+ signature = _get_header(headers, "Signature")
331
+ if not timestamp or not version or not signature:
332
+ raise xRocketWebhookSignatureException(
333
+ "Webhook headers Signature-Timestamp, Signature-Version, and Signature are required."
334
+ )
335
+ if version != "v1":
336
+ raise xRocketWebhookUnsupportedVersionException(
337
+ "Unsupported xRocket webhook signature version: {}.".format(version)
338
+ )
339
+
340
+ signed_body = timestamp.encode("utf-8") + b"." + body
341
+ expected_signature = hmac.new(
342
+ webhook_secret.encode("utf-8"), signed_body, hashlib.sha256
343
+ ).hexdigest()
344
+ if not hmac.compare_digest(signature, expected_signature):
345
+ raise xRocketWebhookSignatureException("xRocket webhook signature does not match.")
346
+
347
+
348
+ def parse_webhook(payload: bytes | bytearray | str | Mapping[str, Any]) -> xRocketWebhook:
349
+ """Parse an xRocket Pay webhook body into a typed model without verifying it.
350
+
351
+ API: https://docs.xrocket.exchange/api/pay/pay-api-overview
352
+
353
+ Use this only after independently calling :func:`verify_webhook_signature`.
354
+ In normal HTTP handling, prefer :func:`parse_and_verify_webhook`.
355
+
356
+ :param payload: Original JSON body as bytes or text, or a parsed envelope
357
+ mapping when signature verification has already occurred.
358
+ :return: A typed webhook model, or ``UnknownWebhook`` for an unknown type or invoice event.
359
+ :raises xRocketWebhookParseException: If the body is invalid JSON or does
360
+ not have the required webhook envelope structure.
361
+ """
362
+ envelope = _get_envelope(payload)
363
+ _validate_envelope(envelope)
364
+
365
+ webhook_type = envelope["type"]
366
+ if webhook_type == "invoice":
367
+ return _parse_invoice_webhook(envelope)
368
+ if webhook_type == "cheque":
369
+ _require_mapping(envelope["data"], "data")
370
+ return ChequeWebhook.de_json(envelope)
371
+ if webhook_type == "payout":
372
+ _require_mapping(envelope["data"], "data")
373
+ return PayoutWebhook.de_json(envelope)
374
+ if webhook_type == "withdrawal":
375
+ _require_mapping(envelope["data"], "data")
376
+ return WithdrawalWebhook.de_json(envelope)
377
+ return UnknownWebhook.de_json(envelope)
378
+
379
+
380
+ def parse_and_verify_webhook(
381
+ raw_body: bytes | bytearray | str,
382
+ headers: Mapping[str, Any],
383
+ webhook_secret: str,
384
+ ) -> xRocketWebhook:
385
+ """Verify and parse an xRocket Pay webhook in the safe order.
386
+
387
+ API: https://docs.xrocket.exchange/api/pay/pay-api-overview
388
+
389
+ The signature is verified against the unchanged request body before JSON
390
+ parsing. The returned event must be deduplicated by its ``id`` in the
391
+ application's persistent storage before an irreversible business action.
392
+
393
+ :param raw_body: Original request body as bytes, bytearray, or unchanged
394
+ text.
395
+ :param headers: HTTP request headers. Header names are case-insensitive.
396
+ :param webhook_secret: Application webhook secret used as the HMAC key without transformations.
397
+ :return: A typed webhook model, or ``UnknownWebhook`` for an unknown type or invoice event.
398
+ :raises xRocketWebhookSignatureException: If signature validation fails.
399
+ :raises xRocketWebhookParseException: If the verified body is invalid JSON
400
+ or does not have the required webhook envelope structure.
401
+ """
402
+ verify_webhook_signature(raw_body, headers, webhook_secret)
403
+ return parse_webhook(raw_body)
404
+
405
+
406
+ def _get_raw_body(raw_body: bytes | bytearray | str) -> bytes:
407
+ """Return the unchanged request body as bytes for signature verification."""
408
+ if isinstance(raw_body, bytes):
409
+ return raw_body
410
+ if isinstance(raw_body, bytearray):
411
+ return bytes(raw_body)
412
+ if isinstance(raw_body, str):
413
+ return raw_body.encode("utf-8")
414
+ raise xRocketWebhookSignatureException(
415
+ "raw_body must be bytes, bytearray, or unchanged request body text."
416
+ )
417
+
418
+
419
+ def _get_header(headers: Mapping[str, Any], expected_name: str) -> str | None:
420
+ """Read one HTTP header without relying on a framework's header type."""
421
+ expected_name = expected_name.lower()
422
+ for name, value in headers.items():
423
+ if str(name).lower() == expected_name:
424
+ return str(value)
425
+ return None
426
+
427
+
428
+ def _get_envelope(payload: bytes | bytearray | str | Mapping[str, Any]) -> dict[str, Any]:
429
+ """Decode a JSON webhook body or make a shallow copy of a mapping."""
430
+ if isinstance(payload, Mapping):
431
+ return dict(payload)
432
+ if isinstance(payload, bytearray):
433
+ payload = bytes(payload)
434
+ if isinstance(payload, bytes):
435
+ try:
436
+ payload = payload.decode("utf-8")
437
+ except UnicodeDecodeError as error:
438
+ raise xRocketWebhookParseException("Webhook body is not UTF-8 JSON.") from error
439
+ if isinstance(payload, str):
440
+ try:
441
+ payload = json.loads(payload)
442
+ except json.JSONDecodeError as error:
443
+ raise xRocketWebhookParseException("Webhook body is not valid JSON.") from error
444
+ if not isinstance(payload, Mapping):
445
+ raise xRocketWebhookParseException("Webhook body must contain a JSON object.")
446
+ return dict(payload)
447
+
448
+
449
+ def _validate_envelope(envelope: Mapping[str, Any]) -> None:
450
+ """Validate the fields required by every documented webhook envelope."""
451
+ for field in ("id", "timestamp", "type", "data"):
452
+ if field not in envelope:
453
+ raise xRocketWebhookParseException(
454
+ "Webhook body is missing required field: {}.".format(field)
455
+ )
456
+ if not isinstance(envelope["type"], str):
457
+ raise xRocketWebhookParseException("Webhook field type must be a string.")
458
+
459
+
460
+ def _parse_invoice_webhook(envelope: dict[str, Any]) -> xRocketWebhook:
461
+ """Choose the typed invoice webhook model using the nested event field."""
462
+ data = _require_mapping(envelope["data"], "data")
463
+ event = data.get("event")
464
+ if event == "invoice_status_changed":
465
+ _require_mapping(data.get("invoice"), "data.invoice")
466
+ return InvoiceStatusChangedWebhook.de_json(envelope)
467
+ if event == "payment_status_changed":
468
+ _require_mapping(data.get("invoice"), "data.invoice")
469
+ _require_mapping(data.get("payment"), "data.payment")
470
+ return PaymentStatusChangedWebhook.de_json(envelope)
471
+ return UnknownWebhook.de_json(envelope)
472
+
473
+
474
+ def _require_mapping(value: Any, field_name: str) -> Mapping[str, Any]:
475
+ """Return a payload object or raise a precise structural parse error."""
476
+ if not isinstance(value, Mapping):
477
+ raise xRocketWebhookParseException(
478
+ "Webhook field {} must be a JSON object.".format(field_name)
479
+ )
480
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyXRocketAPI
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python client for the xRocket Pay API
5
5
  Author: Badiboy
6
6
  License-Expression: MIT
@@ -17,7 +17,11 @@ License-File: LICENSE
17
17
  Requires-Dist: requests>=2.28
18
18
  Dynamic: license-file
19
19
 
20
- # pyXRocketAPI
20
+ [![PyPi Package Version](https://img.shields.io/pypi/v/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
21
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyXRocketAPI.svg)](https://pypi.python.org/pypi/pyXRocketAPI)
22
+ [![PyPi downloads](https://img.shields.io/pypi/dm/pyXRocketAPI.svg)](https://pypi.org/project/pyXRocketAPI/)
23
+
24
+ # <p align="center">pyXRocketAPI</p>
21
25
 
22
26
  **pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
23
27
 
@@ -6,9 +6,11 @@ pyXRocketAPI/api.py
6
6
  pyXRocketAPI/exceptions.py
7
7
  pyXRocketAPI/models.py
8
8
  pyXRocketAPI/version.py
9
+ pyXRocketAPI/webhooks.py
9
10
  pyXRocketAPI.egg-info/PKG-INFO
10
11
  pyXRocketAPI.egg-info/SOURCES.txt
11
12
  pyXRocketAPI.egg-info/dependency_links.txt
12
13
  pyXRocketAPI.egg-info/requires.txt
13
14
  pyXRocketAPI.egg-info/top_level.txt
14
- tests/test_api.py
15
+ tests/test_api.py
16
+ tests/test_webhooks.py
@@ -0,0 +1,105 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import unittest
5
+
6
+ from pyXRocketAPI import (
7
+ Cheque,
8
+ ChequeWebhook,
9
+ Invoice,
10
+ Payout,
11
+ PayoutWebhook,
12
+ PaymentStatusChangedWebhook,
13
+ UnknownWebhook,
14
+ Withdrawal,
15
+ WithdrawalWebhook,
16
+ parse_and_verify_webhook,
17
+ parse_webhook,
18
+ xRocketWebhookParseException,
19
+ xRocketWebhookSignatureException,
20
+ )
21
+ from pyXRocketAPI.models import InvoicePayment
22
+
23
+
24
+ WEBHOOK_SECRET = "webhook-secret"
25
+ SIGNATURE_TIMESTAMP = "1735689600000"
26
+
27
+
28
+ def signed_webhook(payload):
29
+ body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
30
+ signature = hmac.new(
31
+ WEBHOOK_SECRET.encode("utf-8"),
32
+ SIGNATURE_TIMESTAMP.encode("utf-8") + b"." + body,
33
+ hashlib.sha256,
34
+ ).hexdigest()
35
+ return body, {
36
+ "signature-timestamp": SIGNATURE_TIMESTAMP,
37
+ "signature-version": "v1",
38
+ "signature": signature,
39
+ }
40
+
41
+
42
+ class XRocketWebhookTests(unittest.TestCase):
43
+ def test_parses_and_verifies_payment_status_changed_webhook(self):
44
+ body, headers = signed_webhook({
45
+ "id": "delivery-1",
46
+ "timestamp": "2025-01-01T00:00:00.000Z",
47
+ "type": "invoice",
48
+ "data": {
49
+ "event": "payment_status_changed",
50
+ "invoice": {"id": "invoice-1", "status": "paid"},
51
+ "payment": {"id": "payment-1", "status": "paid", "finalizedAt": "2025-01-01T00:00:00.000Z"},
52
+ },
53
+ })
54
+
55
+ event = parse_and_verify_webhook(body, headers, WEBHOOK_SECRET)
56
+
57
+ self.assertIsInstance(event, PaymentStatusChangedWebhook)
58
+ self.assertEqual(event.id, "delivery-1")
59
+ self.assertIsInstance(event.data.invoice, Invoice)
60
+ self.assertIsInstance(event.data.payment, InvoicePayment)
61
+ self.assertEqual(event.data.payment.status, "paid")
62
+
63
+ def test_rejects_invalid_signature_before_parsing(self):
64
+ body, headers = signed_webhook({"id": "delivery-1", "timestamp": "now", "type": "payout", "data": {}})
65
+
66
+ with self.assertRaises(xRocketWebhookSignatureException):
67
+ parse_and_verify_webhook(body + b" ", headers, WEBHOOK_SECRET)
68
+
69
+ def test_returns_unknown_webhook_for_future_type_and_invoice_event(self):
70
+ future_type = parse_webhook({
71
+ "id": "delivery-1", "timestamp": "now", "type": "future_type", "data": {"value": 1},
72
+ })
73
+ future_invoice_event = parse_webhook({
74
+ "id": "delivery-2", "timestamp": "now", "type": "invoice", "data": {"event": "future_event"},
75
+ })
76
+
77
+ self.assertIsInstance(future_type, UnknownWebhook)
78
+ self.assertEqual(future_type.data, {"value": 1})
79
+ self.assertIsInstance(future_invoice_event, UnknownWebhook)
80
+ self.assertEqual(future_invoice_event.data["event"], "future_event")
81
+
82
+ def test_parses_other_documented_webhook_types(self):
83
+ cases = (
84
+ ("cheque", {"chequeId": "cheque-1"}, ChequeWebhook, Cheque),
85
+ ("payout", {"payoutId": "payout-1"}, PayoutWebhook, Payout),
86
+ ("withdrawal", {"withdrawalId": "withdrawal-1"}, WithdrawalWebhook, Withdrawal),
87
+ )
88
+
89
+ for webhook_type, data, webhook_class, data_class in cases:
90
+ with self.subTest(webhook_type=webhook_type):
91
+ event = parse_webhook({
92
+ "id": "delivery-1", "timestamp": "now", "type": webhook_type, "data": data,
93
+ })
94
+ self.assertIsInstance(event, webhook_class)
95
+ self.assertIsInstance(event.data, data_class)
96
+
97
+ def test_rejects_malformed_webhook_envelope(self):
98
+ with self.assertRaises(xRocketWebhookParseException):
99
+ parse_webhook(b"not json")
100
+ with self.assertRaises(xRocketWebhookParseException):
101
+ parse_webhook({"id": "delivery-1", "timestamp": "now", "type": "payout"})
102
+
103
+
104
+ if __name__ == "__main__":
105
+ unittest.main()
File without changes
File without changes