orionapi 2.30.2__tar.gz → 2.31.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orionapi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.31.1
|
|
4
4
|
Summary: A python interface for the Orion Advisors platform web API
|
|
5
5
|
Author: spencerogden-dsam
|
|
6
6
|
Author-email: 67068943+spencerogden-dsam@users.noreply.github.com
|
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
15
|
Requires-Dist: rapidfuzz (>=3.0.0,<4.0.0)
|
|
15
16
|
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
|
16
17
|
Requires-Dist: tabulate (>=0.8.9,<0.9.0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.31.1"
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
@@ -127,6 +127,8 @@ BILLING_RUN_FOR_TYPES = [
|
|
|
127
127
|
"SingleHHRerun",
|
|
128
128
|
"Accounts",
|
|
129
129
|
"ImportedHouseholds",
|
|
130
|
+
"ImportedHHRerun",
|
|
131
|
+
"FinalBill",
|
|
130
132
|
]
|
|
131
133
|
|
|
132
134
|
BILLING_ACCOUNT_FILTERS = ["ActiveAccounts", "InActiveAccounts", "AllAccounts"]
|
|
@@ -170,6 +172,19 @@ BILLING_BILL_TYPES = [
|
|
|
170
172
|
"AdvanceCreditDebit",
|
|
171
173
|
]
|
|
172
174
|
|
|
175
|
+
# Billing instance statusValue strings as the live API returns them. The
|
|
176
|
+
# swagger enum names differ ("BillDataFilesNeeded", "FeeFileNeeded", ...);
|
|
177
|
+
# the API sends display strings instead.
|
|
178
|
+
BILLING_STATUS_NOT_GENERATED = "Not Generated"
|
|
179
|
+
BILLING_STATUS_DATA_FILES_NEEDED = "Data Files Needed"
|
|
180
|
+
BILLING_STATUS_FEE_FILE_NEEDED = "Fee File Needed"
|
|
181
|
+
BILLING_STATUS_COMPLETE = "Complete"
|
|
182
|
+
# Instance statuses that mean something went wrong; waiting further is pointless.
|
|
183
|
+
BILLING_ERROR_STATUSES = {"Fee File Failed", "Error Deleting Instance"}
|
|
184
|
+
# Per-household (BillInstanceClient) statuses still in flight during generation.
|
|
185
|
+
BILLING_CLIENT_PENDING_STATUSES = {"NotGenerated", "PendingGeneration"}
|
|
186
|
+
|
|
187
|
+
|
|
173
188
|
# Fields of the Orion API ReceiveableSummaryDto, the body of
|
|
174
189
|
# POST /Billing/SyncCashtoEclipse. Identical to the CashFundingGridDto rows
|
|
175
190
|
# returned by get_cash_funding(), except the grid's "id" is "accountId" here.
|
|
@@ -225,6 +240,48 @@ class AmbiguousAccountError(OrionAPIError):
|
|
|
225
240
|
pass
|
|
226
241
|
|
|
227
242
|
|
|
243
|
+
class BillingGenerationError(OrionAPIError):
|
|
244
|
+
"""A billing run reached an error state (instance or household level)."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, message, instance=None, errors=None):
|
|
247
|
+
super().__init__(message)
|
|
248
|
+
self.instance = instance
|
|
249
|
+
self.errors = errors or []
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _require_positive_int_list(name, values):
|
|
253
|
+
"""Raise ValueError unless ``values`` is a non-empty list of positive ints."""
|
|
254
|
+
if not isinstance(values, list) or not values:
|
|
255
|
+
raise ValueError(f"{name} must be a non-empty list")
|
|
256
|
+
for value in values:
|
|
257
|
+
if not isinstance(value, int) or value < 1:
|
|
258
|
+
raise ValueError(f"{name} must be positive integers")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _require_poll_args(timeout, poll_interval):
|
|
262
|
+
"""Raise ValueError unless ``timeout`` and ``poll_interval`` are positive numbers."""
|
|
263
|
+
if not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
264
|
+
raise ValueError("timeout must be a positive number")
|
|
265
|
+
if not isinstance(poll_interval, (int, float)) or poll_interval <= 0:
|
|
266
|
+
raise ValueError("poll_interval must be a positive number")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _squash(value):
|
|
270
|
+
"""Remove spaces, so display strings ("Pending Generation") match enum names."""
|
|
271
|
+
return (value or "").replace(" ", "")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _json_or_none(res):
|
|
275
|
+
"""Parse a JSON response body, or return None when the body is empty.
|
|
276
|
+
|
|
277
|
+
Several Orion billing actions (e.g. Generate, PostAuditFiles) answer
|
|
278
|
+
200/204 with no body.
|
|
279
|
+
"""
|
|
280
|
+
if not res.content or not res.content.strip():
|
|
281
|
+
return None
|
|
282
|
+
return res.json()
|
|
283
|
+
|
|
284
|
+
|
|
228
285
|
class RateLimiter:
|
|
229
286
|
"""Thread-safe rate limiter to prevent API abuse.
|
|
230
287
|
|
|
@@ -366,7 +423,9 @@ class BaseAPI:
|
|
|
366
423
|
**kwargs: Additional arguments passed to the request
|
|
367
424
|
|
|
368
425
|
Returns:
|
|
369
|
-
requests.Response object
|
|
426
|
+
requests.Response object. Some mutating endpoints answer with an
|
|
427
|
+
empty body; parse those with ``_json_or_none(res)`` rather than
|
|
428
|
+
``res.json()``.
|
|
370
429
|
|
|
371
430
|
Raises:
|
|
372
431
|
AuthenticationError: On 401/403 responses
|
|
@@ -1412,14 +1471,22 @@ class OrionAPI(BaseAPI):
|
|
|
1412
1471
|
as_of_date=None,
|
|
1413
1472
|
end_date_override=None,
|
|
1414
1473
|
include_cash_flow=False,
|
|
1474
|
+
allow_duplicate_mock_bills=None,
|
|
1475
|
+
value_as_of_override=None,
|
|
1476
|
+
date_range=None,
|
|
1415
1477
|
):
|
|
1416
1478
|
"""Create a new billing instance (live or forecast).
|
|
1417
1479
|
|
|
1480
|
+
The billing period is derived from ``as_of_date`` (e.g. 2026-09-30
|
|
1481
|
+
creates a 2026-10-01..2026-12-31 quarterly instance). A new instance
|
|
1482
|
+
starts in "Not Generated"; call generate_billing() to run it.
|
|
1483
|
+
|
|
1418
1484
|
Args:
|
|
1419
1485
|
is_forecast: If True, creates a forecast/mock bill (default False)
|
|
1420
1486
|
run_for: Scope of the billing run. One of: AllHouseholds, FundFamily,
|
|
1421
1487
|
Custodian, Representative, BusinessLine, SingleHHNewBill,
|
|
1422
|
-
SingleHHRerun, Accounts, ImportedHouseholds
|
|
1488
|
+
SingleHHRerun, Accounts, ImportedHouseholds, ImportedHHRerun,
|
|
1489
|
+
FinalBill
|
|
1423
1490
|
run_for_accounts: Account filter. One of: ActiveAccounts,
|
|
1424
1491
|
InActiveAccounts, AllAccounts
|
|
1425
1492
|
bill_type: Type of bill. One of: Unknown, Renewal, NewMoney,
|
|
@@ -1429,9 +1496,14 @@ class OrionAPI(BaseAPI):
|
|
|
1429
1496
|
as_of_date: Optional as-of date (YYYY-MM-DD format)
|
|
1430
1497
|
end_date_override: Optional end date override (YYYY-MM-DD format)
|
|
1431
1498
|
include_cash_flow: Whether to include cash flow (default False)
|
|
1499
|
+
allow_duplicate_mock_bills: Optional bool, sent as
|
|
1500
|
+
allowDuplicateMockBills. Orion accepted a second single-household
|
|
1501
|
+
forecast for an already-forecast period without it (2026-09).
|
|
1502
|
+
value_as_of_override: Optional valuation date override (YYYY-MM-DD)
|
|
1503
|
+
date_range: Optional list of dates, sent as-is as dateRange
|
|
1432
1504
|
|
|
1433
1505
|
Returns:
|
|
1434
|
-
dict: Created billing instance details
|
|
1506
|
+
dict: Created billing instance details (BillInstanceDto)
|
|
1435
1507
|
"""
|
|
1436
1508
|
if run_for not in BILLING_RUN_FOR_TYPES:
|
|
1437
1509
|
raise ValueError(f"run_for must be one of {BILLING_RUN_FOR_TYPES}")
|
|
@@ -1445,6 +1517,9 @@ class OrionAPI(BaseAPI):
|
|
|
1445
1517
|
if keys is not None and not isinstance(keys, list):
|
|
1446
1518
|
raise ValueError("keys must be a list of integers")
|
|
1447
1519
|
|
|
1520
|
+
if date_range is not None and not isinstance(date_range, list):
|
|
1521
|
+
raise ValueError("date_range must be a list")
|
|
1522
|
+
|
|
1448
1523
|
payload = {
|
|
1449
1524
|
"isMockBill": is_forecast,
|
|
1450
1525
|
"runFor": run_for,
|
|
@@ -1460,6 +1535,12 @@ class OrionAPI(BaseAPI):
|
|
|
1460
1535
|
payload["asOfDate"] = as_of_date
|
|
1461
1536
|
if end_date_override is not None:
|
|
1462
1537
|
payload["endDateOverride"] = end_date_override
|
|
1538
|
+
if allow_duplicate_mock_bills is not None:
|
|
1539
|
+
payload["allowDuplicateMockBills"] = allow_duplicate_mock_bills
|
|
1540
|
+
if value_as_of_override is not None:
|
|
1541
|
+
payload["valueAsOfOverride"] = value_as_of_override
|
|
1542
|
+
if date_range is not None:
|
|
1543
|
+
payload["dateRange"] = date_range
|
|
1463
1544
|
|
|
1464
1545
|
res = self.api_request(
|
|
1465
1546
|
f"{self.base_url}/Billing/BillGenerator/Action/Instance",
|
|
@@ -1468,25 +1549,39 @@ class OrionAPI(BaseAPI):
|
|
|
1468
1549
|
)
|
|
1469
1550
|
return res.json()
|
|
1470
1551
|
|
|
1471
|
-
def generate_billing(self, instance_id, lock_down=True):
|
|
1472
|
-
"""
|
|
1552
|
+
def generate_billing(self, instance_id, lock_down=True, ids=None):
|
|
1553
|
+
"""Start bill generation for a billing instance.
|
|
1554
|
+
|
|
1555
|
+
Generation runs in the background and returns immediately with an
|
|
1556
|
+
empty body. Use wait_for_billing_instance() to block until it finishes.
|
|
1473
1557
|
|
|
1474
1558
|
Args:
|
|
1475
1559
|
instance_id: Billing instance ID
|
|
1476
1560
|
lock_down: Whether to lock down the instance during generation (default True)
|
|
1561
|
+
ids: Optional list of household-record IDs to limit the run to:
|
|
1562
|
+
the ``id`` of get_billing_instance_clients() rows, NOT client
|
|
1563
|
+
IDs (a client ID gets 404 "No available bills found"). Sent as
|
|
1564
|
+
a bare int array body. Omitted, no body is sent and the whole
|
|
1565
|
+
instance is generated. To rerun one household, delete its bill
|
|
1566
|
+
(delete_bills) and pass its record id here.
|
|
1477
1567
|
|
|
1478
1568
|
Returns:
|
|
1479
|
-
dict:
|
|
1569
|
+
dict | None: Parsed response, or None when Orion returns an empty
|
|
1570
|
+
body (the normal case).
|
|
1480
1571
|
"""
|
|
1481
1572
|
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1482
1573
|
raise ValueError("instance_id must be a positive integer")
|
|
1574
|
+
if ids is not None and (not isinstance(ids, list) or not ids):
|
|
1575
|
+
raise ValueError("ids must be a non-empty list of integers")
|
|
1483
1576
|
|
|
1484
1577
|
params = urlencode({"lockDown": str(lock_down).lower()})
|
|
1578
|
+
kwargs = {"json": ids} if ids is not None else {}
|
|
1485
1579
|
res = self.api_request(
|
|
1486
1580
|
f"{self.base_url}/Billing/Instances/{instance_id}/Action/Generate?{params}",
|
|
1487
1581
|
requests.put,
|
|
1582
|
+
**kwargs,
|
|
1488
1583
|
)
|
|
1489
|
-
return res
|
|
1584
|
+
return _json_or_none(res)
|
|
1490
1585
|
|
|
1491
1586
|
def complete_billing_instance(self, instance_id):
|
|
1492
1587
|
"""Finalize/complete a billing instance.
|
|
@@ -1506,23 +1601,24 @@ class OrionAPI(BaseAPI):
|
|
|
1506
1601
|
)
|
|
1507
1602
|
return res.json()
|
|
1508
1603
|
|
|
1509
|
-
def invalidate_billing_instance(self, instance_id):
|
|
1604
|
+
def invalidate_billing_instance(self, instance_id, validate=None):
|
|
1510
1605
|
"""Invalidate/cancel a billing instance.
|
|
1511
1606
|
|
|
1512
1607
|
Args:
|
|
1513
1608
|
instance_id: Billing instance ID
|
|
1609
|
+
validate: Optional bool, sent as the ``validate`` query param
|
|
1514
1610
|
|
|
1515
1611
|
Returns:
|
|
1516
|
-
dict: Updated billing instance
|
|
1612
|
+
dict | None: Updated billing instance, or None on an empty body
|
|
1517
1613
|
"""
|
|
1518
1614
|
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1519
1615
|
raise ValueError("instance_id must be a positive integer")
|
|
1520
1616
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
)
|
|
1525
|
-
return res
|
|
1617
|
+
url = f"{self.base_url}/Billing/Instances/{instance_id}/Action/Invalidate"
|
|
1618
|
+
if validate is not None:
|
|
1619
|
+
url += "?" + urlencode({"validate": str(bool(validate)).lower()})
|
|
1620
|
+
res = self.api_request(url, requests.post)
|
|
1621
|
+
return _json_or_none(res)
|
|
1526
1622
|
|
|
1527
1623
|
def generate_fee_files(self, instance_id, custodian_id=None):
|
|
1528
1624
|
"""Generate fee files for a billing instance.
|
|
@@ -1531,8 +1627,12 @@ class OrionAPI(BaseAPI):
|
|
|
1531
1627
|
instance_id: Billing instance ID
|
|
1532
1628
|
custodian_id: Optional custodian ID to filter fee files
|
|
1533
1629
|
|
|
1630
|
+
The instance must be in "Fee File Needed" status; otherwise Orion
|
|
1631
|
+
returns 400 "You cannot generate fee file. The status needs to be Fee
|
|
1632
|
+
File Needed".
|
|
1633
|
+
|
|
1534
1634
|
Returns:
|
|
1535
|
-
dict: Fee file generation result
|
|
1635
|
+
dict | None: Fee file generation result, or None on an empty body
|
|
1536
1636
|
"""
|
|
1537
1637
|
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1538
1638
|
raise ValueError("instance_id must be a positive integer")
|
|
@@ -1543,8 +1643,10 @@ class OrionAPI(BaseAPI):
|
|
|
1543
1643
|
raise ValueError("custodian_id must be a positive integer")
|
|
1544
1644
|
url += "?" + urlencode({"custodianId": custodian_id})
|
|
1545
1645
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1646
|
+
# Bare int array. Live-verified 2026-09: {"ids": [...]} gets a generic
|
|
1647
|
+
# 501 "An error has occurred"; the bare array reaches Orion's status check.
|
|
1648
|
+
res = self.api_request(url, requests.post, json=[instance_id])
|
|
1649
|
+
return _json_or_none(res)
|
|
1548
1650
|
|
|
1549
1651
|
def get_fee_files(self, instance_id):
|
|
1550
1652
|
"""Get fee files for a billing instance.
|
|
@@ -1561,6 +1663,318 @@ class OrionAPI(BaseAPI):
|
|
|
1561
1663
|
res = self.api_request(f"{self.base_url}/Billing/FeeFile/instance/{instance_id}")
|
|
1562
1664
|
return res.json()
|
|
1563
1665
|
|
|
1666
|
+
def get_billing_instance_clients(self, instance_id):
|
|
1667
|
+
"""Get the per-household records of a billing instance.
|
|
1668
|
+
|
|
1669
|
+
Each record carries the household's generation ``status`` (one of
|
|
1670
|
+
NotGenerated, Generated, Errored, PendingGeneration, OnHold, Warning,
|
|
1671
|
+
FeeFileError, ReconError), ``errorMessage``, ``billId`` and amounts.
|
|
1672
|
+
This is where generation errors show up; the instance's own
|
|
1673
|
+
statusValue has no generation-error state.
|
|
1674
|
+
|
|
1675
|
+
Args:
|
|
1676
|
+
instance_id: Billing instance ID
|
|
1677
|
+
|
|
1678
|
+
Returns:
|
|
1679
|
+
list: BillInstanceClient records
|
|
1680
|
+
"""
|
|
1681
|
+
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1682
|
+
raise ValueError("instance_id must be a positive integer")
|
|
1683
|
+
|
|
1684
|
+
res = self.api_request(
|
|
1685
|
+
f"{self.base_url}/Billing/BillGenerator/Instance/{instance_id}/ClientList"
|
|
1686
|
+
)
|
|
1687
|
+
return res.json()
|
|
1688
|
+
|
|
1689
|
+
def wait_for_billing_instance(
|
|
1690
|
+
self,
|
|
1691
|
+
instance_id,
|
|
1692
|
+
target_statuses=(BILLING_STATUS_DATA_FILES_NEEDED,),
|
|
1693
|
+
timeout=600,
|
|
1694
|
+
poll_interval=10,
|
|
1695
|
+
raise_on_client_errors=True,
|
|
1696
|
+
):
|
|
1697
|
+
"""Block until a billing instance reaches one of ``target_statuses``.
|
|
1698
|
+
|
|
1699
|
+
Generation has no job id, so the only completion signal is the
|
|
1700
|
+
instance's statusValue plus the per-household statuses. A generated
|
|
1701
|
+
instance moves from "Not Generated" to "Data Files Needed", which is
|
|
1702
|
+
the default target (a one-household forecast took ~15s in 2026-09).
|
|
1703
|
+
On a rerun the instance can already sit at "Data Files Needed", so the
|
|
1704
|
+
wait also requires that no household is still "Not Generated" /
|
|
1705
|
+
"Pending Generation". Call it right after generate_billing(). The live
|
|
1706
|
+
API returns display strings ("Data Files Needed"), not the swagger's
|
|
1707
|
+
enum names ("BillDataFilesNeeded").
|
|
1708
|
+
|
|
1709
|
+
Args:
|
|
1710
|
+
instance_id: Billing instance ID
|
|
1711
|
+
target_statuses: statusValue strings that count as done
|
|
1712
|
+
timeout: Wall-clock timeout in seconds (default 600)
|
|
1713
|
+
poll_interval: Seconds between polls (default 10)
|
|
1714
|
+
raise_on_client_errors: If True (default), once done, raise if any
|
|
1715
|
+
household has status "Errored".
|
|
1716
|
+
"Warning" (e.g. "No receivables with value.") is not an error.
|
|
1717
|
+
|
|
1718
|
+
Returns:
|
|
1719
|
+
dict: The final billing instance
|
|
1720
|
+
|
|
1721
|
+
Raises:
|
|
1722
|
+
ValueError: on invalid args
|
|
1723
|
+
BillingGenerationError: if the instance enters an error status
|
|
1724
|
+
("Fee File Failed", "Error Deleting Instance"); if every
|
|
1725
|
+
household finished, some errored, and the instance never
|
|
1726
|
+
reached a target; or (with raise_on_client_errors) if the
|
|
1727
|
+
target is reached with errored households. ``.errors`` holds
|
|
1728
|
+
the errored household records.
|
|
1729
|
+
TimeoutError: if no target status is reached within ``timeout``
|
|
1730
|
+
"""
|
|
1731
|
+
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1732
|
+
raise ValueError("instance_id must be a positive integer")
|
|
1733
|
+
if isinstance(target_statuses, str):
|
|
1734
|
+
target_statuses = (target_statuses,)
|
|
1735
|
+
if not target_statuses:
|
|
1736
|
+
raise ValueError("target_statuses must be a non-empty list of statuses")
|
|
1737
|
+
_require_poll_args(timeout, poll_interval)
|
|
1738
|
+
|
|
1739
|
+
deadline = time.monotonic() + timeout
|
|
1740
|
+
while True:
|
|
1741
|
+
instance = self.get_billing_instance(instance_id)
|
|
1742
|
+
status = instance.get("statusValue")
|
|
1743
|
+
|
|
1744
|
+
if status in BILLING_ERROR_STATUSES:
|
|
1745
|
+
raise BillingGenerationError(
|
|
1746
|
+
f"Billing instance {instance_id} is in error status {status!r}",
|
|
1747
|
+
instance=instance,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
# Household statuses come back as display strings ("Pending
|
|
1751
|
+
# Generation"); compare with spaces removed.
|
|
1752
|
+
clients = self.get_billing_instance_clients(instance_id)
|
|
1753
|
+
pending = any(
|
|
1754
|
+
_squash(c.get("status")) in BILLING_CLIENT_PENDING_STATUSES for c in clients
|
|
1755
|
+
)
|
|
1756
|
+
errors = [c for c in clients if _squash(c.get("status")) == "Errored"]
|
|
1757
|
+
|
|
1758
|
+
done = status in target_statuses
|
|
1759
|
+
# Raise on errored households once none are pending: at the target
|
|
1760
|
+
# (unless told not to), or when the instance never got there and
|
|
1761
|
+
# further polling won't help.
|
|
1762
|
+
if not pending and errors and (raise_on_client_errors or not done):
|
|
1763
|
+
verb = "reached" if done else "stayed"
|
|
1764
|
+
raise BillingGenerationError(
|
|
1765
|
+
f"Billing instance {instance_id} {verb} {status!r} with "
|
|
1766
|
+
f"{len(errors)} errored household(s)",
|
|
1767
|
+
instance=instance,
|
|
1768
|
+
errors=errors,
|
|
1769
|
+
)
|
|
1770
|
+
if not pending and done:
|
|
1771
|
+
return instance
|
|
1772
|
+
|
|
1773
|
+
if time.monotonic() >= deadline:
|
|
1774
|
+
raise TimeoutError(
|
|
1775
|
+
f"Billing instance {instance_id} still {status!r} after {timeout}s; "
|
|
1776
|
+
f"waiting for one of {list(target_statuses)}"
|
|
1777
|
+
)
|
|
1778
|
+
time.sleep(poll_interval)
|
|
1779
|
+
|
|
1780
|
+
def cancel_billing_generation(self, instance_id):
|
|
1781
|
+
"""Cancel a running bill generation job.
|
|
1782
|
+
|
|
1783
|
+
Not live-verified: a one-household forecast finishes generating in
|
|
1784
|
+
~15s, before there is anything to cancel.
|
|
1785
|
+
|
|
1786
|
+
Args:
|
|
1787
|
+
instance_id: Billing instance ID
|
|
1788
|
+
|
|
1789
|
+
Returns:
|
|
1790
|
+
dict | None: Parsed response, or None on an empty body
|
|
1791
|
+
"""
|
|
1792
|
+
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1793
|
+
raise ValueError("instance_id must be a positive integer")
|
|
1794
|
+
|
|
1795
|
+
res = self.api_request(
|
|
1796
|
+
f"{self.base_url}/Billing/Instances/{instance_id}/Action/Generate/Cancel",
|
|
1797
|
+
requests.put,
|
|
1798
|
+
)
|
|
1799
|
+
return _json_or_none(res)
|
|
1800
|
+
|
|
1801
|
+
def delete_billing_instances(self, instance_ids, forecast_only=True):
|
|
1802
|
+
"""Delete billing instances. Irreversible.
|
|
1803
|
+
|
|
1804
|
+
Deletion is asynchronous but quick: verified 2026-09 on a
|
|
1805
|
+
one-household forecast, the instance read "Deletion In Progress"
|
|
1806
|
+
immediately and was gone within ~5s, after which
|
|
1807
|
+
get_billing_instance() raises OrionAPIError 400 "Unable to find Bill
|
|
1808
|
+
Instance" (not NotFoundError/404). "Error Deleting Instance" is the
|
|
1809
|
+
failure status.
|
|
1810
|
+
|
|
1811
|
+
The instance's audit files are NOT deleted with it; remove them first
|
|
1812
|
+
with get_audit_files(instance_id=...) and delete_audit_file().
|
|
1813
|
+
|
|
1814
|
+
Args:
|
|
1815
|
+
instance_ids: List of billing instance IDs
|
|
1816
|
+
forecast_only: If True (default), fetch each instance first and
|
|
1817
|
+
refuse (ValueError, nothing deleted) unless every one is a
|
|
1818
|
+
forecast (``isMockBill``). ``instanceType`` is not used for
|
|
1819
|
+
this check because it reads "Live" on a forecast until the
|
|
1820
|
+
forecast has been generated. Pass False to delete live
|
|
1821
|
+
instances.
|
|
1822
|
+
|
|
1823
|
+
Returns:
|
|
1824
|
+
list: IDs Orion accepted for deletion
|
|
1825
|
+
"""
|
|
1826
|
+
_require_positive_int_list("instance_ids", instance_ids)
|
|
1827
|
+
|
|
1828
|
+
if forecast_only:
|
|
1829
|
+
live = [i for i in instance_ids if not self.get_billing_instance(i).get("isMockBill")]
|
|
1830
|
+
if live:
|
|
1831
|
+
raise ValueError(
|
|
1832
|
+
f"Refusing to delete non-forecast billing instance(s) {live}; "
|
|
1833
|
+
"pass forecast_only=False to delete live instances"
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
res = self.api_request(
|
|
1837
|
+
f"{self.base_url}/Billing/Instances/Action/Delete",
|
|
1838
|
+
requests.put,
|
|
1839
|
+
json=instance_ids,
|
|
1840
|
+
)
|
|
1841
|
+
return _json_or_none(res)
|
|
1842
|
+
|
|
1843
|
+
def generate_audit_files(self, instance_ids, start_date=None, end_date=None, entity_list=None):
|
|
1844
|
+
"""Generate the billing audit files for billing instance(s).
|
|
1845
|
+
|
|
1846
|
+
Works on forecast instances. For one instance Orion produced three
|
|
1847
|
+
files within ~10s: "billing audit.CSV", "billing data.XLSX" and
|
|
1848
|
+
"payable summary data.XLSX". The call returns before they exist; poll
|
|
1849
|
+
get_audit_files() to see them.
|
|
1850
|
+
|
|
1851
|
+
Args:
|
|
1852
|
+
instance_ids: List of billing instance IDs (sent to Orion as a
|
|
1853
|
+
comma-separated string, billInstanceIds)
|
|
1854
|
+
start_date: Optional start of the instance date range (YYYY-MM-DD)
|
|
1855
|
+
end_date: Optional end of the instance date range (YYYY-MM-DD)
|
|
1856
|
+
entity_list: Optional list of payable entities to filter accounts on
|
|
1857
|
+
|
|
1858
|
+
Returns:
|
|
1859
|
+
None: Orion answers 204 No Content
|
|
1860
|
+
"""
|
|
1861
|
+
_require_positive_int_list("instance_ids", instance_ids)
|
|
1862
|
+
|
|
1863
|
+
# billData/payableSummary/billingAudit are marked UNUSED in the spec.
|
|
1864
|
+
payload = {"billInstanceIds": ",".join(str(i) for i in instance_ids)}
|
|
1865
|
+
if start_date is not None:
|
|
1866
|
+
payload["startDate"] = start_date
|
|
1867
|
+
if end_date is not None:
|
|
1868
|
+
payload["endDate"] = end_date
|
|
1869
|
+
if entity_list is not None:
|
|
1870
|
+
payload["entityList"] = entity_list
|
|
1871
|
+
|
|
1872
|
+
res = self.api_request(
|
|
1873
|
+
f"{self.base_url}/Billing/PostAuditFiles", requests.post, json=payload
|
|
1874
|
+
)
|
|
1875
|
+
return _json_or_none(res)
|
|
1876
|
+
|
|
1877
|
+
def get_audit_files(self, instance_id=None, start_date=None, end_date=None):
|
|
1878
|
+
"""List billing audit files.
|
|
1879
|
+
|
|
1880
|
+
``blobText`` is null in the listing; fetch content with
|
|
1881
|
+
download_audit_file(file["id"]).
|
|
1882
|
+
|
|
1883
|
+
Args:
|
|
1884
|
+
instance_id: Optional billing instance ID to filter by
|
|
1885
|
+
start_date: Optional start date (YYYY-MM-DD)
|
|
1886
|
+
end_date: Optional end date (YYYY-MM-DD)
|
|
1887
|
+
|
|
1888
|
+
Returns:
|
|
1889
|
+
list: AuditFileBlobDto records (id, instanceId, fileName, blobDesc,
|
|
1890
|
+
openWith, isZip, createdDate, startDate, endDate, ...)
|
|
1891
|
+
"""
|
|
1892
|
+
params = {}
|
|
1893
|
+
if instance_id is not None:
|
|
1894
|
+
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1895
|
+
raise ValueError("instance_id must be a positive integer")
|
|
1896
|
+
params["billInstanceId"] = instance_id
|
|
1897
|
+
if start_date is not None:
|
|
1898
|
+
params["startDate"] = start_date
|
|
1899
|
+
if end_date is not None:
|
|
1900
|
+
params["endDate"] = end_date
|
|
1901
|
+
|
|
1902
|
+
url = f"{self.base_url}/Billing/Audit/AuditFiles"
|
|
1903
|
+
if params:
|
|
1904
|
+
url += "?" + urlencode(params)
|
|
1905
|
+
res = self.api_request(url)
|
|
1906
|
+
return res.json()
|
|
1907
|
+
|
|
1908
|
+
def download_audit_file(self, file_id):
|
|
1909
|
+
"""Download one billing audit file.
|
|
1910
|
+
|
|
1911
|
+
Args:
|
|
1912
|
+
file_id: Audit file ID (``id`` from get_audit_files)
|
|
1913
|
+
|
|
1914
|
+
Returns:
|
|
1915
|
+
dict: ``content`` (bytes), ``filename`` (from Content-Disposition,
|
|
1916
|
+
or None) and ``content_type``. CSVs come back as
|
|
1917
|
+
application/octet-stream, XLSX as the OpenXML spreadsheet type.
|
|
1918
|
+
"""
|
|
1919
|
+
if not isinstance(file_id, int) or file_id < 1:
|
|
1920
|
+
raise ValueError("file_id must be a positive integer")
|
|
1921
|
+
|
|
1922
|
+
res = self.api_request(f"{self.base_url}/Billing/Audit/{file_id}/File")
|
|
1923
|
+
disposition = res.headers.get("Content-Disposition") or ""
|
|
1924
|
+
match = re.search(r'filename="?([^";]+)"?', disposition)
|
|
1925
|
+
return {
|
|
1926
|
+
"content": res.content,
|
|
1927
|
+
"filename": match.group(1) if match else None,
|
|
1928
|
+
"content_type": res.headers.get("Content-Type"),
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
def delete_audit_file(self, file_id):
|
|
1932
|
+
"""Delete one billing audit file.
|
|
1933
|
+
|
|
1934
|
+
Args:
|
|
1935
|
+
file_id: Audit file ID (``id`` from get_audit_files)
|
|
1936
|
+
|
|
1937
|
+
Returns:
|
|
1938
|
+
None: Orion answers with an empty body (verified 2026-09)
|
|
1939
|
+
"""
|
|
1940
|
+
if not isinstance(file_id, int) or file_id < 1:
|
|
1941
|
+
raise ValueError("file_id must be a positive integer")
|
|
1942
|
+
|
|
1943
|
+
res = self.api_request(f"{self.base_url}/Billing/Audit/{file_id}/File", requests.delete)
|
|
1944
|
+
return _json_or_none(res)
|
|
1945
|
+
|
|
1946
|
+
def get_bill_data_export(self, instance_id=None, start_date=None, end_date=None):
|
|
1947
|
+
"""Get the bill data export (Orion: "list of unpaid bills").
|
|
1948
|
+
|
|
1949
|
+
Returns unpaid live bills only. Verified 2026-09: it returns [] for a
|
|
1950
|
+
generated forecast instance, so for forecast rows use get_bills(
|
|
1951
|
+
instance_id=...) or the "billing data.XLSX" audit file
|
|
1952
|
+
(generate_audit_files / download_audit_file) instead.
|
|
1953
|
+
|
|
1954
|
+
Args:
|
|
1955
|
+
instance_id: Optional billing instance ID to filter by
|
|
1956
|
+
start_date: Optional start date (YYYY-MM-DD)
|
|
1957
|
+
end_date: Optional end date (YYYY-MM-DD)
|
|
1958
|
+
|
|
1959
|
+
Returns:
|
|
1960
|
+
list: BillDataDto records
|
|
1961
|
+
"""
|
|
1962
|
+
params = {}
|
|
1963
|
+
if instance_id is not None:
|
|
1964
|
+
if not isinstance(instance_id, int) or instance_id < 1:
|
|
1965
|
+
raise ValueError("instance_id must be a positive integer")
|
|
1966
|
+
params["billInstanceId"] = instance_id
|
|
1967
|
+
if start_date is not None:
|
|
1968
|
+
params["startDate"] = start_date
|
|
1969
|
+
if end_date is not None:
|
|
1970
|
+
params["endDate"] = end_date
|
|
1971
|
+
|
|
1972
|
+
url = f"{self.base_url}/Billing/BillDataExport"
|
|
1973
|
+
if params:
|
|
1974
|
+
url += "?" + urlencode(params)
|
|
1975
|
+
res = self.api_request(url)
|
|
1976
|
+
return res.json()
|
|
1977
|
+
|
|
1564
1978
|
def get_bills(self, instance_id=None, is_valid=None, bill_type=None):
|
|
1565
1979
|
"""Get bills, optionally filtered.
|
|
1566
1980
|
|
|
@@ -1778,8 +2192,17 @@ class OrionAPI(BaseAPI):
|
|
|
1778
2192
|
"""Export one cash funding row to Eclipse as a cash set-aside.
|
|
1779
2193
|
|
|
1780
2194
|
This is the API equivalent of the Cash Funding grid's export action:
|
|
1781
|
-
Orion creates
|
|
1782
|
-
|
|
2195
|
+
Orion creates a set-aside in Eclipse for the account's balance due,
|
|
2196
|
+
so the fee cash is reserved from trading.
|
|
2197
|
+
|
|
2198
|
+
Every call INSERTS a new set-aside; it never updates an existing one.
|
|
2199
|
+
Live-verified 2026-09: two calls for the same account left two active
|
|
2200
|
+
set-asides. Each is a dollar amount with description "OC to Eclipse
|
|
2201
|
+
Sync" and expiration type "None" (it never expires on its own). To
|
|
2202
|
+
re-export, first retire the account's earlier active "OC to Eclipse
|
|
2203
|
+
Sync" set-asides. Prefer Eclipse expire_set_asides(), which keeps them
|
|
2204
|
+
as inactive history, over delete_account_set_aside_cash(), which
|
|
2205
|
+
removes them.
|
|
1783
2206
|
|
|
1784
2207
|
Orion documents this as a draft endpoint that takes a single account
|
|
1785
2208
|
per call, so exporting a whole report means looping over the rows of
|
|
@@ -1835,7 +2258,9 @@ class OrionAPI(BaseAPI):
|
|
|
1835
2258
|
if delete_related_households:
|
|
1836
2259
|
url += "?" + urlencode({"deleteRelatedHouseholds": "true"})
|
|
1837
2260
|
|
|
1838
|
-
|
|
2261
|
+
# Bare int array. Live-verified 2026-09: {"ids": [...]} gets 400
|
|
2262
|
+
# "Bill Ids are missing."; the bare array deletes and echoes the ids.
|
|
2263
|
+
res = self.api_request(url, requests.put, json=bill_ids)
|
|
1839
2264
|
return res.json()
|
|
1840
2265
|
|
|
1841
2266
|
# -------------------------------------------------------------------------
|
|
@@ -2119,10 +2544,7 @@ class OrionAPI(BaseAPI):
|
|
|
2119
2544
|
"""
|
|
2120
2545
|
if not isinstance(batch_id, int) or batch_id < 1:
|
|
2121
2546
|
raise ValueError("batch_id must be a positive integer")
|
|
2122
|
-
|
|
2123
|
-
raise ValueError("timeout must be a positive number")
|
|
2124
|
-
if not isinstance(poll_interval, (int, float)) or poll_interval <= 0:
|
|
2125
|
-
raise ValueError("poll_interval must be a positive number")
|
|
2547
|
+
_require_poll_args(timeout, poll_interval)
|
|
2126
2548
|
|
|
2127
2549
|
deadline = time.monotonic() + timeout
|
|
2128
2550
|
last_reported = None
|
|
@@ -2628,11 +3050,13 @@ class EclipseV1(EclipseBase):
|
|
|
2628
3050
|
max_amount: Maximum cash amount (default 0.0)
|
|
2629
3051
|
description: Description of the set-aside
|
|
2630
3052
|
cash_type: '$' for dollar amount, '%' for percentage (default '$')
|
|
2631
|
-
start_date:
|
|
3053
|
+
start_date: Optional start date (YYYY-MM-DD)
|
|
2632
3054
|
expire_type: 'None', 'Date', or 'Transaction' (default 'None')
|
|
2633
3055
|
expire_date: Expiration date (if expire_type='Date')
|
|
2634
3056
|
expire_trans_tol: Transaction tolerance value (default 0)
|
|
2635
|
-
expire_trans_type:
|
|
3057
|
+
expire_trans_type: For expire_type='Transaction', the transaction
|
|
3058
|
+
that expires it: 1 or 'Distribution / Merge Out', 3 or 'Fee'
|
|
3059
|
+
(default 1). Orion's billing export uses 'Fee' with tolerance 15.
|
|
2636
3060
|
deplete_over_time: Maps to the UI "Deplete Over Time" checkbox
|
|
2637
3061
|
(``isDepleteOverTime``). Only meaningful when
|
|
2638
3062
|
expire_type='Transaction' (default False)
|
|
@@ -2691,8 +3115,10 @@ class EclipseV1(EclipseBase):
|
|
|
2691
3115
|
if expire_type == 1:
|
|
2692
3116
|
expire_value = expire_date
|
|
2693
3117
|
elif expire_type == 2:
|
|
2694
|
-
# Transaction
|
|
2695
|
-
|
|
3118
|
+
# For Transaction expiration, expirationValue carries the transaction
|
|
3119
|
+
# type id (3 = Fee). Live-verified 2026-09: Eclipse ignores a separate
|
|
3120
|
+
# transactionTypeId and stores no type when expirationValue is 0.
|
|
3121
|
+
expire_value = expire_trans_type
|
|
2696
3122
|
elif expire_type == 3:
|
|
2697
3123
|
expire_value = 0
|
|
2698
3124
|
|
|
@@ -2704,22 +3130,24 @@ class EclipseV1(EclipseBase):
|
|
|
2704
3130
|
if isinstance(percent_calc_type, str):
|
|
2705
3131
|
percent_calc_type = percent_calc_type_map[percent_calc_type]
|
|
2706
3132
|
|
|
3133
|
+
payload = {
|
|
3134
|
+
"cashAmountTypeId": cash_type,
|
|
3135
|
+
"cashAmount": float(amount),
|
|
3136
|
+
"minCashAmount": float(min_amount),
|
|
3137
|
+
"maxCashAmount": float(max_amount),
|
|
3138
|
+
"expirationTypeId": expire_type,
|
|
3139
|
+
"expirationValue": expire_value,
|
|
3140
|
+
"toleranceValue": expire_trans_tol,
|
|
3141
|
+
"isDepleteOverTime": bool(deplete_over_time),
|
|
3142
|
+
"description": description,
|
|
3143
|
+
"percentCalculationTypeId": percent_calc_type,
|
|
3144
|
+
}
|
|
3145
|
+
if start_date is not None:
|
|
3146
|
+
payload["startDate"] = start_date
|
|
2707
3147
|
res = self.api_request(
|
|
2708
3148
|
f"{self.base_url}/account/accounts/{account_id}/asidecash",
|
|
2709
3149
|
requests.post,
|
|
2710
|
-
json=
|
|
2711
|
-
"cashAmountTypeId": cash_type,
|
|
2712
|
-
"cashAmount": float(amount),
|
|
2713
|
-
"minCashAmount": float(min_amount),
|
|
2714
|
-
"maxCashAmount": float(max_amount),
|
|
2715
|
-
"expirationTypeId": expire_type,
|
|
2716
|
-
"expirationValue": expire_value,
|
|
2717
|
-
"toleranceValue": expire_trans_tol,
|
|
2718
|
-
"transactionTypeId": expire_trans_type,
|
|
2719
|
-
"isDepleteOverTime": bool(deplete_over_time),
|
|
2720
|
-
"description": description,
|
|
2721
|
-
"percentCalculationTypeId": percent_calc_type,
|
|
2722
|
-
},
|
|
3150
|
+
json=payload,
|
|
2723
3151
|
)
|
|
2724
3152
|
result = res.json()
|
|
2725
3153
|
self._maybe_wait_for_analytics(sync)
|
|
@@ -9248,44 +9676,60 @@ class EclipseV2(EclipseBase):
|
|
|
9248
9676
|
return res.json()
|
|
9249
9677
|
|
|
9250
9678
|
def billing_set_aside_cash(self, payload):
|
|
9251
|
-
"""Create billing set-aside cash (mutating).
|
|
9679
|
+
"""Create or update billing set-aside cash (mutating).
|
|
9680
|
+
|
|
9681
|
+
This is the endpoint behind Orion's Cash Funding Export.
|
|
9252
9682
|
|
|
9253
9683
|
Args:
|
|
9254
|
-
payload:
|
|
9684
|
+
payload: List of ``{"orionConnectExternalAccountId": int,
|
|
9685
|
+
"orionConnectFirmId": int, "amount": float}`` (dollar amount)
|
|
9255
9686
|
|
|
9256
9687
|
Returns:
|
|
9257
|
-
dict:
|
|
9688
|
+
dict | None: Parsed response, or None on the empty body the
|
|
9689
|
+
endpoint normally returns
|
|
9258
9690
|
"""
|
|
9259
9691
|
res = self.api_request(
|
|
9260
9692
|
f"{self.base_url_v2}/SetAsideCash/BillingSetAsideCash", requests.post, json=payload
|
|
9261
9693
|
)
|
|
9262
|
-
return res
|
|
9694
|
+
return _json_or_none(res)
|
|
9263
9695
|
|
|
9264
9696
|
def delete_account_set_aside_cash(self, payload):
|
|
9265
9697
|
"""Delete account set-aside cash (mutating).
|
|
9266
9698
|
|
|
9267
9699
|
Args:
|
|
9268
|
-
payload:
|
|
9700
|
+
payload: ``{"setAsideIds": [...], "skipAnalytics": bool}``
|
|
9701
|
+
(skipAnalytics optional)
|
|
9702
|
+
|
|
9703
|
+
Returns:
|
|
9704
|
+
dict | None: Parsed response, or None on an empty body. The
|
|
9705
|
+
account variant was live-verified (2026-09) to answer with an
|
|
9706
|
+
empty body.
|
|
9269
9707
|
"""
|
|
9270
9708
|
res = self.api_request(
|
|
9271
9709
|
f"{self.base_url_v2}/SetAsideCash/DeleteAccountSetAsideCash",
|
|
9272
9710
|
requests.post,
|
|
9273
9711
|
json=payload,
|
|
9274
9712
|
)
|
|
9275
|
-
return res
|
|
9713
|
+
return _json_or_none(res)
|
|
9276
9714
|
|
|
9277
9715
|
def delete_portfolio_set_aside_cash(self, payload):
|
|
9278
9716
|
"""Delete portfolio set-aside cash (mutating).
|
|
9279
9717
|
|
|
9280
9718
|
Args:
|
|
9281
|
-
payload:
|
|
9719
|
+
payload: ``{"setAsideIds": [...], "skipAnalytics": bool}``
|
|
9720
|
+
(skipAnalytics optional)
|
|
9721
|
+
|
|
9722
|
+
Returns:
|
|
9723
|
+
dict | None: Parsed response, or None on an empty body. The
|
|
9724
|
+
account variant was live-verified (2026-09) to answer with an
|
|
9725
|
+
empty body.
|
|
9282
9726
|
"""
|
|
9283
9727
|
res = self.api_request(
|
|
9284
9728
|
f"{self.base_url_v2}/SetAsideCash/DeletePortfolioSetAsideCash",
|
|
9285
9729
|
requests.post,
|
|
9286
9730
|
json=payload,
|
|
9287
9731
|
)
|
|
9288
|
-
return res
|
|
9732
|
+
return _json_or_none(res)
|
|
9289
9733
|
|
|
9290
9734
|
# =========================================================================
|
|
9291
9735
|
# Data-management CRUD / actions (v2). Account, portfolio, and model data
|
|
@@ -9358,10 +9802,16 @@ class EclipseV2(EclipseBase):
|
|
|
9358
9802
|
return res.json()
|
|
9359
9803
|
|
|
9360
9804
|
def expire_account_set_asides(self, payload):
|
|
9361
|
-
"""Expire account set-asides (mutating).
|
|
9805
|
+
"""Expire account set-asides (mutating). Raw form of expire_set_asides().
|
|
9362
9806
|
|
|
9363
9807
|
Args:
|
|
9364
|
-
payload:
|
|
9808
|
+
payload: List of ``{"setAsideId": int, "setAsideTransactions":
|
|
9809
|
+
[{"transactionId": int, "transactionExternalId": int}]}``
|
|
9810
|
+
|
|
9811
|
+
Returns:
|
|
9812
|
+
list: One result per set-aside (setAsideId, accountId,
|
|
9813
|
+
systemExpiredOn, errorMessage, ...). Failures come back as 200
|
|
9814
|
+
with a non-empty errorMessage.
|
|
9365
9815
|
"""
|
|
9366
9816
|
res = self.api_request(
|
|
9367
9817
|
f"{self.base_url_v2}/Account/Accounts/expireAccountSetAsides",
|
|
@@ -9370,6 +9820,41 @@ class EclipseV2(EclipseBase):
|
|
|
9370
9820
|
)
|
|
9371
9821
|
return res.json()
|
|
9372
9822
|
|
|
9823
|
+
def expire_set_asides(self, set_aside_ids, raise_on_error=True):
|
|
9824
|
+
"""Expire account set-asides now, keeping them as history.
|
|
9825
|
+
|
|
9826
|
+
Unlike delete_account_set_aside_cash(), an expired set-aside stays on
|
|
9827
|
+
the account: get_set_asides() still returns it with ``isActive``
|
|
9828
|
+
False and ``expiredOn`` set, and it drops out of
|
|
9829
|
+
``get_set_asides(active_only=True)``. Live-verified 2026-09.
|
|
9830
|
+
|
|
9831
|
+
Expiring an already-expired set-aside succeeds again and overwrites
|
|
9832
|
+
its ``expiredOn`` with the new time, so pass only active ids if the
|
|
9833
|
+
original expiry time matters.
|
|
9834
|
+
|
|
9835
|
+
Args:
|
|
9836
|
+
set_aside_ids: List of set-aside ids (``id`` from get_set_asides)
|
|
9837
|
+
raise_on_error: If True (default), raise OrionAPIError when any
|
|
9838
|
+
result carries an errorMessage (e.g. "No matching set aside
|
|
9839
|
+
cash found for SetAsideId: ..."). Eclipse reports those with
|
|
9840
|
+
HTTP 200, so without this they pass silently.
|
|
9841
|
+
|
|
9842
|
+
Returns:
|
|
9843
|
+
list: One result per set-aside (setAsideId, accountId,
|
|
9844
|
+
systemExpiredOn, errorMessage, ...)
|
|
9845
|
+
"""
|
|
9846
|
+
_require_positive_int_list("set_aside_ids", set_aside_ids)
|
|
9847
|
+
|
|
9848
|
+
results = self.expire_account_set_asides(
|
|
9849
|
+
[{"setAsideId": i, "setAsideTransactions": []} for i in set_aside_ids]
|
|
9850
|
+
)
|
|
9851
|
+
if raise_on_error:
|
|
9852
|
+
failed = [r for r in results or [] if r.get("errorMessage")]
|
|
9853
|
+
if failed:
|
|
9854
|
+
details = "; ".join(f"{r.get('setAsideId')}: {r['errorMessage']}" for r in failed)
|
|
9855
|
+
raise OrionAPIError(f"Failed to expire set-aside(s): {details}")
|
|
9856
|
+
return results
|
|
9857
|
+
|
|
9373
9858
|
def set_account_tags(self, payload):
|
|
9374
9859
|
"""Set tags on accounts (mutating).
|
|
9375
9860
|
|
|
File without changes
|