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