dime-python-sdk 1.2.0__tar.gz → 1.3.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.
Files changed (75) hide show
  1. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/PKG-INFO +46 -2
  2. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/README.md +45 -1
  3. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/pyproject.toml +1 -1
  4. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/config.py +1 -1
  5. dime_python_sdk-1.3.0/src/dime_payments/data_objects/cover_fee_quote.py +48 -0
  6. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice.py +13 -0
  7. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_payment.py +10 -1
  8. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_invoice.py +4 -1
  9. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/transport.py +1 -1
  10. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/invoices.py +12 -0
  11. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_invoices.py +7 -0
  12. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_invoices.py +102 -0
  13. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/.github/workflows/publish.yml +0 -0
  14. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/.gitignore +0 -0
  15. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/LICENSE +0 -0
  16. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/__init__.py +0 -0
  17. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/client.py +0 -0
  18. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/__init__.py +0 -0
  19. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/address.py +0 -0
  20. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/customer.py +0 -0
  21. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit.py +0 -0
  22. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_group.py +0 -0
  23. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_with_transactions.py +0 -0
  24. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/form_link.py +0 -0
  25. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_customer.py +0 -0
  26. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_event.py +0 -0
  27. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_item.py +0 -0
  28. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_link.py +0 -0
  29. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/line_item.py +0 -0
  30. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/merchant.py +0 -0
  31. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/message_result.py +0 -0
  32. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/payment_method.py +0 -0
  33. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment.py +0 -0
  34. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment_method.py +0 -0
  35. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/tokenize_result.py +0 -0
  36. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction.py +0 -0
  37. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction_address.py +0 -0
  38. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/__init__.py +0 -0
  39. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/api_exception.py +0 -0
  40. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/authentication_exception.py +0 -0
  41. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/connection_exception.py +0 -0
  42. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/dime_exception.py +0 -0
  43. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/not_found_exception.py +0 -0
  44. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/permission_denied_exception.py +0 -0
  45. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/rate_limit_exception.py +0 -0
  46. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/server_exception.py +0 -0
  47. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/validation_exception.py +0 -0
  48. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/__init__.py +0 -0
  49. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/error_handler.py +0 -0
  50. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/__init__.py +0 -0
  51. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/cursor_page.py +0 -0
  52. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/__init__.py +0 -0
  53. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/abstract_resource.py +0 -0
  54. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/addresses.py +0 -0
  55. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/customers.py +0 -0
  56. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/deposits.py +0 -0
  57. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/merchants.py +0 -0
  58. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/payment_methods.py +0 -0
  59. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_payments.py +0 -0
  60. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/transactions.py +0 -0
  61. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/__init__.py +0 -0
  62. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/arr.py +0 -0
  63. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/__init__.py +0 -0
  64. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/helpers.py +0 -0
  65. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/__init__.py +0 -0
  66. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_addresses.py +0 -0
  67. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_customers.py +0 -0
  68. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_deposits.py +0 -0
  69. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_error_handling.py +0 -0
  70. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_merchants.py +0 -0
  71. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_pagination.py +0 -0
  72. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_payment_methods.py +0 -0
  73. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_recurring_invoices.py +0 -0
  74. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_recurring_payments.py +0 -0
  75. {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_transactions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dime-python-sdk
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Python client for the Dime Payments API
5
5
  Author: Dime Technology
6
6
  License: MIT License
@@ -267,9 +267,52 @@ dime.invoices.void('000010', invoice.id)
267
267
  dime.invoices.duplicate('000010', invoice.id)
268
268
  ```
269
269
 
270
+ #### Making the customer cover processing fees
271
+
272
+ Set `cover_fee_required` and the customer must pay the processing fee — it is not an optional
273
+ checkbox at checkout. The fee is **not** a line item and is **not** part of `total`: the merchant is
274
+ still owed `total`, and the fee is added on top of whatever the customer pays.
275
+
276
+ Card and ACH rates differ, so the charge depends on how the customer pays. `cover_fee_quote` gives
277
+ you both, quoted against the outstanding balance:
278
+
279
+ ```python
280
+ invoice = dime.invoices.create('000010', {
281
+ 'customer_uuid': customer.uuid,
282
+ 'customer_name': 'Jane Doe',
283
+ 'customer_email': 'jane@example.com',
284
+ 'payment_terms': 'net_15',
285
+ 'cover_fee_required': True, # omit to inherit the merchant's invoice setting
286
+ 'lines': [
287
+ {'item_id': item.id, 'name': 'Consulting', 'quantity': 1, 'unit_price': 100},
288
+ ],
289
+ })
290
+
291
+ invoice.total # '100.00' — what the merchant is owed
292
+ invoice.cover_fee_quote.cc_total # '104.32' — charged if they pay by card
293
+ invoice.cover_fee_quote.ach_total # '101.26' — charged if they pay by bank
294
+ ```
295
+
296
+ The card figure is the higher of the two and is what the invoice and its emails lead with. A partial
297
+ payment re-quotes the fee against the partial amount, so treat the quote as "settling in full today"
298
+ rather than a fixed charge. `cover_fee_quote` is `None` when no fee is required.
299
+
300
+ To reconcile a payment, `amount` was credited to the invoice and `cover_fee` was charged on top:
301
+
302
+ ```python
303
+ payment = invoice.payments[0]
304
+ payment.amount # '100.00' — applied to the balance
305
+ payment.cover_fee # '4.32' — the fee the customer also paid
306
+ # The customer was charged amount + cover_fee.
307
+ ```
308
+
309
+ `pay()` behaves the same way: the fee for the `payment_type` you pass is added to `amount`, so the
310
+ card or bank account is debited more than the invoice is credited.
311
+
270
312
  ### Recurring invoices
271
313
 
272
- Templates that emit an invoice on a schedule.
314
+ Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
315
+ template generates.
273
316
 
274
317
  ```python
275
318
  ri = dime.recurring_invoices.create('000010', {
@@ -277,6 +320,7 @@ ri = dime.recurring_invoices.create('000010', {
277
320
  'payment_terms': 'net_30',
278
321
  'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
279
322
  'recurring_start_date': '2026-09-01',
323
+ 'cover_fee_required': True, # optional
280
324
  'lines': [
281
325
  {'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
282
326
  ],
@@ -223,9 +223,52 @@ dime.invoices.void('000010', invoice.id)
223
223
  dime.invoices.duplicate('000010', invoice.id)
224
224
  ```
225
225
 
226
+ #### Making the customer cover processing fees
227
+
228
+ Set `cover_fee_required` and the customer must pay the processing fee — it is not an optional
229
+ checkbox at checkout. The fee is **not** a line item and is **not** part of `total`: the merchant is
230
+ still owed `total`, and the fee is added on top of whatever the customer pays.
231
+
232
+ Card and ACH rates differ, so the charge depends on how the customer pays. `cover_fee_quote` gives
233
+ you both, quoted against the outstanding balance:
234
+
235
+ ```python
236
+ invoice = dime.invoices.create('000010', {
237
+ 'customer_uuid': customer.uuid,
238
+ 'customer_name': 'Jane Doe',
239
+ 'customer_email': 'jane@example.com',
240
+ 'payment_terms': 'net_15',
241
+ 'cover_fee_required': True, # omit to inherit the merchant's invoice setting
242
+ 'lines': [
243
+ {'item_id': item.id, 'name': 'Consulting', 'quantity': 1, 'unit_price': 100},
244
+ ],
245
+ })
246
+
247
+ invoice.total # '100.00' — what the merchant is owed
248
+ invoice.cover_fee_quote.cc_total # '104.32' — charged if they pay by card
249
+ invoice.cover_fee_quote.ach_total # '101.26' — charged if they pay by bank
250
+ ```
251
+
252
+ The card figure is the higher of the two and is what the invoice and its emails lead with. A partial
253
+ payment re-quotes the fee against the partial amount, so treat the quote as "settling in full today"
254
+ rather than a fixed charge. `cover_fee_quote` is `None` when no fee is required.
255
+
256
+ To reconcile a payment, `amount` was credited to the invoice and `cover_fee` was charged on top:
257
+
258
+ ```python
259
+ payment = invoice.payments[0]
260
+ payment.amount # '100.00' — applied to the balance
261
+ payment.cover_fee # '4.32' — the fee the customer also paid
262
+ # The customer was charged amount + cover_fee.
263
+ ```
264
+
265
+ `pay()` behaves the same way: the fee for the `payment_type` you pass is added to `amount`, so the
266
+ card or bank account is debited more than the invoice is credited.
267
+
226
268
  ### Recurring invoices
227
269
 
228
- Templates that emit an invoice on a schedule.
270
+ Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
271
+ template generates.
229
272
 
230
273
  ```python
231
274
  ri = dime.recurring_invoices.create('000010', {
@@ -233,6 +276,7 @@ ri = dime.recurring_invoices.create('000010', {
233
276
  'payment_terms': 'net_30',
234
277
  'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
235
278
  'recurring_start_date': '2026-09-01',
279
+ 'cover_fee_required': True, # optional
236
280
  'lines': [
237
281
  {'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
238
282
  ],
@@ -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.0"
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.0'
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.0'
15
15
 
16
16
 
17
17
  class Transport:
@@ -39,6 +39,12 @@ class Invoices(AbstractResource):
39
39
  ``customer_name``, ``customer_email``, ``payment_terms``
40
40
  (``due_on_receipt`` | ``net_15`` | ``net_30`` | ``net_60``) and at least
41
41
  one entry in ``lines``, each referencing a Merchant ``item_id``.
42
+
43
+ Pass ``cover_fee_required`` to make the customer pay the processing fee. The
44
+ fee is added on top of the invoice at payment time rather than becoming a
45
+ line item, so ``total`` stays the amount owed to the merchant — read
46
+ :attr:`Invoice.cover_fee_quote` for what the customer will actually be
47
+ charged. Omit it to inherit the Merchant's invoice setting.
42
48
  """
43
49
  body = self._envelope({'sid': sid} | attributes)
44
50
  raw = self._transport.request('POST', 'invoice/create', body)
@@ -86,6 +92,12 @@ class Invoices(AbstractResource):
86
92
  ``expiration_date``; for ACH, pass ``routing_number`` /
87
93
  ``account_number`` / ``account_type`` / ``account_name``. Omit ``amount``
88
94
  to pay the full balance.
95
+
96
+ On a cover-fee invoice the processing fee for ``payment_type`` is charged on
97
+ top of ``amount``, so the card or bank account is debited more than the
98
+ invoice is credited. The fee lands as ``cover_fee`` on the matching entry in
99
+ :attr:`Invoice.payments`. Card and ACH rates differ, so the same ``amount``
100
+ settles differently per ``payment_type``.
89
101
  """
90
102
  body = self._envelope({'sid': sid, 'invoice_id': invoice_id} | attributes)
91
103
  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