pacspace-sdk 0.1.2__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.
- pacspace_sdk/__init__.py +35 -0
- pacspace_sdk/_internal/__init__.py +1 -0
- pacspace_sdk/_internal/batch_transport.py +43 -0
- pacspace_sdk/balance.py +535 -0
- pacspace_sdk/client.py +322 -0
- pacspace_sdk/customers.py +37 -0
- pacspace_sdk/errors.py +198 -0
- pacspace_sdk/pacspace.py +76 -0
- pacspace_sdk/polling.py +32 -0
- pacspace_sdk/submission.py +387 -0
- pacspace_sdk/webhooks/__init__.py +3 -0
- pacspace_sdk/webhooks/types.py +33 -0
- pacspace_sdk/webhooks/verify.py +111 -0
- pacspace_sdk-0.1.2.dist-info/METADATA +62 -0
- pacspace_sdk-0.1.2.dist-info/RECORD +18 -0
- pacspace_sdk-0.1.2.dist-info/WHEEL +5 -0
- pacspace_sdk-0.1.2.dist-info/licenses/LICENSE +21 -0
- pacspace_sdk-0.1.2.dist-info/top_level.txt +1 -0
pacspace_sdk/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .pacspace import PacSpace
|
|
2
|
+
from .errors import (
|
|
3
|
+
PacSpaceError,
|
|
4
|
+
InvalidApiKeyError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
ContractNotDeployedError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
CadenceLimitError,
|
|
10
|
+
ServiceUnavailableError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
InvalidScopeCombinationError,
|
|
13
|
+
ScopeTooWideError,
|
|
14
|
+
TimeoutError,
|
|
15
|
+
WebhookVerificationError,
|
|
16
|
+
)
|
|
17
|
+
from .webhooks.verify import Webhooks
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"PacSpace",
|
|
21
|
+
"PacSpaceError",
|
|
22
|
+
"InvalidApiKeyError",
|
|
23
|
+
"InsufficientCreditsError",
|
|
24
|
+
"NotFoundError",
|
|
25
|
+
"ContractNotDeployedError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"CadenceLimitError",
|
|
28
|
+
"ServiceUnavailableError",
|
|
29
|
+
"ValidationError",
|
|
30
|
+
"InvalidScopeCombinationError",
|
|
31
|
+
"ScopeTooWideError",
|
|
32
|
+
"TimeoutError",
|
|
33
|
+
"WebhookVerificationError",
|
|
34
|
+
"Webhooks",
|
|
35
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal SDK helpers. Not part of the public SDK surface."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Internal batch transport used by SubmissionCoordinator only.
|
|
2
|
+
|
|
3
|
+
Not part of the public SDK surface. Do not reference from public code paths.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
8
|
+
|
|
9
|
+
from ..client import HttpClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def submit_internal_batch(
|
|
13
|
+
client: HttpClient,
|
|
14
|
+
deltas: Iterable[Dict[str, Any]],
|
|
15
|
+
options: Optional[Dict[str, Any]] = None,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
"""Submit deltas via the internal transport used by the summary submission coordinator.
|
|
18
|
+
|
|
19
|
+
This helper is internal; do not invoke it from public SDK code paths.
|
|
20
|
+
"""
|
|
21
|
+
payload_deltas: List[Dict[str, Any]] = list(deltas)
|
|
22
|
+
response = client.post(
|
|
23
|
+
"/api/v1/balance/delta/batch",
|
|
24
|
+
{"deltas": payload_deltas},
|
|
25
|
+
options,
|
|
26
|
+
)
|
|
27
|
+
return _normalize_internal_batch_response(response)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_internal_batch_response(data: Any) -> Dict[str, Any]:
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
return {"totalQueued": 0, "totalFailed": 0, "results": []}
|
|
33
|
+
|
|
34
|
+
results = data.get("results")
|
|
35
|
+
if not isinstance(results, list):
|
|
36
|
+
results = []
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"totalQueued": int(data.get("totalQueued", len(results))),
|
|
40
|
+
"totalFailed": int(data.get("totalFailed", 0)),
|
|
41
|
+
"results": results,
|
|
42
|
+
"_efficiency": data.get("_efficiency"),
|
|
43
|
+
}
|
pacspace_sdk/balance.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Dict, Iterable, Optional
|
|
5
|
+
from urllib.parse import quote, urlencode
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
from .polling import poll_until_terminal
|
|
9
|
+
from .submission import SubmissionCoordinator
|
|
10
|
+
from ._internal.batch_transport import submit_internal_batch
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_query(params: Dict[str, Any]) -> str:
|
|
14
|
+
compact = {key: value for key, value in params.items() if value is not None}
|
|
15
|
+
if not compact:
|
|
16
|
+
return ""
|
|
17
|
+
return "?" + urlencode(compact, doseq=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BalanceResource:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
client: HttpClient,
|
|
24
|
+
submission_options: Optional[Dict[str, Any]] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._client = client
|
|
27
|
+
self.submission = SubmissionCoordinator(
|
|
28
|
+
submit_batch=lambda deltas: submit_internal_batch(self._client, deltas),
|
|
29
|
+
options=submission_options,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _normalize_status(status: Any) -> str:
|
|
34
|
+
normalized = str(status or "").upper()
|
|
35
|
+
if normalized in {"QUEUED", "PROCESSING", "VERIFIED", "FAILED"}:
|
|
36
|
+
return normalized
|
|
37
|
+
return "QUEUED"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _to_api_checkpoint_type(checkpoint_type: Any) -> Any:
|
|
41
|
+
return checkpoint_type
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _validate_scope_mode(options: Dict[str, Any]) -> None:
|
|
45
|
+
period = options.get("period")
|
|
46
|
+
time_preset = options.get("timePreset")
|
|
47
|
+
start_date = options.get("startDate")
|
|
48
|
+
end_date = options.get("endDate")
|
|
49
|
+
starting_checkpoint = options.get("startingCheckpoint")
|
|
50
|
+
|
|
51
|
+
has_scope = any(
|
|
52
|
+
value is not None for value in (period, time_preset, start_date, end_date)
|
|
53
|
+
)
|
|
54
|
+
if starting_checkpoint is not None and has_scope:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"startingCheckpoint cannot be combined with period, timePreset, startDate, or endDate."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if period is not None and any(
|
|
60
|
+
value is not None for value in (time_preset, start_date, end_date)
|
|
61
|
+
):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"period cannot be combined with timePreset, startDate, or endDate."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if time_preset is not None and time_preset != "custom" and any(
|
|
67
|
+
value is not None for value in (start_date, end_date)
|
|
68
|
+
):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"startDate/endDate can only be used when timePreset is 'custom' or omitted."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if time_preset == "custom" and (start_date is None or end_date is None):
|
|
74
|
+
raise ValueError("timePreset='custom' requires both startDate and endDate.")
|
|
75
|
+
|
|
76
|
+
if time_preset is None and ((start_date is None) != (end_date is None)):
|
|
77
|
+
raise ValueError("startDate and endDate must be provided together.")
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _to_utc_iso(dt: datetime) -> str:
|
|
81
|
+
return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
82
|
+
|
|
83
|
+
def _normalize_payload(self, value: Any) -> Any:
|
|
84
|
+
if isinstance(value, list):
|
|
85
|
+
return [self._normalize_payload(item) for item in value]
|
|
86
|
+
if not isinstance(value, dict):
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
normalized: Dict[str, Any] = {
|
|
90
|
+
key: self._normalize_payload(payload) for key, payload in value.items()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if "status" in normalized:
|
|
94
|
+
normalized["status"] = self._normalize_status(normalized["status"])
|
|
95
|
+
|
|
96
|
+
return normalized
|
|
97
|
+
|
|
98
|
+
def _normalize_invoice_proof_payload(self, value: Any) -> Dict[str, Any]:
|
|
99
|
+
normalized = self._normalize_payload(value)
|
|
100
|
+
if not isinstance(normalized, dict):
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
for key in (
|
|
104
|
+
"verificationApiUrl",
|
|
105
|
+
"accessHint",
|
|
106
|
+
"customerLinkUrl",
|
|
107
|
+
"customerLinkHost",
|
|
108
|
+
):
|
|
109
|
+
field_value = normalized.get(key)
|
|
110
|
+
normalized[key] = None if field_value is None else str(field_value)
|
|
111
|
+
|
|
112
|
+
return normalized
|
|
113
|
+
|
|
114
|
+
def emit(
|
|
115
|
+
self,
|
|
116
|
+
customer_id: str,
|
|
117
|
+
delta: float,
|
|
118
|
+
reason: str,
|
|
119
|
+
options: Optional[Dict[str, Any]] = None,
|
|
120
|
+
) -> Dict[str, Any]:
|
|
121
|
+
options = options or {}
|
|
122
|
+
body: Dict[str, Any] = {
|
|
123
|
+
"customerId": customer_id,
|
|
124
|
+
"delta": delta,
|
|
125
|
+
"reason": reason,
|
|
126
|
+
}
|
|
127
|
+
if "referenceId" in options:
|
|
128
|
+
body["referenceId"] = options["referenceId"]
|
|
129
|
+
if "metadata" in options:
|
|
130
|
+
body["metadata"] = options["metadata"]
|
|
131
|
+
|
|
132
|
+
request_options = {
|
|
133
|
+
key: value
|
|
134
|
+
for key, value in options.items()
|
|
135
|
+
if key not in {"referenceId", "metadata"}
|
|
136
|
+
}
|
|
137
|
+
response = self._client.post("/api/v1/balance/delta", body, request_options)
|
|
138
|
+
return self._normalize_payload(response)
|
|
139
|
+
|
|
140
|
+
def emit_and_wait(
|
|
141
|
+
self,
|
|
142
|
+
customer_id: str,
|
|
143
|
+
delta: float,
|
|
144
|
+
reason: str,
|
|
145
|
+
options: Optional[Dict[str, Any]] = None,
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
options = options or {}
|
|
148
|
+
timeout_ms = int(options.get("timeout", 60_000))
|
|
149
|
+
poll_interval_ms = int(options.get("pollInterval", 2_000))
|
|
150
|
+
emit_options = {
|
|
151
|
+
key: value
|
|
152
|
+
for key, value in options.items()
|
|
153
|
+
if key not in {"timeout", "pollInterval"}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
initial = self.emit(customer_id, delta, reason, emit_options)
|
|
157
|
+
if str(initial.get("status", "")).upper() in {"VERIFIED", "FAILED"}:
|
|
158
|
+
return initial
|
|
159
|
+
|
|
160
|
+
record_id = str(initial["recordId"])
|
|
161
|
+
return poll_until_terminal(
|
|
162
|
+
lambda: self.delta_status(record_id),
|
|
163
|
+
timeout_ms=timeout_ms,
|
|
164
|
+
poll_interval_ms=poll_interval_ms,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def delta_status(
|
|
168
|
+
self,
|
|
169
|
+
record_id: str,
|
|
170
|
+
options: Optional[Dict[str, Any]] = None,
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
response = self._client.get(f"/api/v1/balance/delta/{quote(record_id, safe='')}", options)
|
|
173
|
+
return self._normalize_payload(response)
|
|
174
|
+
|
|
175
|
+
def get_record_status(
|
|
176
|
+
self,
|
|
177
|
+
record_id: str,
|
|
178
|
+
options: Optional[Dict[str, Any]] = None,
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
return self.delta_status(record_id, options)
|
|
181
|
+
|
|
182
|
+
def wait_for_verified(
|
|
183
|
+
self,
|
|
184
|
+
record_id: str,
|
|
185
|
+
options: Optional[Dict[str, Any]] = None,
|
|
186
|
+
) -> Dict[str, Any]:
|
|
187
|
+
options = options or {}
|
|
188
|
+
timeout_ms = int(options.get("timeout", 60_000))
|
|
189
|
+
poll_interval_ms = int(options.get("pollInterval", 2_000))
|
|
190
|
+
request_options = {
|
|
191
|
+
key: value
|
|
192
|
+
for key, value in options.items()
|
|
193
|
+
if key not in {"timeout", "pollInterval"}
|
|
194
|
+
}
|
|
195
|
+
return poll_until_terminal(
|
|
196
|
+
lambda: self.get_record_status(record_id, request_options),
|
|
197
|
+
timeout_ms=timeout_ms,
|
|
198
|
+
poll_interval_ms=poll_interval_ms,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def usage(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
202
|
+
response = self._client.get("/api/v1/balance/usage", options)
|
|
203
|
+
return self._normalize_payload(response)
|
|
204
|
+
|
|
205
|
+
def gaps(
|
|
206
|
+
self,
|
|
207
|
+
customer_id: str,
|
|
208
|
+
options: Optional[Dict[str, Any]] = None,
|
|
209
|
+
) -> Dict[str, Any]:
|
|
210
|
+
options = options or {}
|
|
211
|
+
query = _build_query(
|
|
212
|
+
{
|
|
213
|
+
"fromSequence": options.get("fromSequence"),
|
|
214
|
+
"toSequence": options.get("toSequence"),
|
|
215
|
+
"limit": options.get("limit"),
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
request_options = {
|
|
219
|
+
key: value
|
|
220
|
+
for key, value in options.items()
|
|
221
|
+
if key not in {"fromSequence", "toSequence", "limit"}
|
|
222
|
+
}
|
|
223
|
+
path = f"/api/v1/balance/gaps/{quote(customer_id, safe='')}{query}"
|
|
224
|
+
response = self._client.get(path, request_options)
|
|
225
|
+
return self._normalize_payload(response)
|
|
226
|
+
|
|
227
|
+
def queue_summary(self, summary: Dict[str, Any]) -> Dict[str, Any]:
|
|
228
|
+
return self.submission.queue_summary(summary)
|
|
229
|
+
|
|
230
|
+
def queue_summaries(self, summaries: Iterable[Dict[str, Any]]) -> list[Dict[str, Any]]:
|
|
231
|
+
return self.submission.queue_summaries(summaries)
|
|
232
|
+
|
|
233
|
+
def flush_summaries(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
234
|
+
return self.submission.flush_summaries(options)
|
|
235
|
+
|
|
236
|
+
def start_summary_scheduler(self) -> None:
|
|
237
|
+
self.submission.start()
|
|
238
|
+
|
|
239
|
+
def stop_summary_scheduler(self) -> None:
|
|
240
|
+
self.submission.stop()
|
|
241
|
+
|
|
242
|
+
def get_summary_queue_state(self) -> Dict[str, Any]:
|
|
243
|
+
return self.submission.get_queue_state()
|
|
244
|
+
|
|
245
|
+
def shutdown_summary_scheduler(self) -> None:
|
|
246
|
+
self.submission.shutdown()
|
|
247
|
+
|
|
248
|
+
def derive(
|
|
249
|
+
self,
|
|
250
|
+
customer_id: str,
|
|
251
|
+
options: Optional[Dict[str, Any]] = None,
|
|
252
|
+
) -> Dict[str, Any]:
|
|
253
|
+
options = options or {}
|
|
254
|
+
self._validate_scope_mode(options)
|
|
255
|
+
checkpoint_type = self._to_api_checkpoint_type(options.get("startingCheckpointType"))
|
|
256
|
+
query = _build_query(
|
|
257
|
+
{
|
|
258
|
+
"startingBalance": options.get("startingBalance"),
|
|
259
|
+
"startingCheckpoint": options.get("startingCheckpoint"),
|
|
260
|
+
"startingCheckpointType": checkpoint_type,
|
|
261
|
+
"limit": options.get("limit"),
|
|
262
|
+
"offset": options.get("offset"),
|
|
263
|
+
"period": options.get("period"),
|
|
264
|
+
"timePreset": options.get("timePreset"),
|
|
265
|
+
"startDate": options.get("startDate"),
|
|
266
|
+
"endDate": options.get("endDate"),
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
request_options = {
|
|
270
|
+
key: value
|
|
271
|
+
for key, value in options.items()
|
|
272
|
+
if key
|
|
273
|
+
not in {
|
|
274
|
+
"startingBalance",
|
|
275
|
+
"startingCheckpoint",
|
|
276
|
+
"startingCheckpointType",
|
|
277
|
+
"limit",
|
|
278
|
+
"offset",
|
|
279
|
+
"period",
|
|
280
|
+
"timePreset",
|
|
281
|
+
"startDate",
|
|
282
|
+
"endDate",
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
path = f"/api/v1/balance/derive/{quote(customer_id, safe='')}{query}"
|
|
286
|
+
response = self._client.get(path, request_options)
|
|
287
|
+
return self._normalize_payload(response)
|
|
288
|
+
|
|
289
|
+
def derive_current_month(
|
|
290
|
+
self,
|
|
291
|
+
customer_id: str,
|
|
292
|
+
options: Optional[Dict[str, Any]] = None,
|
|
293
|
+
) -> Dict[str, Any]:
|
|
294
|
+
merged = {**(options or {}), "timePreset": "current_month"}
|
|
295
|
+
return self.derive(customer_id, merged)
|
|
296
|
+
|
|
297
|
+
def derive_for_period(
|
|
298
|
+
self,
|
|
299
|
+
customer_id: str,
|
|
300
|
+
period: str,
|
|
301
|
+
options: Optional[Dict[str, Any]] = None,
|
|
302
|
+
) -> Dict[str, Any]:
|
|
303
|
+
merged = {**(options or {}), "period": period}
|
|
304
|
+
return self.derive(customer_id, merged)
|
|
305
|
+
|
|
306
|
+
def derive_months_back(
|
|
307
|
+
self,
|
|
308
|
+
customer_id: str,
|
|
309
|
+
months: int,
|
|
310
|
+
options: Optional[Dict[str, Any]] = None,
|
|
311
|
+
) -> Dict[str, Any]:
|
|
312
|
+
if not isinstance(months, int) or months < 1:
|
|
313
|
+
raise ValueError("months must be a positive integer.")
|
|
314
|
+
|
|
315
|
+
now = datetime.now(timezone.utc)
|
|
316
|
+
end_year = now.year + (1 if now.month == 12 else 0)
|
|
317
|
+
end_month = 1 if now.month == 12 else now.month + 1
|
|
318
|
+
end_exclusive = datetime(end_year, end_month, 1, tzinfo=timezone.utc)
|
|
319
|
+
|
|
320
|
+
start_month = now.month - (months - 1)
|
|
321
|
+
start_year = now.year
|
|
322
|
+
while start_month <= 0:
|
|
323
|
+
start_month += 12
|
|
324
|
+
start_year -= 1
|
|
325
|
+
start_inclusive = datetime(start_year, start_month, 1, tzinfo=timezone.utc)
|
|
326
|
+
|
|
327
|
+
merged = {
|
|
328
|
+
**(options or {}),
|
|
329
|
+
"timePreset": "custom",
|
|
330
|
+
"startDate": self._to_utc_iso(start_inclusive),
|
|
331
|
+
"endDate": self._to_utc_iso(end_exclusive),
|
|
332
|
+
}
|
|
333
|
+
return self.derive(customer_id, merged)
|
|
334
|
+
|
|
335
|
+
def compare(
|
|
336
|
+
self,
|
|
337
|
+
customer_id: str,
|
|
338
|
+
balances: Dict[str, Any],
|
|
339
|
+
options: Optional[Dict[str, Any]] = None,
|
|
340
|
+
) -> Dict[str, Any]:
|
|
341
|
+
options = options or {}
|
|
342
|
+
self._validate_scope_mode(options)
|
|
343
|
+
body: Dict[str, Any] = {
|
|
344
|
+
"customerId": customer_id,
|
|
345
|
+
"yourBalance": balances["yours"],
|
|
346
|
+
"theirBalance": balances["theirs"],
|
|
347
|
+
"startingBalance": options.get("startingBalance", 0),
|
|
348
|
+
}
|
|
349
|
+
if options.get("startingCheckpoint") is not None:
|
|
350
|
+
body["startingCheckpoint"] = options["startingCheckpoint"]
|
|
351
|
+
if options.get("startingCheckpointType") is not None:
|
|
352
|
+
body["startingCheckpointType"] = self._to_api_checkpoint_type(
|
|
353
|
+
options["startingCheckpointType"]
|
|
354
|
+
)
|
|
355
|
+
for key in ("period", "timePreset", "startDate", "endDate"):
|
|
356
|
+
if options.get(key) is not None:
|
|
357
|
+
body[key] = options[key]
|
|
358
|
+
|
|
359
|
+
request_options = {
|
|
360
|
+
key: value
|
|
361
|
+
for key, value in options.items()
|
|
362
|
+
if key
|
|
363
|
+
not in {
|
|
364
|
+
"startingBalance",
|
|
365
|
+
"startingCheckpoint",
|
|
366
|
+
"startingCheckpointType",
|
|
367
|
+
"period",
|
|
368
|
+
"timePreset",
|
|
369
|
+
"startDate",
|
|
370
|
+
"endDate",
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
response = self._client.post("/api/v1/balance/compare", body, request_options)
|
|
374
|
+
return self._normalize_payload(response)
|
|
375
|
+
|
|
376
|
+
def receipt(
|
|
377
|
+
self,
|
|
378
|
+
customer_id: str,
|
|
379
|
+
options: Optional[Dict[str, Any]] = None,
|
|
380
|
+
) -> Dict[str, Any]:
|
|
381
|
+
options = options or {}
|
|
382
|
+
has_scoped_receipt = any(
|
|
383
|
+
options.get(key) is not None for key in ("period", "timePreset", "startDate", "endDate")
|
|
384
|
+
)
|
|
385
|
+
if has_scoped_receipt:
|
|
386
|
+
return self._receipt_for_period(customer_id, options)
|
|
387
|
+
|
|
388
|
+
request_options = {
|
|
389
|
+
key: value
|
|
390
|
+
for key, value in options.items()
|
|
391
|
+
if key not in {"period", "timePreset", "startDate", "endDate"}
|
|
392
|
+
}
|
|
393
|
+
response = self._client.get(
|
|
394
|
+
f"/api/v1/balance/receipt/{quote(customer_id, safe='')}",
|
|
395
|
+
request_options or None,
|
|
396
|
+
)
|
|
397
|
+
return self._normalize_payload(response)
|
|
398
|
+
|
|
399
|
+
def _receipt_for_period(
|
|
400
|
+
self,
|
|
401
|
+
customer_id: str,
|
|
402
|
+
options: Optional[Dict[str, Any]] = None,
|
|
403
|
+
) -> Dict[str, Any]:
|
|
404
|
+
options = options or {}
|
|
405
|
+
query = _build_query(
|
|
406
|
+
{
|
|
407
|
+
"period": options.get("period"),
|
|
408
|
+
"timePreset": options.get("timePreset"),
|
|
409
|
+
"startDate": options.get("startDate"),
|
|
410
|
+
"endDate": options.get("endDate"),
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
request_options = {
|
|
414
|
+
key: value
|
|
415
|
+
for key, value in options.items()
|
|
416
|
+
if key not in {"period", "timePreset", "startDate", "endDate"}
|
|
417
|
+
}
|
|
418
|
+
path = f"/api/v1/balance/invoice-proof/{quote(customer_id, safe='')}{query}"
|
|
419
|
+
response = self._client.get(path, request_options)
|
|
420
|
+
return self._normalize_invoice_proof_payload(response)
|
|
421
|
+
|
|
422
|
+
def checkpoint(
|
|
423
|
+
self,
|
|
424
|
+
customer_id: Optional[str] = None,
|
|
425
|
+
options: Optional[Dict[str, Any]] = None,
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
options = options or {}
|
|
428
|
+
body: Dict[str, Any] = {}
|
|
429
|
+
if customer_id is not None:
|
|
430
|
+
body["customerId"] = customer_id
|
|
431
|
+
|
|
432
|
+
for key in ("period", "timePreset", "startDate", "endDate", "mode", "fingerprints"):
|
|
433
|
+
if options.get(key) is not None:
|
|
434
|
+
body[key] = options[key]
|
|
435
|
+
|
|
436
|
+
request_options = {
|
|
437
|
+
key: value
|
|
438
|
+
for key, value in options.items()
|
|
439
|
+
if key not in {"period", "timePreset", "startDate", "endDate", "mode", "fingerprints"}
|
|
440
|
+
}
|
|
441
|
+
response = self._client.post("/api/v1/balance/checkpoint", body, request_options)
|
|
442
|
+
return self._normalize_payload(response)
|
|
443
|
+
|
|
444
|
+
def list_checkpoints(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
445
|
+
options = options or {}
|
|
446
|
+
query = _build_query(
|
|
447
|
+
{
|
|
448
|
+
"customerId": options.get("customerId"),
|
|
449
|
+
"period": options.get("period"),
|
|
450
|
+
"timePreset": options.get("timePreset"),
|
|
451
|
+
"startDate": options.get("startDate"),
|
|
452
|
+
"endDate": options.get("endDate"),
|
|
453
|
+
"limit": options.get("limit"),
|
|
454
|
+
"offset": options.get("offset"),
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
request_options = {
|
|
458
|
+
key: value
|
|
459
|
+
for key, value in options.items()
|
|
460
|
+
if key not in {"customerId", "period", "timePreset", "startDate", "endDate", "limit", "offset"}
|
|
461
|
+
}
|
|
462
|
+
response = self._client.get(f"/api/v1/balance/checkpoints{query}", request_options)
|
|
463
|
+
return self._normalize_payload(response)
|
|
464
|
+
|
|
465
|
+
def list_webhook_deliveries(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
466
|
+
options = options or {}
|
|
467
|
+
query = _build_query(
|
|
468
|
+
{
|
|
469
|
+
"status": options.get("status"),
|
|
470
|
+
"limit": options.get("limit"),
|
|
471
|
+
"offset": options.get("offset"),
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
request_options = {
|
|
475
|
+
key: value
|
|
476
|
+
for key, value in options.items()
|
|
477
|
+
if key not in {"status", "limit", "offset"}
|
|
478
|
+
}
|
|
479
|
+
response = self._client.get(f"/api/v1/balance/webhooks{query}", request_options)
|
|
480
|
+
return self._normalize_payload(response)
|
|
481
|
+
|
|
482
|
+
def retry_webhook(
|
|
483
|
+
self,
|
|
484
|
+
event_id: str,
|
|
485
|
+
options: Optional[Dict[str, Any]] = None,
|
|
486
|
+
) -> Dict[str, Any]:
|
|
487
|
+
path = f"/api/v1/balance/webhooks/{quote(event_id, safe='')}/retry"
|
|
488
|
+
response = self._client.post(path, {}, options)
|
|
489
|
+
return self._normalize_payload(response)
|
|
490
|
+
|
|
491
|
+
def customers(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
492
|
+
options = options or {}
|
|
493
|
+
query = _build_query(
|
|
494
|
+
{
|
|
495
|
+
"search": options.get("search"),
|
|
496
|
+
"limit": options.get("limit"),
|
|
497
|
+
"page": options.get("page"),
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
request_options = {
|
|
501
|
+
key: value
|
|
502
|
+
for key, value in options.items()
|
|
503
|
+
if key not in {"search", "limit", "page"}
|
|
504
|
+
}
|
|
505
|
+
response = self._client.get(f"/api/v1/balance/customers{query}", request_options)
|
|
506
|
+
return self._normalize_payload(response)
|
|
507
|
+
|
|
508
|
+
def customer(
|
|
509
|
+
self,
|
|
510
|
+
customer_id: str,
|
|
511
|
+
options: Optional[Dict[str, Any]] = None,
|
|
512
|
+
) -> Dict[str, Any]:
|
|
513
|
+
options = options or {}
|
|
514
|
+
query = _build_query(
|
|
515
|
+
{
|
|
516
|
+
"deltaPage": options.get("deltaPage"),
|
|
517
|
+
"deltaLimit": options.get("deltaLimit"),
|
|
518
|
+
"period": options.get("period"),
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
request_options = {
|
|
522
|
+
key: value
|
|
523
|
+
for key, value in options.items()
|
|
524
|
+
if key not in {"deltaPage", "deltaLimit", "period"}
|
|
525
|
+
}
|
|
526
|
+
path = f"/api/v1/balance/customers/{quote(customer_id, safe='')}{query}"
|
|
527
|
+
response = self._client.get(path, request_options)
|
|
528
|
+
return self._normalize_payload(response)
|
|
529
|
+
|
|
530
|
+
def invoice_proof(
|
|
531
|
+
self,
|
|
532
|
+
customer_id: str,
|
|
533
|
+
options: Optional[Dict[str, Any]] = None,
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
return self._receipt_for_period(customer_id, options)
|