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.
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/PKG-INFO +46 -2
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/README.md +45 -1
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/pyproject.toml +1 -1
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/config.py +1 -1
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/cover_fee_quote.py +48 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice.py +13 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_payment.py +10 -1
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_invoice.py +4 -1
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/transport.py +1 -1
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/invoices.py +12 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_invoices.py +7 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_invoices.py +102 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/.github/workflows/publish.yml +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/.gitignore +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/LICENSE +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/client.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/address.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/customer.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_group.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_with_transactions.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/form_link.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_customer.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_event.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_item.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_link.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/line_item.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/merchant.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/message_result.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/payment_method.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment_method.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/tokenize_result.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction_address.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/api_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/authentication_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/connection_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/dime_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/not_found_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/permission_denied_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/rate_limit_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/server_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/validation_exception.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/error_handler.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/cursor_page.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/abstract_resource.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/addresses.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/customers.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/deposits.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/merchants.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/payment_methods.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_payments.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/transactions.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/arr.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/helpers.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/__init__.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_addresses.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_customers.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_deposits.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_error_handling.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_merchants.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_pagination.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_payment_methods.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_recurring_invoices.py +0 -0
- {dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/tests/unit/test_recurring_payments.py +0 -0
- {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.
|
|
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
|
],
|
|
@@ -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')),
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_payment.py
RENAMED
|
@@ -6,9 +6,17 @@ from ..support.arr import arr_int, arr_string
|
|
|
6
6
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class InvoicePayment:
|
|
9
|
-
"""
|
|
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'),
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_invoice.py
RENAMED
|
@@ -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'),
|
|
@@ -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)
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_invoices.py
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_group.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_customer.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_event.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_item.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/invoice_link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/message_result.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/payment_method.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment.py
RENAMED
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/tokenize_result.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/api_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/connection_exception.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/dime_exception.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/not_found_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/rate_limit_exception.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/server_exception.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/validation_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/abstract_resource.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/payment_methods.py
RENAMED
|
File without changes
|
{dime_python_sdk-1.2.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_payments.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|