dime-python-sdk 1.2.0__tar.gz → 1.3.1__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.
Files changed (75) hide show
  1. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/PKG-INFO +52 -3
  2. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/README.md +50 -1
  3. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/pyproject.toml +1 -1
  4. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/config.py +1 -1
  5. dime_python_sdk-1.3.1/src/dime_payments/data_objects/cover_fee_quote.py +48 -0
  6. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice.py +13 -0
  7. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice_payment.py +10 -1
  8. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/recurring_invoice.py +4 -1
  9. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/http/transport.py +1 -1
  10. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/invoices.py +18 -0
  11. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/recurring_invoices.py +7 -0
  12. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_invoices.py +102 -0
  13. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/.github/workflows/publish.yml +0 -0
  14. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/.gitignore +0 -0
  15. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/LICENSE +0 -0
  16. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/__init__.py +0 -0
  17. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/client.py +0 -0
  18. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/__init__.py +0 -0
  19. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/address.py +0 -0
  20. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/customer.py +0 -0
  21. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/deposit.py +0 -0
  22. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/deposit_group.py +0 -0
  23. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/deposit_with_transactions.py +0 -0
  24. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/form_link.py +0 -0
  25. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice_customer.py +0 -0
  26. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice_event.py +0 -0
  27. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice_item.py +0 -0
  28. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/invoice_link.py +0 -0
  29. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/line_item.py +0 -0
  30. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/merchant.py +0 -0
  31. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/message_result.py +0 -0
  32. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/payment_method.py +0 -0
  33. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/recurring_payment.py +0 -0
  34. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/recurring_payment_method.py +0 -0
  35. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/tokenize_result.py +0 -0
  36. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/transaction.py +0 -0
  37. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/data_objects/transaction_address.py +0 -0
  38. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/__init__.py +0 -0
  39. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/api_exception.py +0 -0
  40. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/authentication_exception.py +0 -0
  41. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/connection_exception.py +0 -0
  42. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/dime_exception.py +0 -0
  43. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/not_found_exception.py +0 -0
  44. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/permission_denied_exception.py +0 -0
  45. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/rate_limit_exception.py +0 -0
  46. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/server_exception.py +0 -0
  47. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/exceptions/validation_exception.py +0 -0
  48. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/http/__init__.py +0 -0
  49. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/http/error_handler.py +0 -0
  50. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/pagination/__init__.py +0 -0
  51. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/pagination/cursor_page.py +0 -0
  52. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/__init__.py +0 -0
  53. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/abstract_resource.py +0 -0
  54. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/addresses.py +0 -0
  55. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/customers.py +0 -0
  56. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/deposits.py +0 -0
  57. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/merchants.py +0 -0
  58. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/payment_methods.py +0 -0
  59. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/recurring_payments.py +0 -0
  60. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/resources/transactions.py +0 -0
  61. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/support/__init__.py +0 -0
  62. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/src/dime_payments/support/arr.py +0 -0
  63. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/__init__.py +0 -0
  64. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/helpers.py +0 -0
  65. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/__init__.py +0 -0
  66. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_addresses.py +0 -0
  67. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_customers.py +0 -0
  68. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_deposits.py +0 -0
  69. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_error_handling.py +0 -0
  70. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_merchants.py +0 -0
  71. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_pagination.py +0 -0
  72. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_payment_methods.py +0 -0
  73. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_recurring_invoices.py +0 -0
  74. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_recurring_payments.py +0 -0
  75. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.1}/tests/unit/test_transactions.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: dime-python-sdk
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: Python client for the Dime Payments API
5
5
  Author: Dime Technology
6
6
  License: MIT License
@@ -218,6 +218,11 @@ item (a fund or designation). Draft invoices can be edited; once sent they are l
218
218
  Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
219
219
  identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
220
220
 
221
+ **Statuses.** `invoice.status` is one of `draft`, `sent`, `viewed`, `partially_paid`, `paid`, `void` or
222
+ `refunded`. `paid` is not always final: if the customer's bank returns an ACH payment, the invoice is
223
+ reopened (back to `partially_paid`, `viewed` or `sent`, with `amount_paid` and `balance` updated) and an
224
+ `invoice_payment_returned` webhook fires. Re-read the invoice rather than caching a `paid` status forever.
225
+
221
226
  ```python
222
227
  # Look up (or create) the merchant items a line can reference
223
228
  items = dime.invoices.list_items('000010')
@@ -267,9 +272,52 @@ dime.invoices.void('000010', invoice.id)
267
272
  dime.invoices.duplicate('000010', invoice.id)
268
273
  ```
269
274
 
275
+ #### Making the customer cover processing fees
276
+
277
+ Set `cover_fee_required` and the customer must pay the processing fee — it is not an optional
278
+ checkbox at checkout. The fee is **not** a line item and is **not** part of `total`: the merchant is
279
+ still owed `total`, and the fee is added on top of whatever the customer pays.
280
+
281
+ Card and ACH rates differ, so the charge depends on how the customer pays. `cover_fee_quote` gives
282
+ you both, quoted against the outstanding balance:
283
+
284
+ ```python
285
+ invoice = dime.invoices.create('000010', {
286
+ 'customer_uuid': customer.uuid,
287
+ 'customer_name': 'Jane Doe',
288
+ 'customer_email': 'jane@example.com',
289
+ 'payment_terms': 'net_15',
290
+ 'cover_fee_required': True, # omit to inherit the merchant's invoice setting
291
+ 'lines': [
292
+ {'item_id': item.id, 'name': 'Consulting', 'quantity': 1, 'unit_price': 100},
293
+ ],
294
+ })
295
+
296
+ invoice.total # '100.00' — what the merchant is owed
297
+ invoice.cover_fee_quote.cc_total # '104.32' — charged if they pay by card
298
+ invoice.cover_fee_quote.ach_total # '101.26' — charged if they pay by bank
299
+ ```
300
+
301
+ The card figure is the higher of the two and is what the invoice and its emails lead with. A partial
302
+ payment re-quotes the fee against the partial amount, so treat the quote as "settling in full today"
303
+ rather than a fixed charge. `cover_fee_quote` is `None` when no fee is required.
304
+
305
+ To reconcile a payment, `amount` was credited to the invoice and `cover_fee` was charged on top:
306
+
307
+ ```python
308
+ payment = invoice.payments[0]
309
+ payment.amount # '100.00' — applied to the balance
310
+ payment.cover_fee # '4.32' — the fee the customer also paid
311
+ # The customer was charged amount + cover_fee.
312
+ ```
313
+
314
+ `pay()` behaves the same way: the fee for the `payment_type` you pass is added to `amount`, so the
315
+ card or bank account is debited more than the invoice is credited.
316
+
270
317
  ### Recurring invoices
271
318
 
272
- Templates that emit an invoice on a schedule.
319
+ Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
320
+ template generates.
273
321
 
274
322
  ```python
275
323
  ri = dime.recurring_invoices.create('000010', {
@@ -277,6 +325,7 @@ ri = dime.recurring_invoices.create('000010', {
277
325
  'payment_terms': 'net_30',
278
326
  'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
279
327
  'recurring_start_date': '2026-09-01',
328
+ 'cover_fee_required': True, # optional
280
329
  'lines': [
281
330
  {'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
282
331
  ],
@@ -174,6 +174,11 @@ item (a fund or designation). Draft invoices can be edited; once sent they are l
174
174
  Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
175
175
  identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
176
176
 
177
+ **Statuses.** `invoice.status` is one of `draft`, `sent`, `viewed`, `partially_paid`, `paid`, `void` or
178
+ `refunded`. `paid` is not always final: if the customer's bank returns an ACH payment, the invoice is
179
+ reopened (back to `partially_paid`, `viewed` or `sent`, with `amount_paid` and `balance` updated) and an
180
+ `invoice_payment_returned` webhook fires. Re-read the invoice rather than caching a `paid` status forever.
181
+
177
182
  ```python
178
183
  # Look up (or create) the merchant items a line can reference
179
184
  items = dime.invoices.list_items('000010')
@@ -223,9 +228,52 @@ dime.invoices.void('000010', invoice.id)
223
228
  dime.invoices.duplicate('000010', invoice.id)
224
229
  ```
225
230
 
231
+ #### Making the customer cover processing fees
232
+
233
+ Set `cover_fee_required` and the customer must pay the processing fee — it is not an optional
234
+ checkbox at checkout. The fee is **not** a line item and is **not** part of `total`: the merchant is
235
+ still owed `total`, and the fee is added on top of whatever the customer pays.
236
+
237
+ Card and ACH rates differ, so the charge depends on how the customer pays. `cover_fee_quote` gives
238
+ you both, quoted against the outstanding balance:
239
+
240
+ ```python
241
+ invoice = dime.invoices.create('000010', {
242
+ 'customer_uuid': customer.uuid,
243
+ 'customer_name': 'Jane Doe',
244
+ 'customer_email': 'jane@example.com',
245
+ 'payment_terms': 'net_15',
246
+ 'cover_fee_required': True, # omit to inherit the merchant's invoice setting
247
+ 'lines': [
248
+ {'item_id': item.id, 'name': 'Consulting', 'quantity': 1, 'unit_price': 100},
249
+ ],
250
+ })
251
+
252
+ invoice.total # '100.00' — what the merchant is owed
253
+ invoice.cover_fee_quote.cc_total # '104.32' — charged if they pay by card
254
+ invoice.cover_fee_quote.ach_total # '101.26' — charged if they pay by bank
255
+ ```
256
+
257
+ The card figure is the higher of the two and is what the invoice and its emails lead with. A partial
258
+ payment re-quotes the fee against the partial amount, so treat the quote as "settling in full today"
259
+ rather than a fixed charge. `cover_fee_quote` is `None` when no fee is required.
260
+
261
+ To reconcile a payment, `amount` was credited to the invoice and `cover_fee` was charged on top:
262
+
263
+ ```python
264
+ payment = invoice.payments[0]
265
+ payment.amount # '100.00' — applied to the balance
266
+ payment.cover_fee # '4.32' — the fee the customer also paid
267
+ # The customer was charged amount + cover_fee.
268
+ ```
269
+
270
+ `pay()` behaves the same way: the fee for the `payment_type` you pass is added to `amount`, so the
271
+ card or bank account is debited more than the invoice is credited.
272
+
226
273
  ### Recurring invoices
227
274
 
228
- Templates that emit an invoice on a schedule.
275
+ Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
276
+ template generates.
229
277
 
230
278
  ```python
231
279
  ri = dime.recurring_invoices.create('000010', {
@@ -233,6 +281,7 @@ ri = dime.recurring_invoices.create('000010', {
233
281
  'payment_terms': 'net_30',
234
282
  'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
235
283
  'recurring_start_date': '2026-09-01',
284
+ 'cover_fee_required': True, # optional
236
285
  'lines': [
237
286
  {'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
238
287
  ],
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dime-python-sdk"
7
- version = "1.2.0"
7
+ version = "1.3.1"
8
8
  description = "Python client for the Dime Payments API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
 
9
9
  class Config:
10
10
  DEFAULT_BASE_URL = 'https://app.dimepayments.com'
11
- VERSION = '1.2.0'
11
+ VERSION = '1.3.1'
12
12
 
13
13
  def __init__(
14
14
  self,
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from ..support.arr import arr_object, arr_string
5
+
6
+
7
+ @dataclass
8
+ class CoverFeeQuote:
9
+ """
10
+ The processing fee a cover-fee invoice adds on top of what the customer pays,
11
+ quoted for both payment methods.
12
+
13
+ Present on an :class:`Invoice` only when ``cover_fee_required`` is true. The fee
14
+ is NOT a line item and is NOT part of the invoice's ``total``: the merchant is
15
+ owed ``total``, and the customer is charged ``total`` plus this fee. Card and
16
+ ACH rates differ, so the amount depends on how the customer chooses to pay —
17
+ ``cc_total`` is the higher of the two and what the invoice and its emails lead
18
+ with.
19
+
20
+ ``basis`` names what the quote was computed against — currently always
21
+ ``balance``, the amount still outstanding. Paying a partial amount re-quotes the
22
+ fee against that amount, so treat these as a quote for settling in full today
23
+ rather than a fixed charge.
24
+
25
+ Money is kept as strings, consistent with the rest of the SDK, to avoid float
26
+ rounding.
27
+ """
28
+
29
+ basis: str | None = None
30
+ base: str | None = None
31
+ cc_fee: str | None = None
32
+ cc_total: str | None = None
33
+ ach_fee: str | None = None
34
+ ach_total: str | None = None
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: dict[str, Any]) -> 'CoverFeeQuote':
38
+ cc = arr_object(data, 'cc')
39
+ ach = arr_object(data, 'ach')
40
+
41
+ return cls(
42
+ basis=arr_string(data, 'basis'),
43
+ base=arr_string(data, 'base'),
44
+ cc_fee=arr_string(cc, 'fee'),
45
+ cc_total=arr_string(cc, 'total'),
46
+ ach_fee=arr_string(ach, 'fee'),
47
+ ach_total=arr_string(ach, 'total'),
48
+ )
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
2
2
  from typing import Any
3
3
 
4
4
  from ..support.arr import arr_array, arr_bool, arr_int, arr_object, arr_string
5
+ from .cover_fee_quote import CoverFeeQuote
5
6
  from .invoice_customer import InvoiceCustomer
6
7
  from .invoice_event import InvoiceEvent
7
8
  from .invoice_payment import InvoicePayment
@@ -15,6 +16,10 @@ class Invoice:
15
16
 
16
17
  Money fields arrive as dollar amounts and are kept as strings, consistent
17
18
  with the rest of the SDK, to avoid float rounding.
19
+
20
+ When ``cover_fee_required`` is set the customer must also pay the processing
21
+ fee, which is reported on :attr:`cover_fee_quote` rather than included in
22
+ ``total`` — so what settles is more than what the invoice says.
18
23
  """
19
24
 
20
25
  id: int | None = None
@@ -30,6 +35,8 @@ class Invoice:
30
35
  amount_paid: str | None = None
31
36
  balance: str | None = None
32
37
  allow_partial_payment: bool = False
38
+ cover_fee_required: bool = False
39
+ cover_fee_quote: CoverFeeQuote | None = None
33
40
  thank_you_note: str | None = None
34
41
  public_url: str | None = None
35
42
  customer: InvoiceCustomer = field(default_factory=InvoiceCustomer)
@@ -53,6 +60,12 @@ class Invoice:
53
60
  amount_paid=arr_string(data, 'amount_paid'),
54
61
  balance=arr_string(data, 'balance'),
55
62
  allow_partial_payment=arr_bool(data, 'allow_partial_payment'),
63
+ cover_fee_required=arr_bool(data, 'cover_fee_required'),
64
+ cover_fee_quote=(
65
+ CoverFeeQuote.from_dict(data['cover_fee_quote'])
66
+ if isinstance(data.get('cover_fee_quote'), dict)
67
+ else None
68
+ ),
56
69
  thank_you_note=arr_string(data, 'thank_you_note'),
57
70
  public_url=arr_string(data, 'public_url'),
58
71
  customer=InvoiceCustomer.from_dict(arr_object(data, 'customer')),
@@ -6,9 +6,17 @@ from ..support.arr import arr_int, arr_string
6
6
 
7
7
  @dataclass
8
8
  class InvoicePayment:
9
- """A payment recorded against an invoice."""
9
+ """
10
+ A payment recorded against an invoice.
11
+
12
+ ``amount`` is what was credited to the invoice; ``cover_fee`` is the processing
13
+ fee charged on top of it, so ``amount + cover_fee`` is what the customer
14
+ actually paid. It is zero unless the invoice required the customer to cover
15
+ fees.
16
+ """
10
17
 
11
18
  amount: str | None = None
19
+ cover_fee: str | None = None
12
20
  paid_at: str | None = None
13
21
  method: str | None = None
14
22
  transaction_id: int | None = None
@@ -17,6 +25,7 @@ class InvoicePayment:
17
25
  def from_dict(cls, data: dict[str, Any]) -> 'InvoicePayment':
18
26
  return cls(
19
27
  amount=arr_string(data, 'amount'),
28
+ cover_fee=arr_string(data, 'cover_fee'),
20
29
  paid_at=arr_string(data, 'paid_at'),
21
30
  method=arr_string(data, 'method'),
22
31
  transaction_id=arr_int(data, 'transaction_id'),
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass, field
2
2
  from typing import Any
3
3
 
4
- from ..support.arr import arr_array, arr_int, arr_object, arr_string
4
+ from ..support.arr import arr_array, arr_bool, arr_int, arr_object, arr_string
5
5
  from .invoice_customer import InvoiceCustomer
6
6
  from .line_item import LineItem
7
7
 
@@ -38,6 +38,8 @@ class RecurringInvoice:
38
38
  status: str | None = None
39
39
  recurrence_schedule: str | None = None
40
40
  payment_terms: str | None = None
41
+ # Copied onto every invoice this template generates.
42
+ cover_fee_required: bool = False
41
43
  start_date: str | None = None
42
44
  end_date: str | None = None
43
45
  next_run_date: str | None = None
@@ -55,6 +57,7 @@ class RecurringInvoice:
55
57
  status=arr_string(data, 'status'),
56
58
  recurrence_schedule=arr_string(data, 'recurrence_schedule'),
57
59
  payment_terms=arr_string(data, 'payment_terms'),
60
+ cover_fee_required=arr_bool(data, 'cover_fee_required'),
58
61
  start_date=arr_string(data, 'start_date'),
59
62
  end_date=arr_string(data, 'end_date'),
60
63
  next_run_date=arr_string(data, 'next_run_date'),
@@ -11,7 +11,7 @@ from .error_handler import ErrorHandler
11
11
  if TYPE_CHECKING:
12
12
  from ..config import Config
13
13
 
14
- SDK_VERSION = '1.2.0'
14
+ SDK_VERSION = '1.3.1'
15
15
 
16
16
 
17
17
  class Transport:
@@ -25,6 +25,12 @@ class Invoices(AbstractResource):
25
25
  """
26
26
 
27
27
  def list(self, sid: str, filters: dict[str, Any] | None = None) -> CursorPage[Invoice]:
28
+ """
29
+ List invoices for a merchant.
30
+
31
+ ``filters['status']`` is one of draft, sent, viewed, partially_paid, paid, void,
32
+ refunded, overdue or all.
33
+ """
28
34
  body = self._envelope({'sid': sid}, filters or {})
29
35
  return self._paginate('GET', 'invoices', body, Invoice.from_dict)
30
36
 
@@ -39,6 +45,12 @@ class Invoices(AbstractResource):
39
45
  ``customer_name``, ``customer_email``, ``payment_terms``
40
46
  (``due_on_receipt`` | ``net_15`` | ``net_30`` | ``net_60``) and at least
41
47
  one entry in ``lines``, each referencing a Merchant ``item_id``.
48
+
49
+ Pass ``cover_fee_required`` to make the customer pay the processing fee. The
50
+ fee is added on top of the invoice at payment time rather than becoming a
51
+ line item, so ``total`` stays the amount owed to the merchant — read
52
+ :attr:`Invoice.cover_fee_quote` for what the customer will actually be
53
+ charged. Omit it to inherit the Merchant's invoice setting.
42
54
  """
43
55
  body = self._envelope({'sid': sid} | attributes)
44
56
  raw = self._transport.request('POST', 'invoice/create', body)
@@ -86,6 +98,12 @@ class Invoices(AbstractResource):
86
98
  ``expiration_date``; for ACH, pass ``routing_number`` /
87
99
  ``account_number`` / ``account_type`` / ``account_name``. Omit ``amount``
88
100
  to pay the full balance.
101
+
102
+ On a cover-fee invoice the processing fee for ``payment_type`` is charged on
103
+ top of ``amount``, so the card or bank account is debited more than the
104
+ invoice is credited. The fee lands as ``cover_fee`` on the matching entry in
105
+ :attr:`Invoice.payments`. Card and ACH rates differ, so the same ``amount``
106
+ settles differently per ``payment_type``.
89
107
  """
90
108
  body = self._envelope({'sid': sid, 'invoice_id': invoice_id} | attributes)
91
109
  raw = self._transport.request('POST', 'invoice/pay', body)
@@ -18,6 +18,13 @@ class RecurringInvoices(AbstractResource):
18
18
  return RecurringInvoice.from_dict(raw.get('data') or {})
19
19
 
20
20
  def create(self, sid: str, attributes: dict[str, Any]) -> RecurringInvoice:
21
+ """
22
+ Create a recurring-invoice template. When ``recurring_start_date`` is today
23
+ the first invoice is generated and sent immediately.
24
+
25
+ ``cover_fee_required`` makes the customer cover the processing fee on every
26
+ invoice this template generates.
27
+ """
21
28
  body = self._envelope({'sid': sid} | attributes)
22
29
  raw = self._transport.request('POST', 'recurring-invoice/create', body)
23
30
  return RecurringInvoice.from_dict(raw.get('data') or {})
@@ -254,3 +254,105 @@ def test_delete_line_item_sends_both_ids_and_returns_invoice():
254
254
  assert data['invoice_id'] == 7
255
255
  assert data['line_item_id'] == 1
256
256
  assert invoice.id == 7
257
+
258
+
259
+ # --- Required cover fees -----------------------------------------------------
260
+ #
261
+ # The fee is quoted per payment method against the balance and is deliberately
262
+ # absent from `total`, which stays the amount owed to the merchant.
263
+
264
+ COVER_FEE_BODY = {
265
+ **INVOICE_BODY,
266
+ 'subtotal': 100.0,
267
+ 'total': 100.0,
268
+ 'balance': 100.0,
269
+ 'cover_fee_required': True,
270
+ 'cover_fee_quote': {
271
+ 'basis': 'balance',
272
+ 'base': 100.0,
273
+ 'cc': {'fee': 4.32, 'total': 104.32},
274
+ 'ach': {'fee': 1.26, 'total': 101.26},
275
+ },
276
+ 'payments': [
277
+ {
278
+ 'amount': 100.0,
279
+ 'cover_fee': 4.32,
280
+ 'paid_at': '2026-08-06T10:00:00+00:00',
281
+ 'method': '+CC',
282
+ 'transaction_id': 91,
283
+ },
284
+ ],
285
+ }
286
+
287
+
288
+ def test_create_sends_cover_fee_required():
289
+ client, mock = fake_client([{'status': 201, 'body': {'data': INVOICE_BODY}}])
290
+ client.invoices.create(
291
+ '000010',
292
+ {
293
+ 'customer_uuid': 'cus-uuid',
294
+ 'customer_name': 'Jane Doe',
295
+ 'customer_email': 'jane@example.com',
296
+ 'payment_terms': 'net_15',
297
+ 'cover_fee_required': True,
298
+ 'lines': [{'item_id': 5, 'name': 'Consulting', 'quantity': 1, 'unit_price': 100}],
299
+ },
300
+ )
301
+ assert sent_body(mock)['data']['cover_fee_required'] is True
302
+
303
+
304
+ def test_recurring_create_sends_cover_fee_required():
305
+ client, mock = fake_client([{'status': 201, 'body': {'data': {'id': 3, 'status': 'Active'}}}])
306
+ client.recurring_invoices.create(
307
+ '000010',
308
+ {
309
+ 'customer_uuid': 'cus-uuid',
310
+ 'payment_terms': 'net_30',
311
+ 'cover_fee_required': True,
312
+ 'recurring_frequency': 'Monthly',
313
+ 'recurring_start_date': '2026-09-01',
314
+ 'lines': [{'item_id': 5, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500}],
315
+ },
316
+ )
317
+ assert sent_body(mock)['data']['cover_fee_required'] is True
318
+
319
+
320
+ def test_show_parses_the_per_method_quote_and_keeps_it_out_of_total():
321
+ client, _ = fake_client([{'status': 200, 'body': {'data': COVER_FEE_BODY}}])
322
+ invoice = client.invoices.show('000010', 7)
323
+
324
+ assert invoice.cover_fee_required is True
325
+ assert invoice.cover_fee_quote is not None
326
+ assert invoice.cover_fee_quote.basis == 'balance'
327
+ assert invoice.cover_fee_quote.cc_fee == '4.32'
328
+ assert invoice.cover_fee_quote.cc_total == '104.32'
329
+ assert invoice.cover_fee_quote.ach_fee == '1.26'
330
+ assert invoice.cover_fee_quote.ach_total == '101.26'
331
+ # The merchant is still owed the invoice amount; the fee sits on top of it.
332
+ assert invoice.total == '100.0'
333
+
334
+
335
+ def test_payment_reports_the_fee_charged_alongside_the_amount_credited():
336
+ client, _ = fake_client([{'status': 200, 'body': {'data': COVER_FEE_BODY}}])
337
+ payment = client.invoices.show('000010', 7).payments[0]
338
+
339
+ # amount + cover_fee is what the customer was actually charged.
340
+ assert payment.amount == '100.0'
341
+ assert payment.cover_fee == '4.32'
342
+
343
+
344
+ def test_quote_is_none_when_no_fee_is_required():
345
+ client, _ = fake_client([invoice_response()])
346
+ invoice = client.invoices.show('000010', 7)
347
+
348
+ assert invoice.cover_fee_required is False
349
+ assert invoice.cover_fee_quote is None
350
+
351
+
352
+ def test_list_reads_the_cover_fee_flag():
353
+ client, _ = fake_client(
354
+ [{'status': 200, 'body': {'data': [{**INVOICE_BODY, 'cover_fee_required': True}], 'meta': {}}}]
355
+ )
356
+ page = client.invoices.list('000010')
357
+
358
+ assert page.data[0].cover_fee_required is True
File without changes