dime-python-sdk 1.0.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.3.0/.github/workflows/publish.yml +62 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/PKG-INFO +127 -2
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/README.md +126 -1
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/pyproject.toml +1 -1
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/client.py +4 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/config.py +1 -1
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/__init__.py +17 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/cover_fee_quote.py +48 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice.py +75 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice_customer.py +21 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice_event.py +23 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice_item.py +29 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice_link.py +19 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/invoice_payment.py +32 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/line_item.py +33 -0
- dime_python_sdk-1.3.0/src/dime_payments/data_objects/recurring_invoice.py +70 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/transport.py +1 -1
- dime_python_sdk-1.3.0/src/dime_payments/resources/invoices.py +156 -0
- dime_python_sdk-1.3.0/src/dime_payments/resources/recurring_invoices.py +35 -0
- dime_python_sdk-1.3.0/tests/unit/test_invoices.py +358 -0
- dime_python_sdk-1.3.0/tests/unit/test_recurring_invoices.py +83 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/.gitignore +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/LICENSE +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/address.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/customer.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_group.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/deposit_with_transactions.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/form_link.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/merchant.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/message_result.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/payment_method.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/recurring_payment_method.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/tokenize_result.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/data_objects/transaction_address.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/api_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/authentication_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/connection_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/dime_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/not_found_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/permission_denied_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/rate_limit_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/server_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/exceptions/validation_exception.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/http/error_handler.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/pagination/cursor_page.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/abstract_resource.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/addresses.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/customers.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/deposits.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/merchants.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/payment_methods.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/recurring_payments.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/resources/transactions.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/src/dime_payments/support/arr.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/helpers.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/__init__.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_addresses.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_customers.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_deposits.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_error_handling.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_merchants.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_pagination.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_payment_methods.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_recurring_payments.py +0 -0
- {dime_python_sdk-1.0.0 → dime_python_sdk-1.3.0}/tests/unit/test_transactions.py +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Publishing is driven by GitHub Releases: tag, write the release notes, hit
|
|
4
|
+
# publish, and this ships the tag to PyPI. workflow_dispatch is the escape hatch
|
|
5
|
+
# for republishing a tag whose release already exists — select the tag as the
|
|
6
|
+
# ref when dispatching.
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
publish:
|
|
17
|
+
name: Publish to PyPI
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
environment: pypi
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
id-token: write # required for PyPI trusted publishing
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: '3.12'
|
|
30
|
+
|
|
31
|
+
# The SDK carries its version in three places and they drift silently.
|
|
32
|
+
# v1.0.0 sat on PyPI for ten weeks while main had moved on, so fail loudly
|
|
33
|
+
# rather than publish a package that misreports itself in X-Dime-Sdk.
|
|
34
|
+
- name: Verify the tag matches every declared version
|
|
35
|
+
env:
|
|
36
|
+
TAG_NAME: ${{ github.event.release.tag_name || github.ref_name }}
|
|
37
|
+
run: |
|
|
38
|
+
tag="${TAG_NAME#v}"
|
|
39
|
+
proj=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
40
|
+
cfg=$(python -c "import re; print(re.search(r\"VERSION = '([^']+)'\", open('src/dime_payments/config.py').read()).group(1))")
|
|
41
|
+
trn=$(python -c "import re; print(re.search(r\"SDK_VERSION = '([^']+)'\", open('src/dime_payments/http/transport.py').read()).group(1))")
|
|
42
|
+
echo "tag=$tag pyproject.toml=$proj config.py=$cfg transport.py=$trn"
|
|
43
|
+
[ "$tag" = "$proj" ] || { echo "::error::pyproject.toml ($proj) does not match tag ($tag)"; exit 1; }
|
|
44
|
+
[ "$tag" = "$cfg" ] || { echo "::error::config.py ($cfg) does not match tag ($tag)"; exit 1; }
|
|
45
|
+
[ "$tag" = "$trn" ] || { echo "::error::transport.py ($trn) does not match tag ($tag)"; exit 1; }
|
|
46
|
+
|
|
47
|
+
- name: Install and test
|
|
48
|
+
run: |
|
|
49
|
+
python -m pip install --upgrade pip
|
|
50
|
+
pip install -e ".[dev]"
|
|
51
|
+
pytest
|
|
52
|
+
|
|
53
|
+
- name: Build distributions
|
|
54
|
+
run: |
|
|
55
|
+
pip install build
|
|
56
|
+
python -m build
|
|
57
|
+
|
|
58
|
+
# Trusted publishing — authenticates via OIDC, so there is no PyPI token
|
|
59
|
+
# to store or rotate. Requires a trusted publisher configured on PyPI for
|
|
60
|
+
# this repo, this workflow filename, and the "pypi" environment.
|
|
61
|
+
- name: Publish
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -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
|
|
@@ -118,6 +118,8 @@ returned as strings to avoid float rounding.
|
|
|
118
118
|
| `dime.addresses` | list, show, create, update, delete |
|
|
119
119
|
| `dime.deposits` | list, list_with_transactions, show |
|
|
120
120
|
| `dime.recurring_payments` | list, show, create, edit, pause, cancel, activate, delete |
|
|
121
|
+
| `dime.invoices` | list, show, create, update, delete, send, mark_sent, void, duplicate, pay, get_link, list_items, add_line_item, update_line_item, delete_line_item, create_item |
|
|
122
|
+
| `dime.recurring_invoices` | list, show, create, cancel |
|
|
121
123
|
|
|
122
124
|
### Transactions
|
|
123
125
|
|
|
@@ -208,6 +210,127 @@ dime.recurring_payments.activate('000010', rp.id)
|
|
|
208
210
|
dime.recurring_payments.cancel('000010', rp.id)
|
|
209
211
|
```
|
|
210
212
|
|
|
213
|
+
### Invoices
|
|
214
|
+
|
|
215
|
+
Invoices are scoped to a merchant `sid` and built from line items that each reference a merchant
|
|
216
|
+
item (a fund or designation). Draft invoices can be edited; once sent they are locked.
|
|
217
|
+
|
|
218
|
+
Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
|
|
219
|
+
identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# Look up (or create) the merchant items a line can reference
|
|
223
|
+
items = dime.invoices.list_items('000010')
|
|
224
|
+
item = dime.invoices.create_item('000010', {
|
|
225
|
+
'name': 'Consulting',
|
|
226
|
+
'description': 'Professional services',
|
|
227
|
+
'price': 125,
|
|
228
|
+
'tax_deductible': False,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
# Create a draft invoice with one or more line items
|
|
232
|
+
invoice = dime.invoices.create('000010', {
|
|
233
|
+
'customer_uuid': customer.uuid,
|
|
234
|
+
'customer_name': 'Jane Doe',
|
|
235
|
+
'customer_email': 'jane@example.com',
|
|
236
|
+
'payment_terms': 'net_15', # due_on_receipt | net_15 | net_30 | net_60
|
|
237
|
+
'lines': [
|
|
238
|
+
{'item_id': item.id, 'name': 'Consulting', 'description': '2 hours',
|
|
239
|
+
'quantity': 2, 'unit_price': 125},
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
# Line-item edits return the refreshed invoice, with totals recalculated
|
|
244
|
+
invoice = dime.invoices.add_line_item('000010', invoice.id, {
|
|
245
|
+
'item_id': item.id, 'name': 'Setup', 'quantity': 1, 'unit_price': 50,
|
|
246
|
+
})
|
|
247
|
+
invoice = dime.invoices.update_line_item('000010', invoice.id, invoice.items[0].id, {'quantity': 3})
|
|
248
|
+
invoice = dime.invoices.delete_line_item('000010', invoice.id, invoice.items[0].id)
|
|
249
|
+
|
|
250
|
+
# Email it to the customer, or activate the pay link without emailing
|
|
251
|
+
dime.invoices.send('000010', invoice.id)
|
|
252
|
+
dime.invoices.mark_sent('000010', invoice.id)
|
|
253
|
+
|
|
254
|
+
# Share the public pay link
|
|
255
|
+
link = dime.invoices.get_link('000010', invoice.id)
|
|
256
|
+
print(link.public_url)
|
|
257
|
+
|
|
258
|
+
# Take a merchant-initiated payment. payment_type is required; omit amount to
|
|
259
|
+
# pay the full balance.
|
|
260
|
+
dime.invoices.pay('000010', invoice.id, {
|
|
261
|
+
'payment_type': 'cc', # cc | ach
|
|
262
|
+
'token': pm.token,
|
|
263
|
+
'amount': 125,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
dime.invoices.void('000010', invoice.id)
|
|
267
|
+
dime.invoices.duplicate('000010', invoice.id)
|
|
268
|
+
```
|
|
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
|
+
|
|
312
|
+
### Recurring invoices
|
|
313
|
+
|
|
314
|
+
Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
|
|
315
|
+
template generates.
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
ri = dime.recurring_invoices.create('000010', {
|
|
319
|
+
'customer_uuid': customer.uuid,
|
|
320
|
+
'payment_terms': 'net_30',
|
|
321
|
+
'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
|
|
322
|
+
'recurring_start_date': '2026-09-01',
|
|
323
|
+
'cover_fee_required': True, # optional
|
|
324
|
+
'lines': [
|
|
325
|
+
{'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
|
|
326
|
+
],
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
print(ri.next_run_date, ri.upcoming_run_dates)
|
|
330
|
+
|
|
331
|
+
dime.recurring_invoices.cancel('000010', ri.id)
|
|
332
|
+
```
|
|
333
|
+
|
|
211
334
|
## Pagination
|
|
212
335
|
|
|
213
336
|
List endpoints return a `CursorPage`. Iterate one page, walk pages manually, or stream every
|
|
@@ -271,7 +394,9 @@ except DimeException as e:
|
|
|
271
394
|
## Notes
|
|
272
395
|
|
|
273
396
|
- **GET requests carry a JSON body.** The Dime API expects read parameters in the request body
|
|
274
|
-
even for `GET` endpoints; the SDK handles this transparently.
|
|
397
|
+
even for `GET` endpoints; the SDK handles this transparently. Point `base_url` at an `https://`
|
|
398
|
+
origin — an `http://` URL that 301-redirects to `https` will have its request body dropped by the
|
|
399
|
+
redirect, which surfaces as a `403` "You do not have access to this company." from the API.
|
|
275
400
|
- **No API versioning.** Endpoints live under `/api` with no version prefix.
|
|
276
401
|
|
|
277
402
|
## Development
|
|
@@ -74,6 +74,8 @@ returned as strings to avoid float rounding.
|
|
|
74
74
|
| `dime.addresses` | list, show, create, update, delete |
|
|
75
75
|
| `dime.deposits` | list, list_with_transactions, show |
|
|
76
76
|
| `dime.recurring_payments` | list, show, create, edit, pause, cancel, activate, delete |
|
|
77
|
+
| `dime.invoices` | list, show, create, update, delete, send, mark_sent, void, duplicate, pay, get_link, list_items, add_line_item, update_line_item, delete_line_item, create_item |
|
|
78
|
+
| `dime.recurring_invoices` | list, show, create, cancel |
|
|
77
79
|
|
|
78
80
|
### Transactions
|
|
79
81
|
|
|
@@ -164,6 +166,127 @@ dime.recurring_payments.activate('000010', rp.id)
|
|
|
164
166
|
dime.recurring_payments.cancel('000010', rp.id)
|
|
165
167
|
```
|
|
166
168
|
|
|
169
|
+
### Invoices
|
|
170
|
+
|
|
171
|
+
Invoices are scoped to a merchant `sid` and built from line items that each reference a merchant
|
|
172
|
+
item (a fund or designation). Draft invoices can be edited; once sent they are locked.
|
|
173
|
+
|
|
174
|
+
Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
|
|
175
|
+
identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# Look up (or create) the merchant items a line can reference
|
|
179
|
+
items = dime.invoices.list_items('000010')
|
|
180
|
+
item = dime.invoices.create_item('000010', {
|
|
181
|
+
'name': 'Consulting',
|
|
182
|
+
'description': 'Professional services',
|
|
183
|
+
'price': 125,
|
|
184
|
+
'tax_deductible': False,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
# Create a draft invoice with one or more line items
|
|
188
|
+
invoice = dime.invoices.create('000010', {
|
|
189
|
+
'customer_uuid': customer.uuid,
|
|
190
|
+
'customer_name': 'Jane Doe',
|
|
191
|
+
'customer_email': 'jane@example.com',
|
|
192
|
+
'payment_terms': 'net_15', # due_on_receipt | net_15 | net_30 | net_60
|
|
193
|
+
'lines': [
|
|
194
|
+
{'item_id': item.id, 'name': 'Consulting', 'description': '2 hours',
|
|
195
|
+
'quantity': 2, 'unit_price': 125},
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
# Line-item edits return the refreshed invoice, with totals recalculated
|
|
200
|
+
invoice = dime.invoices.add_line_item('000010', invoice.id, {
|
|
201
|
+
'item_id': item.id, 'name': 'Setup', 'quantity': 1, 'unit_price': 50,
|
|
202
|
+
})
|
|
203
|
+
invoice = dime.invoices.update_line_item('000010', invoice.id, invoice.items[0].id, {'quantity': 3})
|
|
204
|
+
invoice = dime.invoices.delete_line_item('000010', invoice.id, invoice.items[0].id)
|
|
205
|
+
|
|
206
|
+
# Email it to the customer, or activate the pay link without emailing
|
|
207
|
+
dime.invoices.send('000010', invoice.id)
|
|
208
|
+
dime.invoices.mark_sent('000010', invoice.id)
|
|
209
|
+
|
|
210
|
+
# Share the public pay link
|
|
211
|
+
link = dime.invoices.get_link('000010', invoice.id)
|
|
212
|
+
print(link.public_url)
|
|
213
|
+
|
|
214
|
+
# Take a merchant-initiated payment. payment_type is required; omit amount to
|
|
215
|
+
# pay the full balance.
|
|
216
|
+
dime.invoices.pay('000010', invoice.id, {
|
|
217
|
+
'payment_type': 'cc', # cc | ach
|
|
218
|
+
'token': pm.token,
|
|
219
|
+
'amount': 125,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
dime.invoices.void('000010', invoice.id)
|
|
223
|
+
dime.invoices.duplicate('000010', invoice.id)
|
|
224
|
+
```
|
|
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
|
+
|
|
268
|
+
### Recurring invoices
|
|
269
|
+
|
|
270
|
+
Templates that emit an invoice on a schedule. `cover_fee_required` is copied onto every invoice a
|
|
271
|
+
template generates.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
ri = dime.recurring_invoices.create('000010', {
|
|
275
|
+
'customer_uuid': customer.uuid,
|
|
276
|
+
'payment_terms': 'net_30',
|
|
277
|
+
'recurring_frequency': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
|
|
278
|
+
'recurring_start_date': '2026-09-01',
|
|
279
|
+
'cover_fee_required': True, # optional
|
|
280
|
+
'lines': [
|
|
281
|
+
{'item_id': item.id, 'name': 'Retainer', 'quantity': 1, 'unit_price': 500},
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
print(ri.next_run_date, ri.upcoming_run_dates)
|
|
286
|
+
|
|
287
|
+
dime.recurring_invoices.cancel('000010', ri.id)
|
|
288
|
+
```
|
|
289
|
+
|
|
167
290
|
## Pagination
|
|
168
291
|
|
|
169
292
|
List endpoints return a `CursorPage`. Iterate one page, walk pages manually, or stream every
|
|
@@ -227,7 +350,9 @@ except DimeException as e:
|
|
|
227
350
|
## Notes
|
|
228
351
|
|
|
229
352
|
- **GET requests carry a JSON body.** The Dime API expects read parameters in the request body
|
|
230
|
-
even for `GET` endpoints; the SDK handles this transparently.
|
|
353
|
+
even for `GET` endpoints; the SDK handles this transparently. Point `base_url` at an `https://`
|
|
354
|
+
origin — an `http://` URL that 301-redirects to `https` will have its request body dropped by the
|
|
355
|
+
redirect, which surfaces as a `403` "You do not have access to this company." from the API.
|
|
231
356
|
- **No API versioning.** Endpoints live under `/api` with no version prefix.
|
|
232
357
|
|
|
233
358
|
## Development
|
|
@@ -3,8 +3,10 @@ from .http.transport import Transport
|
|
|
3
3
|
from .resources.addresses import Addresses
|
|
4
4
|
from .resources.customers import Customers
|
|
5
5
|
from .resources.deposits import Deposits
|
|
6
|
+
from .resources.invoices import Invoices
|
|
6
7
|
from .resources.merchants import Merchants
|
|
7
8
|
from .resources.payment_methods import PaymentMethods
|
|
9
|
+
from .resources.recurring_invoices import RecurringInvoices
|
|
8
10
|
from .resources.recurring_payments import RecurringPayments
|
|
9
11
|
from .resources.transactions import Transactions
|
|
10
12
|
|
|
@@ -25,6 +27,8 @@ class Client:
|
|
|
25
27
|
self.addresses = Addresses(transport)
|
|
26
28
|
self.deposits = Deposits(transport)
|
|
27
29
|
self.recurring_payments = RecurringPayments(transport)
|
|
30
|
+
self.invoices = Invoices(transport)
|
|
31
|
+
self.recurring_invoices = RecurringInvoices(transport)
|
|
28
32
|
|
|
29
33
|
def config(self) -> Config:
|
|
30
34
|
return self._config
|
|
@@ -4,9 +4,17 @@ from .deposit import Deposit
|
|
|
4
4
|
from .deposit_group import DepositGroup
|
|
5
5
|
from .deposit_with_transactions import DepositWithTransactions
|
|
6
6
|
from .form_link import FormLink
|
|
7
|
+
from .invoice import Invoice
|
|
8
|
+
from .invoice_customer import InvoiceCustomer
|
|
9
|
+
from .invoice_event import InvoiceEvent
|
|
10
|
+
from .invoice_item import InvoiceItem
|
|
11
|
+
from .invoice_link import InvoiceLink
|
|
12
|
+
from .invoice_payment import InvoicePayment
|
|
13
|
+
from .line_item import LineItem
|
|
7
14
|
from .merchant import Merchant
|
|
8
15
|
from .message_result import MessageResult
|
|
9
16
|
from .payment_method import PaymentMethod
|
|
17
|
+
from .recurring_invoice import RecurringInvoice, RecurringInvoiceRun
|
|
10
18
|
from .recurring_payment import RecurringPayment
|
|
11
19
|
from .recurring_payment_method import RecurringPaymentMethod
|
|
12
20
|
from .tokenize_result import TokenizeResult
|
|
@@ -28,4 +36,13 @@ __all__ = [
|
|
|
28
36
|
'TokenizeResult',
|
|
29
37
|
'MessageResult',
|
|
30
38
|
'FormLink',
|
|
39
|
+
'Invoice',
|
|
40
|
+
'InvoiceCustomer',
|
|
41
|
+
'InvoiceEvent',
|
|
42
|
+
'InvoiceItem',
|
|
43
|
+
'InvoiceLink',
|
|
44
|
+
'InvoicePayment',
|
|
45
|
+
'LineItem',
|
|
46
|
+
'RecurringInvoice',
|
|
47
|
+
'RecurringInvoiceRun',
|
|
31
48
|
]
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..support.arr import arr_array, arr_bool, arr_int, arr_object, arr_string
|
|
5
|
+
from .cover_fee_quote import CoverFeeQuote
|
|
6
|
+
from .invoice_customer import InvoiceCustomer
|
|
7
|
+
from .invoice_event import InvoiceEvent
|
|
8
|
+
from .invoice_payment import InvoicePayment
|
|
9
|
+
from .line_item import LineItem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Invoice:
|
|
14
|
+
"""
|
|
15
|
+
A full invoice as returned by the show, create, update and action endpoints.
|
|
16
|
+
|
|
17
|
+
Money fields arrive as dollar amounts and are kept as strings, consistent
|
|
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.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
id: int | None = None
|
|
26
|
+
token: str | None = None
|
|
27
|
+
invoice_number: str | None = None
|
|
28
|
+
status: str | None = None
|
|
29
|
+
payment_terms: str | None = None
|
|
30
|
+
issue_date: str | None = None
|
|
31
|
+
due_date: str | None = None
|
|
32
|
+
is_overdue: bool = False
|
|
33
|
+
subtotal: str | None = None
|
|
34
|
+
total: str | None = None
|
|
35
|
+
amount_paid: str | None = None
|
|
36
|
+
balance: str | None = None
|
|
37
|
+
allow_partial_payment: bool = False
|
|
38
|
+
cover_fee_required: bool = False
|
|
39
|
+
cover_fee_quote: CoverFeeQuote | None = None
|
|
40
|
+
thank_you_note: str | None = None
|
|
41
|
+
public_url: str | None = None
|
|
42
|
+
customer: InvoiceCustomer = field(default_factory=InvoiceCustomer)
|
|
43
|
+
items: list[LineItem] = field(default_factory=list)
|
|
44
|
+
payments: list[InvoicePayment] = field(default_factory=list)
|
|
45
|
+
events: list[InvoiceEvent] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: dict[str, Any]) -> 'Invoice':
|
|
49
|
+
return cls(
|
|
50
|
+
id=arr_int(data, 'id'),
|
|
51
|
+
token=arr_string(data, 'token'),
|
|
52
|
+
invoice_number=arr_string(data, 'invoice_number'),
|
|
53
|
+
status=arr_string(data, 'status'),
|
|
54
|
+
payment_terms=arr_string(data, 'payment_terms'),
|
|
55
|
+
issue_date=arr_string(data, 'issue_date'),
|
|
56
|
+
due_date=arr_string(data, 'due_date'),
|
|
57
|
+
is_overdue=arr_bool(data, 'is_overdue'),
|
|
58
|
+
subtotal=arr_string(data, 'subtotal'),
|
|
59
|
+
total=arr_string(data, 'total'),
|
|
60
|
+
amount_paid=arr_string(data, 'amount_paid'),
|
|
61
|
+
balance=arr_string(data, 'balance'),
|
|
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
|
+
),
|
|
69
|
+
thank_you_note=arr_string(data, 'thank_you_note'),
|
|
70
|
+
public_url=arr_string(data, 'public_url'),
|
|
71
|
+
customer=InvoiceCustomer.from_dict(arr_object(data, 'customer')),
|
|
72
|
+
items=[LineItem.from_dict(i) for i in arr_array(data, 'items') if isinstance(i, dict)],
|
|
73
|
+
payments=[InvoicePayment.from_dict(p) for p in arr_array(data, 'payments') if isinstance(p, dict)],
|
|
74
|
+
events=[InvoiceEvent.from_dict(e) for e in arr_array(data, 'events') if isinstance(e, dict)],
|
|
75
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..support.arr import arr_int, arr_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InvoiceCustomer:
|
|
9
|
+
"""The customer snapshot embedded in an invoice response."""
|
|
10
|
+
|
|
11
|
+
id: int | None = None
|
|
12
|
+
name: str | None = None
|
|
13
|
+
email: str | None = None
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, data: dict[str, Any]) -> 'InvoiceCustomer':
|
|
17
|
+
return cls(
|
|
18
|
+
id=arr_int(data, 'id'),
|
|
19
|
+
name=arr_string(data, 'name'),
|
|
20
|
+
email=arr_string(data, 'email'),
|
|
21
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..support.arr import arr_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InvoiceEvent:
|
|
9
|
+
"""An entry in an invoice's history (created, sent, paid, voided, ...)."""
|
|
10
|
+
|
|
11
|
+
type: str | None = None
|
|
12
|
+
label: str | None = None
|
|
13
|
+
description: str | None = None
|
|
14
|
+
created_at: str | None = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, data: dict[str, Any]) -> 'InvoiceEvent':
|
|
18
|
+
return cls(
|
|
19
|
+
type=arr_string(data, 'type'),
|
|
20
|
+
label=arr_string(data, 'label'),
|
|
21
|
+
description=arr_string(data, 'description'),
|
|
22
|
+
created_at=arr_string(data, 'created_at'),
|
|
23
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..support.arr import arr_bool, arr_int, arr_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InvoiceItem:
|
|
9
|
+
"""
|
|
10
|
+
A Merchant item (fund or designation) that invoice line items reference by
|
|
11
|
+
``item_id``. This is the catalog entry, not a line on an invoice — for that,
|
|
12
|
+
see :class:`LineItem`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
id: int | None = None
|
|
16
|
+
name: str | None = None
|
|
17
|
+
description: str | None = None
|
|
18
|
+
price: str | None = None
|
|
19
|
+
tax_deductible: bool = False
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, data: dict[str, Any]) -> 'InvoiceItem':
|
|
23
|
+
return cls(
|
|
24
|
+
id=arr_int(data, 'id'),
|
|
25
|
+
name=arr_string(data, 'name'),
|
|
26
|
+
description=arr_string(data, 'description'),
|
|
27
|
+
price=arr_string(data, 'price'),
|
|
28
|
+
tax_deductible=arr_bool(data, 'tax_deductible'),
|
|
29
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..support.arr import arr_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InvoiceLink:
|
|
9
|
+
"""The public payment link for an invoice."""
|
|
10
|
+
|
|
11
|
+
public_url: str | None = None
|
|
12
|
+
token: str | None = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_dict(cls, data: dict[str, Any]) -> 'InvoiceLink':
|
|
16
|
+
return cls(
|
|
17
|
+
public_url=arr_string(data, 'public_url'),
|
|
18
|
+
token=arr_string(data, 'token'),
|
|
19
|
+
)
|