Skip to content

Commit fb4e828

Browse files
committed
Added pay_invoice method
1 parent b918f75 commit fb4e828

File tree

8 files changed

+240
-13
lines changed

8 files changed

+240
-13
lines changed

docs/errors.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ These errors are not mapped to a specific resource
5151

5252
These errors are mapped to the invoice resource
5353

54-
| Code | Error | Type
55-
| ------ | ------ | ------ |
54+
| Code | Error | Type
55+
|--------| ------ | ------ |
5656
| xx0100 | Generic server error | `string` |
5757
| xx0101 | Invoice not found | `string` |
5858
| xx0102 | Invalid params | `string` |
@@ -64,17 +64,24 @@ These errors are mapped to the invoice resource
6464
| xx0111 | Invoice price is above maximum threshold | `string` |
6565
| xx0112 | Invalid sms number | `string` |
6666
| xx0113 | Error verifying sms | `string` |
67+
| xx0114 | Unable to update contact information on high value transaction | `string` |
68+
| xx0115 | Email already set on invoice | `string` |
69+
| xx0116 | Unable to perform action outside of demo environment | `string` |
70+
| xx0117 | nvalid invoice state | `string` |
71+
| xx0118 | Misconfigured account | `string` |
72+
| xx0119 | Unable to apply mock transaction | `string` |
6773

6874
**Refund Errors: xx02xx**
6975

7076
These errors are mapped to the refund resource
7177

72-
| Code | Error | Type
73-
| ------ | ------ | ------ |
78+
| Code | Error | Type
79+
|--------| ------ | ------ |
7480
| xx0200 | Generic refund error | `string` |
7581
| xx0201 | Refund not found | `string` |
7682
| xx0202 | Invalid params | `string` |
7783
| xx0203 | Missing params | `string` |
84+
| xx0204 | Active refund request already exists | `string` |
7885
| xx0207 | Invalid invoice state for refund | `string` |
7986
| xx0208 | Fees are greater than refund amount | `string` |
8087

docs/invoice.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,40 @@ cancel_invoice = bitpay.cancel_invoice(invoice_id)
803803
invoice_status = bitpay.request_invoice_notifications(invoice_id)
804804
```
805805

806+
## Pay an invoice
807+
808+
Pay an invoice with a mock transaction
809+
810+
:warning: Only available in test or demo environment
811+
812+
:warning: HVT invoices require buyer E-mail
813+
814+
`PUT /invoices/pay/:invoiceId`
815+
816+
Facades **`MERCHANT`**
817+
818+
### HTTP Request
819+
820+
**URL Parameters**
821+
822+
| Parameter | Description |Type | Presence
823+
| ------ |------------------------------------------------------------------------------------| -- |------ |
824+
| complete | Indicate if paid invoice should have status if complete true or a confirmed status | `boolean` | **Mandatory** |
825+
826+
**Headers**
827+
828+
| Fields | Description | Presence
829+
| ------ | ------ | ------ |
830+
| X-Accept-Version | must be set to `2.0.0` for requests to the BitPay API | **Mandatory** |
831+
| Content-Type | must be set to `application/json` for requests to the BitPay API | **Mandatory** |
832+
| X-Identity | the hexadecimal public key generated from the client private key. This header is required when using tokens with higher privileges (merchant facade). When using standard pos facade token directly from the BitPay dashboard (with "Require Authentication" disabled), this header is not needed. | C |
833+
| X-Signature | header is the ECDSA signature of the full request URL concatenated with the request body, signed with your private key. This header is required when using tokens with higher privileges (merchant facade). When using standard pos facade token directly from the BitPay dashboard (with "Require Authentication" disabled), this header is not needed. | C |
834+
835+
```python
836+
create_invoice = bitpay.create_invoice(new Invoice(100.0, "USD"))
837+
pay_invoice = bitpay.pay_invoice(create_invoice.get_id())
838+
```
839+
806840
### Error Scenarios & Format:
807841

808842
| Field | Description |Type |
@@ -811,7 +845,7 @@ invoice_status = bitpay.request_invoice_notifications(invoice_id)
811845
| code | six digit code that maps to an error on BitPay’s side | `string` |
812846
| data | will be null in an error scenario | `string` |
813847
| message | error message pertaining to the specific error | `string` |
814-
848+
815849
```json
816850
{
817851
"status": "error",
@@ -822,5 +856,4 @@ invoice_status = bitpay.request_invoice_notifications(invoice_id)
822856

823857
```
824858

825-
826-
### [Back to guide index](../GUIDE.md)
859+
### [Back to guide index](../GUIDE.md)

docs/ledger.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,41 @@ print(ledger)
172172
]
173173
```
174174

175+
## Ledger Codes
175176

176-
### [Back to guide index](../GUIDE.md)
177+
| Code | Name | Type | Description |
178+
|-------------|-------------------------------------------------------------------------------|----------|--|
179+
| 1050 | Immediate Invoice Refund | `debit` | This entry is written once an made on an invoice is created. Theimmediate refund request merchant balance is debited with the refund amount requested. |
180+
181+
## Ledger Entries
182+
183+
Here is an example of what a ledger entry for a refund will look like from the settlement report:
184+
185+
```json
186+
{
187+
"code": 1050,
188+
"description": "Immediate Invoice Refund",
189+
"timestamp": "2022-02-14T16:57:48.810Z",
190+
"amount": -1540,
191+
"invoiceId": "QXfxh8bx1z6qzLsNcTSW9N",
192+
"invoiceData": {
193+
"date": "2022-02-14T15:51:55.048Z",
194+
"price": 1540,
195+
"currency": "USD",
196+
"transactionCurrency": "BTC",
197+
"buyerEmailAddress": "[email protected]",
198+
"payoutPercentage": {
199+
"USD": 100
200+
}
201+
},
202+
"refundInfo": {
203+
"supportRequest": "P2z9ke5phuhZRHoBJtqSjU",
204+
"currency": "USD",
205+
"amounts": {
206+
"USD": 100,
207+
"BTC": 0.002265
208+
},
209+
"reference": "customReference"
210+
}
211+
}
212+
```

docs/refund.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,64 @@ result = bitpay.request_refund_notification(refund_id)
272272
| --- | --- | :---: |
273273
| status | successful response status will be always be “success” | `string` |
274274

275+
## Refund Webhooks
276+
277+
To receive webhooks, pass a `notificationURL` along during invoice creation for the webhook destination.
278+
279+
**Refund Webhook Lifecycle:**
280+
281+
| Order of Execution | Name | Code | Description |
282+
|--------------------|----------------|:----:|:-----------------------------------------------------------------------------------:|
283+
| 1 | refund_created | 7001 | received after refund request creation |
284+
| 2 | refund_pending | 7002 | received after customer has provided refund address |
285+
| 3* | refund_success | 7003 | received after refund request has executed and funds have been paid out to customer |
286+
| 3* | refund_failure | 7004 | received after a refund request fails during execution |
287+
288+
* either one of these will occur on the final step of refund proces
289+
290+
**Refund Webhook Body:**
291+
292+
| Name | Description | Type |
293+
|-------------------------|---------------------------------------------------------------------------------------------------------|:---------:|
294+
| event | metadata about webhook | `object` |
295+
| code | system code associated with the refund status (reference chart above) | `number` |
296+
| name | refund status name (reference chart above) | `string` |
297+
| data | refund request data for webhook | `object` |
298+
| id | the refund request id | `string` |
299+
| invoice | associated invoice id | `string` |
300+
| supportRequest * | associated support request id | `string` |
301+
| status | status refund lifecycle status of the request (refer to field in POST status refunds response) | `string` |
302+
| amount | amount to be refunded in the currency indicated | `number` |
303+
| currency | reference currency used for the refund, usually the same as the currency used to create the invoice | `date` |
304+
| lastRefundNotification* | timestamp of last notification sent to customer about refund | `number` |
305+
| refundFee | the amount of refund fee expressed in terms of pricing currency | `boolean` |
306+
| immediate | whether funds should be removed from merchant ledger immediately on submission or at time of processing | `boolean` |
307+
| buyerPaysRefundFee | whether the buyer should pay the refund fee (default is merchant) | `string` |
308+
| requestDate | timestamp the refund request was created | `date` |
309+
310+
* these fields are only sent on webhooks that occur AFTER refund_created
275311

312+
```json
313+
# example refund webhook
314+
{
315+
"event": {
316+
"code": 7002,
317+
"name": "refund_pending"
318+
},
319+
"data": {
320+
"id": "GZBBLcsgQamua3PN8GX92s",
321+
"invoice": "Wp9cpGphCz7cSeFh6MSYpb",
322+
"supportRequest": "XuuYtZfTw7G99Ws3z38kWZ",
323+
"status": "pending",
324+
"amount": 6,
325+
"currency": "USD",
326+
"lastRefundNotification": "2022-01-11T16:58:23.967Z",
327+
"refundFee": 2.31,
328+
"immediate": false,
329+
"buyerPaysRefundFee": true,
330+
"requestDate": "2022-01-11T16:58:23.000Z"
331+
}
332+
}
333+
```
276334

277-
### [Back to guide index](../GUIDE.md)
335+
### [Back to guide index](../GUIDE.md)

src/bitpay/client.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from .exceptions.currency_query_exception import CurrencyQueryException
4444
from .exceptions.refund_creation_exception import RefundCreationException
4545
from .exceptions.payout_creation_exception import PayoutCreationException
46+
from .exceptions.invoice_payment_exception import InvoicePaymentException
4647
from .exceptions.settlement_query_exception import SettlementQueryException
4748
from .exceptions.invoice_creation_exception import InvoiceCreationException
4849
from .exceptions.payoutbatch_query_exception import PayoutBatchQueryException
@@ -407,18 +408,23 @@ def update_invoice(self, invoice_id: str, buyer_email: str) -> Invoice:
407408

408409
return invoice
409410

410-
def cancel_invoice(self, invoice_id: str) -> Invoice:
411+
def cancel_invoice(self, invoice_id: str, force_cancel: bool = False) -> Invoice:
411412
"""
412413
Delete a previously created BitPay invoice.
413414
414415
:param str invoice_id: The Id of the BitPay invoice to be canceled.
416+
:param bool force_cancel: Query param that will cancel the invoice even if
417+
no contact information is present
415418
:return: A BitPay generated Invoice object.
416419
:rtype: Invoice
417420
:raises BitPayException
418421
:raises InvoiceCancellationException
419422
"""
420423
try:
421-
params = {"token": self.get_access_token(Facade.Merchant)}
424+
params = {
425+
"token": self.get_access_token(Facade.Merchant),
426+
"force_cancel": force_cancel,
427+
}
422428
response_json = self.__restcli.delete("invoices/%s" % invoice_id, params)
423429
except BitPayException as exe:
424430
raise InvoiceCancellationException(
@@ -438,6 +444,45 @@ def cancel_invoice(self, invoice_id: str) -> Invoice:
438444
)
439445
return invoice
440446

447+
def pay_invoice(self, invoice_id: str, complete: bool = True) -> Invoice:
448+
"""
449+
Pay an invoice with a mock transaction.
450+
451+
:param str invoice_id: The Id of the BitPay invoice.
452+
:param bool complete: indicate if paid invoice should have status if complete true or a confirmed status.
453+
:return: A BitPay generated Invoice object.
454+
:rtype: Invoice
455+
:raises BitPayException
456+
:raises InvoicePaymentException
457+
"""
458+
if self.__env.lower() != "test":
459+
raise InvoicePaymentException(
460+
"Pay Invoice method only available in test or demo environments"
461+
)
462+
try:
463+
params = {
464+
"token": self.get_access_token(Facade.Merchant),
465+
"complete": complete,
466+
}
467+
response_json = self.__restcli.update("invoices/pay/%s" % invoice_id, params)
468+
except BitPayException as exe:
469+
raise InvoicePaymentException(
470+
"failed to serialize Invoice object : %s" % str(exe), exe.get_api_code()
471+
)
472+
except Exception as exe:
473+
raise InvoicePaymentException(
474+
"failed to serialize Invoice object : %s" % str(exe)
475+
)
476+
477+
try:
478+
invoice = Invoice(**response_json)
479+
except Exception as exe:
480+
raise InvoicePaymentException(
481+
"failed to deserialize BitPay server"
482+
" response (Invoice) : %s" % str(exe)
483+
)
484+
return invoice
485+
441486
def request_invoice_notifications(self, invoice_id: str) -> bool:
442487
"""
443488
Request a BitPay Invoice Webhook.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Invoice Payment exception gets raised when it fails to pay invoice.
3+
"""
4+
from .invoice_exception import InvoiceException
5+
6+
7+
class InvoicePaymentException(InvoiceException):
8+
"""
9+
InvoicePaymentException
10+
"""
11+
12+
__bitpay_message = "Failed to pay invoice"
13+
__bitpay_code = "BITPAY-INVOICE-PAY-UPDATE"
14+
__api_code = ""
15+
16+
def __init__(self, message, code=107, api_code="000000"):
17+
"""
18+
Construct the InvoicePaymentException.
19+
20+
:param message: The Exception message to throw.
21+
:param code: [optional] The Exception code to throw.
22+
:param api_code: [optional] The API Exception code to throw.
23+
"""
24+
message = self.__bitpay_code + ": " + self.__bitpay_message + ":" + message
25+
self.__api_code = api_code
26+
super().__init__(message, code)

src/bitpay/models/invoice/refund.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Refund:
2323
__invoice_id = None
2424
__preview = None
2525
__immediate = None
26+
__reference = None
2627
__buyer_pays_refund_fee = None
2728
__refund_fee = None
2829
__last_refund_notification = None
@@ -108,6 +109,20 @@ def set_immediate(self, immediate):
108109
"""
109110
self.__immediate = immediate
110111

112+
def get_reference(self):
113+
"""
114+
Get method for the reference
115+
:return: reference
116+
"""
117+
return self.__reference
118+
119+
def set_reference(self, reference):
120+
"""
121+
Set method for the reference
122+
:param reference: reference
123+
"""
124+
self.__reference = reference
125+
111126
def get_buyer_pays_refund_fee(self):
112127
"""
113128
Get method for the buyer_pays_refund_fee
@@ -295,6 +310,7 @@ def to_json(self):
295310
"lastRefundNotification": self.get_last_refund_notification(),
296311
"invoice": self.get_invoice(),
297312
"immediate": self.get_immediate(),
313+
"reference": self.get_reference(),
298314
"preview": self.get_preview(),
299315
"invoiceId": self.get_invoice_id(),
300316
}

tests/test_bitpay.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def test_should_create_update_and_delete_invoice(self):
116116
updated_invoice = self.client.update_invoice(
117117
retrieved_invoice.get_id(), "[email protected]"
118118
)
119-
cancelled_invoice = self.client.cancel_invoice(updated_invoice.get_id())
119+
cancelled_invoice = self.client.cancel_invoice(updated_invoice.get_id(), False)
120120
retrieved_cancelled_invoice = self.client.get_invoice(
121121
cancelled_invoice.get_id()
122122
)
@@ -132,13 +132,19 @@ def test_should_request_invoice_webhook(self):
132132
result = self.client.request_invoice_notifications(basic_invoice.get_id())
133133
self.assertTrue(result)
134134

135+
def test_should_pay_invoice(self):
136+
basic_invoice = self.client.create_invoice(Invoice(2, "btc"))
137+
pay_invoice = self.client.pay_invoice(basic_invoice.get_id())
138+
self.assertIsNotNone(basic_invoice)
139+
self.assertIsNotNone(pay_invoice)
140+
135141
def test_should_create_get_cancel_refund_request_new(self):
136142
today = date.today().strftime("%Y-%m-%d")
137143
date_start = (date.today() - timedelta(days=30)).strftime("%Y-%m-%d")
138144
invoices = self.client.get_invoices(
139145
date_start, today, "complete", None, None, None
140146
)
141-
first_invoice = invoices[0]
147+
first_invoice = invoices[14]
142148
create_refund = self.client.create_refund(
143149
first_invoice.get_id(), first_invoice.get_price(), first_invoice.get_currency(), True, False, False
144150
)

0 commit comments

Comments
 (0)