dime-python-sdk 1.0.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

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_payments/client.py CHANGED
@@ -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
dime_payments/config.py CHANGED
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
 
9
9
  class Config:
10
10
  DEFAULT_BASE_URL = 'https://app.dimepayments.com'
11
- VERSION = '1.0.0'
11
+ VERSION = '1.3.0'
12
12
 
13
13
  def __init__(
14
14
  self,
@@ -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
+ )
@@ -0,0 +1,32 @@
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 InvoicePayment:
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
+ """
17
+
18
+ amount: str | None = None
19
+ cover_fee: str | None = None
20
+ paid_at: str | None = None
21
+ method: str | None = None
22
+ transaction_id: int | None = None
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: dict[str, Any]) -> 'InvoicePayment':
26
+ return cls(
27
+ amount=arr_string(data, 'amount'),
28
+ cover_fee=arr_string(data, 'cover_fee'),
29
+ paid_at=arr_string(data, 'paid_at'),
30
+ method=arr_string(data, 'method'),
31
+ transaction_id=arr_int(data, 'transaction_id'),
32
+ )
@@ -0,0 +1,33 @@
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 LineItem:
9
+ """
10
+ A single line on an invoice. ``item_id`` points at the Merchant item the line
11
+ was sourced from; ``name`` and ``unit_price`` are snapshotted at creation so
12
+ later edits to the item do not rewrite history.
13
+ """
14
+
15
+ id: int | None = None
16
+ item_id: int | None = None
17
+ name: str | None = None
18
+ description: str | None = None
19
+ quantity: str | None = None
20
+ unit_price: str | None = None
21
+ amount: str | None = None
22
+
23
+ @classmethod
24
+ def from_dict(cls, data: dict[str, Any]) -> 'LineItem':
25
+ return cls(
26
+ id=arr_int(data, 'id'),
27
+ item_id=arr_int(data, 'item_id'),
28
+ name=arr_string(data, 'name'),
29
+ description=arr_string(data, 'description'),
30
+ quantity=arr_string(data, 'quantity'),
31
+ unit_price=arr_string(data, 'unit_price'),
32
+ amount=arr_string(data, 'amount'),
33
+ )
@@ -0,0 +1,70 @@
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 .invoice_customer import InvoiceCustomer
6
+ from .line_item import LineItem
7
+
8
+
9
+ @dataclass
10
+ class RecurringInvoiceRun:
11
+ """A summary of one invoice already emitted by a recurring template."""
12
+
13
+ id: int | None = None
14
+ invoice_number: str | None = None
15
+ status: str | None = None
16
+ total: str | None = None
17
+ issue_date: str | None = None
18
+
19
+ @classmethod
20
+ def from_dict(cls, data: dict[str, Any]) -> 'RecurringInvoiceRun':
21
+ return cls(
22
+ id=arr_int(data, 'id'),
23
+ invoice_number=arr_string(data, 'invoice_number'),
24
+ status=arr_string(data, 'status'),
25
+ total=arr_string(data, 'total'),
26
+ issue_date=arr_string(data, 'issue_date'),
27
+ )
28
+
29
+
30
+ @dataclass
31
+ class RecurringInvoice:
32
+ """
33
+ A recurring-invoice template. ``recurrence_schedule`` is one of Weekly,
34
+ Biweekly, FirstFifteenth, Monthly or Yearly.
35
+ """
36
+
37
+ id: int | None = None
38
+ status: str | None = None
39
+ recurrence_schedule: str | None = None
40
+ payment_terms: str | None = None
41
+ # Copied onto every invoice this template generates.
42
+ cover_fee_required: bool = False
43
+ start_date: str | None = None
44
+ end_date: str | None = None
45
+ next_run_date: str | None = None
46
+ last_run_date: str | None = None
47
+ thank_you_note: str | None = None
48
+ customer: InvoiceCustomer = field(default_factory=InvoiceCustomer)
49
+ items: list[LineItem] = field(default_factory=list)
50
+ upcoming_run_dates: list[str] = field(default_factory=list)
51
+ invoices: list[RecurringInvoiceRun] = field(default_factory=list)
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: dict[str, Any]) -> 'RecurringInvoice':
55
+ return cls(
56
+ id=arr_int(data, 'id'),
57
+ status=arr_string(data, 'status'),
58
+ recurrence_schedule=arr_string(data, 'recurrence_schedule'),
59
+ payment_terms=arr_string(data, 'payment_terms'),
60
+ cover_fee_required=arr_bool(data, 'cover_fee_required'),
61
+ start_date=arr_string(data, 'start_date'),
62
+ end_date=arr_string(data, 'end_date'),
63
+ next_run_date=arr_string(data, 'next_run_date'),
64
+ last_run_date=arr_string(data, 'last_run_date'),
65
+ thank_you_note=arr_string(data, 'thank_you_note'),
66
+ customer=InvoiceCustomer.from_dict(arr_object(data, 'customer')),
67
+ items=[LineItem.from_dict(i) for i in arr_array(data, 'items') if isinstance(i, dict)],
68
+ upcoming_run_dates=[str(d) for d in arr_array(data, 'upcoming_run_dates')],
69
+ invoices=[RecurringInvoiceRun.from_dict(i) for i in arr_array(data, 'invoices') if isinstance(i, dict)],
70
+ )
@@ -11,7 +11,7 @@ from .error_handler import ErrorHandler
11
11
  if TYPE_CHECKING:
12
12
  from ..config import Config
13
13
 
14
- SDK_VERSION = '1.0.0'
14
+ SDK_VERSION = '1.3.0'
15
15
 
16
16
 
17
17
  class Transport:
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..data_objects.invoice import Invoice
6
+ from ..data_objects.invoice_item import InvoiceItem
7
+ from ..data_objects.invoice_link import InvoiceLink
8
+ from ..data_objects.message_result import MessageResult
9
+ from ..pagination.cursor_page import CursorPage
10
+ from .abstract_resource import AbstractResource
11
+
12
+
13
+ class Invoices(AbstractResource):
14
+ """
15
+ Invoice endpoints.
16
+
17
+ Every method takes the merchant ``sid`` explicitly; remaining fields go in an
18
+ ``attributes`` dict. Identify the customer with ``customer_uuid`` — the same
19
+ identifier the customer, payment-method and address endpoints use, and the
20
+ only one :class:`Customer` exposes. ``customer_id`` is still accepted for
21
+ integrations written against the original contract; supply exactly one.
22
+
23
+ Line-item and invoice mutations return the refreshed invoice, so the caller
24
+ always sees recalculated totals rather than a detached fragment.
25
+ """
26
+
27
+ def list(self, sid: str, filters: dict[str, Any] | None = None) -> CursorPage[Invoice]:
28
+ body = self._envelope({'sid': sid}, filters or {})
29
+ return self._paginate('GET', 'invoices', body, Invoice.from_dict)
30
+
31
+ def show(self, sid: str, invoice_id: int | str) -> Invoice:
32
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
33
+ raw = self._transport.request('GET', 'invoice', body)
34
+ return Invoice.from_dict(raw.get('data') or {})
35
+
36
+ def create(self, sid: str, attributes: dict[str, Any]) -> Invoice:
37
+ """
38
+ Create a draft invoice. Requires ``customer_uuid`` (or ``customer_id``),
39
+ ``customer_name``, ``customer_email``, ``payment_terms``
40
+ (``due_on_receipt`` | ``net_15`` | ``net_30`` | ``net_60``) and at least
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.
48
+ """
49
+ body = self._envelope({'sid': sid} | attributes)
50
+ raw = self._transport.request('POST', 'invoice/create', body)
51
+ return Invoice.from_dict(raw.get('data') or {})
52
+
53
+ def update(self, sid: str, invoice_id: int | str, attributes: dict[str, Any]) -> Invoice:
54
+ """Update a draft invoice. Passing ``lines`` replaces the existing lines."""
55
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id} | attributes)
56
+ raw = self._transport.request('PATCH', 'invoice/update', body)
57
+ return Invoice.from_dict(raw.get('data') or {})
58
+
59
+ def delete(self, sid: str, invoice_id: int | str) -> MessageResult:
60
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
61
+ raw = self._transport.request('POST', 'invoice/delete', body)
62
+ return MessageResult.from_dict(raw.get('data') or raw)
63
+
64
+ def send(self, sid: str, invoice_id: int | str) -> Invoice:
65
+ """Email the invoice and advance it to Sent."""
66
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
67
+ raw = self._transport.request('POST', 'invoice/send', body)
68
+ return Invoice.from_dict(raw.get('data') or {})
69
+
70
+ def mark_sent(self, sid: str, invoice_id: int | str) -> Invoice:
71
+ """Activate a draft for payment without emailing it."""
72
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
73
+ raw = self._transport.request('POST', 'invoice/mark-sent', body)
74
+ return Invoice.from_dict(raw.get('data') or {})
75
+
76
+ def void(self, sid: str, invoice_id: int | str) -> Invoice:
77
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
78
+ raw = self._transport.request('PATCH', 'invoice/void', body)
79
+ return Invoice.from_dict(raw.get('data') or {})
80
+
81
+ def duplicate(self, sid: str, invoice_id: int | str) -> Invoice:
82
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
83
+ raw = self._transport.request('POST', 'invoice/duplicate', body)
84
+ return Invoice.from_dict(raw.get('data') or {})
85
+
86
+ def pay(self, sid: str, invoice_id: int | str, attributes: dict[str, Any]) -> Invoice:
87
+ """
88
+ Record a merchant-initiated (MOTO) payment against an open invoice.
89
+
90
+ ``payment_type`` is required and is either ``cc`` or ``ach``. For a card,
91
+ pass a stored ``token`` or raw ``cardholder_name`` / ``card_number`` /
92
+ ``expiration_date``; for ACH, pass ``routing_number`` /
93
+ ``account_number`` / ``account_type`` / ``account_name``. Omit ``amount``
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``.
101
+ """
102
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id} | attributes)
103
+ raw = self._transport.request('POST', 'invoice/pay', body)
104
+ return Invoice.from_dict(raw.get('data') or {})
105
+
106
+ def get_link(self, sid: str, invoice_id: int | str) -> InvoiceLink:
107
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id})
108
+ raw = self._transport.request('GET', 'invoice/link', body)
109
+ return InvoiceLink.from_dict(raw.get('data') or {})
110
+
111
+ def list_items(self, sid: str) -> list[InvoiceItem]:
112
+ """
113
+ List the Merchant's items (funds/designations) that line items can
114
+ reference. Scoped to the merchant, not to a single invoice — an
115
+ invoice's own lines are on :attr:`Invoice.items`.
116
+ """
117
+ body = self._envelope({'sid': sid})
118
+ raw = self._transport.request('GET', 'invoice/items', body)
119
+ items = raw.get('data', [])
120
+ if isinstance(items, dict):
121
+ items = list(items.values())
122
+ return [InvoiceItem.from_dict(item) for item in items if isinstance(item, dict)]
123
+
124
+ def create_item(self, sid: str, attributes: dict[str, Any]) -> InvoiceItem:
125
+ """
126
+ Create an invoicing-only Merchant item. ``name`` is required;
127
+ ``description``, ``price`` and ``tax_deductible`` are optional.
128
+ """
129
+ body = self._envelope({'sid': sid} | attributes)
130
+ raw = self._transport.request('POST', 'invoice/item/create', body)
131
+ return InvoiceItem.from_dict(raw.get('data') or {})
132
+
133
+ def add_line_item(self, sid: str, invoice_id: int | str, attributes: dict[str, Any]) -> Invoice:
134
+ body = self._envelope({'sid': sid, 'invoice_id': invoice_id} | attributes)
135
+ raw = self._transport.request('POST', 'invoice/line-item/add', body)
136
+ return Invoice.from_dict(raw.get('data') or {})
137
+
138
+ def update_line_item(
139
+ self,
140
+ sid: str,
141
+ invoice_id: int | str,
142
+ line_item_id: int | str,
143
+ attributes: dict[str, Any],
144
+ ) -> Invoice:
145
+ body = self._envelope(
146
+ {'sid': sid, 'invoice_id': invoice_id, 'line_item_id': line_item_id} | attributes
147
+ )
148
+ raw = self._transport.request('PATCH', 'invoice/line-item/update', body)
149
+ return Invoice.from_dict(raw.get('data') or {})
150
+
151
+ def delete_line_item(self, sid: str, invoice_id: int | str, line_item_id: int | str) -> Invoice:
152
+ body = self._envelope(
153
+ {'sid': sid, 'invoice_id': invoice_id, 'line_item_id': line_item_id}
154
+ )
155
+ raw = self._transport.request('POST', 'invoice/line-item/delete', body)
156
+ return Invoice.from_dict(raw.get('data') or {})
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..data_objects.recurring_invoice import RecurringInvoice
6
+ from ..pagination.cursor_page import CursorPage
7
+ from .abstract_resource import AbstractResource
8
+
9
+
10
+ class RecurringInvoices(AbstractResource):
11
+ def list(self, sid: str, filters: dict[str, Any] | None = None) -> CursorPage[RecurringInvoice]:
12
+ body = self._envelope({'sid': sid}, filters or {})
13
+ return self._paginate('GET', 'recurring-invoices', body, RecurringInvoice.from_dict)
14
+
15
+ def show(self, sid: str, recurring_invoice_id: int | str) -> RecurringInvoice:
16
+ body = self._envelope({'sid': sid, 'recurring_invoice_id': recurring_invoice_id})
17
+ raw = self._transport.request('GET', 'recurring-invoice', body)
18
+ return RecurringInvoice.from_dict(raw.get('data') or {})
19
+
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
+ """
28
+ body = self._envelope({'sid': sid} | attributes)
29
+ raw = self._transport.request('POST', 'recurring-invoice/create', body)
30
+ return RecurringInvoice.from_dict(raw.get('data') or {})
31
+
32
+ def cancel(self, sid: str, recurring_invoice_id: int | str) -> RecurringInvoice:
33
+ body = self._envelope({'sid': sid, 'recurring_invoice_id': recurring_invoice_id})
34
+ raw = self._transport.request('POST', 'recurring-invoice/cancel', body)
35
+ return RecurringInvoice.from_dict(raw.get('data') or {})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dime-python-sdk
3
- Version: 1.0.0
3
+ Version: 1.3.0
4
4
  Summary: Python client for the Dime Payments API
5
5
  Author: Dime Technology
6
6
  License: MIT License
@@ -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
@@ -1,16 +1,25 @@
1
1
  dime_payments/__init__.py,sha256=nJGtKtehbZMxar3wgdk8RIG_nNPSb6G9rmMyC2PnjqM,638
2
- dime_payments/client.py,sha256=K4riOAzgJJvZUfICZxuh34fD0q5oUDeqRExNH51xhRA,1120
3
- dime_payments/config.py,sha256=io97prwhlASHBRl0nm_YwckMuJxXFP2ylpsfbT7Bys8,847
4
- dime_payments/data_objects/__init__.py,sha256=1t2xoZ5rX7RoAO9r-rAsTHoyHBNsK8_sEzHQ_8o1pWY,888
2
+ dime_payments/client.py,sha256=dX_mH2q_hBZrC2s8B3Y3EYy3AWAvUYgTDNf1M0i9aFM,1328
3
+ dime_payments/config.py,sha256=IEINY3s56MAaIDhVO5Cajx4D_lmUoR5SG8BLO3IOpM4,847
4
+ dime_payments/data_objects/__init__.py,sha256=fuavK4aZ-IFM_PyMpJ8fIZlip7-V5m5LOHTCr2XnZng,1409
5
5
  dime_payments/data_objects/address.py,sha256=ocnxRM3JDLr4mDtlnVgsnUb6Hy5u6vNQlWVRFHzuMIw,950
6
+ dime_payments/data_objects/cover_fee_quote.py,sha256=KuS1GMVe-Stixate6crTswvvhnq8e9Jfq3v4C2pRYr0,1717
6
7
  dime_payments/data_objects/customer.py,sha256=fSUPB9PynX_R3Jntnjr130ExeZtgrmRZSVfds54rmV0,1141
7
8
  dime_payments/data_objects/deposit.py,sha256=b0rQdNgdprMtP0pdMrepsWG3jBt7fJ4FGZ1EuHTRQhA,1139
8
9
  dime_payments/data_objects/deposit_group.py,sha256=kmfwnDO0YgmvmlSvIakb4cMFYOMfYDDnXMR4eiabSKA,813
9
10
  dime_payments/data_objects/deposit_with_transactions.py,sha256=HjmWB5v2R67z5xgqoZt1rlV5nxvixrTmySS-zzUFA0c,1420
10
11
  dime_payments/data_objects/form_link.py,sha256=rPU7cJynsrfb_1QjT8DkZ8ShvXVXUScBqJNZVASeJCI,280
12
+ dime_payments/data_objects/invoice.py,sha256=GQ6g-9e4toNsgJF-2WsnJ0S3mgeizrtYbBYhI3ur46E,3251
13
+ dime_payments/data_objects/invoice_customer.py,sha256=GtnomaFO8xlpRuElMPPAAtlT5DwJLyp3NB5wGqdW9Bc,528
14
+ dime_payments/data_objects/invoice_event.py,sha256=dvKOfcB9sxE6ehFQyB_lsfpA6iMFX3oK5WAUjSIpgDk,646
15
+ dime_payments/data_objects/invoice_item.py,sha256=aVgO0UM7vqJB2VjvtYMBpLsCWXKHus0r7pA9I7UHwxo,856
16
+ dime_payments/data_objects/invoice_link.py,sha256=kiWSXkl4jtsCZ-ODsEaFU12mTk3soe0qZ7ICDJ2MiEE,452
17
+ dime_payments/data_objects/invoice_payment.py,sha256=EPvs8FhhDyxEyRo8RoiVSRZK5LIBEwF5Iwjcw7DM0Ac,983
18
+ dime_payments/data_objects/line_item.py,sha256=EJtpxnSOoCOlZJcrq8jDarieICPHtQK-g6jipTDMDAg,1024
11
19
  dime_payments/data_objects/merchant.py,sha256=U6RbP3QJACjOtrSExyodfz9zOzeU2hThXwGD6M0XATI,1900
12
20
  dime_payments/data_objects/message_result.py,sha256=2gZYbokwPRdlBUz_vGgYzrJpxMnjMuzwtTdjXhUv1GM,299
13
21
  dime_payments/data_objects/payment_method.py,sha256=-3kcHBGoXD12mzHocMoKU0aBwoNJ_9GJFx5eldXUJxA,2460
22
+ dime_payments/data_objects/recurring_invoice.py,sha256=pS4DEZdMnxhA6qwcM3bUH7k-yb0NU8-OvJdrk_cdihA,2785
14
23
  dime_payments/data_objects/recurring_payment.py,sha256=E6F3oX6aqWeE2Ek4ePqDDbZ79APdIKKXVip4bTcFU-g,2209
15
24
  dime_payments/data_objects/recurring_payment_method.py,sha256=P22ITvvxAUJ4NaEc7MH2ItCFm6wk1mEuxRs4ffSUDrg,402
16
25
  dime_payments/data_objects/tokenize_result.py,sha256=i27Hc0A-dFM8cX8s9A71yM95kd48ih25fvnb3SEml54,295
@@ -28,7 +37,7 @@ dime_payments/exceptions/server_exception.py,sha256=SCUubaM1wTwFMW3E9mBSxry4A-0Q
28
37
  dime_payments/exceptions/validation_exception.py,sha256=_HGMPN-5iFq6RWJY-06Oaak565MeD-yRf6GnbXp7Sog,689
29
38
  dime_payments/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
39
  dime_payments/http/error_handler.py,sha256=dqClsyDIcCPS6CtiqtGvEwTnORj24m2muXThXfHdhX0,2873
31
- dime_payments/http/transport.py,sha256=G2dPpXckNBUKjc0MwmzopF8YfACMR3LR3M9RROyX_Nw,2642
40
+ dime_payments/http/transport.py,sha256=tcFR_BLZJqti6mrWYRKKvYIEUJKMYG1mmv5FChA-b9Q,2642
32
41
  dime_payments/pagination/__init__.py,sha256=J2k8htn29Nu92KQh4wtEeN0StyMc8cWxVY8jQydF-dQ,62
33
42
  dime_payments/pagination/cursor_page.py,sha256=Xpbt33ceBy_HQlqe3wFbxA93bUfx3rHcqXfhVAkOQJ0,2263
34
43
  dime_payments/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,13 +45,15 @@ dime_payments/resources/abstract_resource.py,sha256=-XmnqJvk2tVNe44JeY_TTAzD4UiU
36
45
  dime_payments/resources/addresses.py,sha256=sg0q3YCnkdGlGM0onbBh_UsatrIu4VyzAA1BdZ1QRZ0,1713
37
46
  dime_payments/resources/customers.py,sha256=6_9gG9tc2uHN-onhzNFP0NO67NxGOYqbwZmF96vU4bo,1545
38
47
  dime_payments/resources/deposits.py,sha256=k8wE7kT_baHjXmXxzN9v6_2BqDb0GSUlINPRVtjmyUI,1213
48
+ dime_payments/resources/invoices.py,sha256=e85DWM2PotddRH-TR-tiWKBtLDxJ8BaSCgsLQSN0HAM,7841
39
49
  dime_payments/resources/merchants.py,sha256=blVx7JIAps9ktdpLjjQofEnVkK_KgXOQP8beBPkHrkg,1370
40
50
  dime_payments/resources/payment_methods.py,sha256=KJWryjnZadyohG1r6pmULjwPIjPIjScSK7ekQYamvYA,1775
51
+ dime_payments/resources/recurring_invoices.py,sha256=gJ4sNPvxv4qyNppsGZnW_orOfEVw4ROTa6yrN71Ax4g,1726
41
52
  dime_payments/resources/recurring_payments.py,sha256=DPihjc-C7oLLfPrP5UveNpEKHmUxayv4k3hFpguJ35c,2940
42
53
  dime_payments/resources/transactions.py,sha256=Q0VwYDicdGuM1WQXqxPX_KS313A0KZVCeuVCyKS_G7k,2742
43
54
  dime_payments/support/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
55
  dime_payments/support/arr.py,sha256=_mboi_q3HGn_Sx_MgU2exkbl-fnP2gdDY8yeSC8hkNc,1722
45
- dime_python_sdk-1.0.0.dist-info/METADATA,sha256=YdWerOwvU8IBJOFjpxJg3HuuEbrDW-oC19Mp9SJJeBo,9553
46
- dime_python_sdk-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
47
- dime_python_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=-Xc5dLgfuFLVQLsSKNYmJKtNZCUU1_9ErffdGiwH-DU,1072
48
- dime_python_sdk-1.0.0.dist-info/RECORD,,
56
+ dime_python_sdk-1.3.0.dist-info/METADATA,sha256=xPcFnbu29Dk0nUZZythxZw8wRMHBF3j9ZzIhLQX6YSc,14703
57
+ dime_python_sdk-1.3.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
58
+ dime_python_sdk-1.3.0.dist-info/licenses/LICENSE,sha256=-Xc5dLgfuFLVQLsSKNYmJKtNZCUU1_9ErffdGiwH-DU,1072
59
+ dime_python_sdk-1.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any