invonetwork 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {invonetwork-2.2.0 → invonetwork-2.3.0}/CHANGELOG.md +18 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/PKG-INFO +7 -9
- {invonetwork-2.2.0 → invonetwork-2.3.0}/README.md +6 -8
- {invonetwork-2.2.0 → invonetwork-2.3.0}/pyproject.toml +1 -1
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/errors.py +14 -6
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/server.py +6 -1
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_claim.py +26 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/.gitignore +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/LICENSE +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/__init__.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/hooks.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/http.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/py.typed +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/types.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/webhooks.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/helpers.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_audit_fixes.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_balance_identities.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_errors.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_guardian_approval.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_hooks.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_inbound_pending.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_item_purchase.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_phone_share.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_reads.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_recovery.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_retry.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_server.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_server_destinations.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_sms_verify.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_transaction_status.py +0 -0
- {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_webhooks.py +0 -0
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [2.3.0] - 2026-07-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
**Claim-time phone-share approval.** The backend closed a security gap: claiming a phone-routed
|
|
11
|
+
transfer now authorizes the collecting identity against the receiver phone before crediting, so
|
|
12
|
+
`claim_transfer` / `claim_currency` can now raise the phone-share `409
|
|
13
|
+
PHONE_SHARE_APPROVAL_REQUIRED` (previously only registration/mint did). `is_phone_share_approval_required`
|
|
14
|
+
already classifies it — no consumer code must change — but:
|
|
15
|
+
|
|
16
|
+
- New **`InvoError.phone_share_last4`** surfaces the `phone_last4` from the 409 body for the UI.
|
|
17
|
+
- `is_phone_share_approval_required` docs + the claim method docstrings + README now spell out
|
|
18
|
+
the claim-time path: **not a failure** — the money is held and the phone owner is texted; show
|
|
19
|
+
`err.message`, then re-issue the same claim once approved (succeeds idempotently); on
|
|
20
|
+
denial/expiry the sender is refunded.
|
|
21
|
+
|
|
22
|
+
Additive, no breaking changes; affects all rails' claim paths.
|
|
23
|
+
|
|
6
24
|
## [2.2.0] - 2026-07-07
|
|
7
25
|
|
|
8
26
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invonetwork
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: INVO Python server SDK -- currency purchase, item purchase, sends/transfers, and webhook verification for partner platforms.
|
|
5
5
|
Project-URL: Homepage, https://docs.invo.network
|
|
6
6
|
Project-URL: Documentation, https://docs.invo.network
|
|
@@ -50,14 +50,12 @@ server-side counterpart to the [INVO JS/Web SDK](https://www.npmjs.com/package/@
|
|
|
50
50
|
same endpoints, same field mappings, and the same webhook HMAC scheme, so both hit the
|
|
51
51
|
same live backend interchangeably.
|
|
52
52
|
|
|
53
|
-
> **Status:** `2.
|
|
53
|
+
> **Status:** `2.3.0` — stable, published on PyPI (`pip install invonetwork`). The backend it
|
|
54
54
|
> wraps is **live** on sandbox + production, so you can build and test against sandbox today.
|
|
55
|
-
> **
|
|
56
|
-
>
|
|
57
|
-
>
|
|
58
|
-
>
|
|
59
|
-
> requires `player_phone` (it's a session mint, not registration), send-by-phone-alone, and
|
|
60
|
-
> `get_player_balance` is email-only.
|
|
55
|
+
> Recent highlights: **2.2.0** reconciled with the backend (`mint_player_token` is a session
|
|
56
|
+
> mint — `player_phone` optional; send-by-phone-alone; email-only `get_player_balance`);
|
|
57
|
+
> **2.3.0** surfaces the claim-time phone-share `409` on `claim_transfer`/`claim_currency`
|
|
58
|
+
> (+ `err.phone_share_last4`). Full history in the [CHANGELOG](CHANGELOG.md).
|
|
61
59
|
> Canonical partner reference: **https://docs.invo.network**.
|
|
62
60
|
|
|
63
61
|
## Highlights
|
|
@@ -592,7 +590,7 @@ Classifiers:
|
|
|
592
590
|
| `.is_receiver_not_enrolled` | recipient has no passkey → switch to claim-code entry |
|
|
593
591
|
| `.is_insufficient_balance` | item purchase failed (400); `required_amount` + `current_balance` on `.body` |
|
|
594
592
|
| `.is_duplicate_request` | idempotency-keyed request was a duplicate (409) |
|
|
595
|
-
| `.is_phone_share_approval_required` |
|
|
593
|
+
| `.is_phone_share_approval_required` | phone needs owner approval — at register/mint **or** at `claim_transfer`/`claim_currency` (contested receiver phone). Not a failure: money held, phone owner texted. Show `.message` (+ `.phone_share_last4`), re-issue the same claim after approval; sender refunded on denial/expiry |
|
|
596
594
|
| `.is_phone_share_already_approved` | the phone-share (phone, requesting_email) pair was already approved |
|
|
597
595
|
| `.retry_after` | seconds to back off on a 429 throttle |
|
|
598
596
|
| `.is_enrollment_authorization_required` | first-enrollment needs the OTP grant |
|
|
@@ -5,14 +5,12 @@ server-side counterpart to the [INVO JS/Web SDK](https://www.npmjs.com/package/@
|
|
|
5
5
|
same endpoints, same field mappings, and the same webhook HMAC scheme, so both hit the
|
|
6
6
|
same live backend interchangeably.
|
|
7
7
|
|
|
8
|
-
> **Status:** `2.
|
|
8
|
+
> **Status:** `2.3.0` — stable, published on PyPI (`pip install invonetwork`). The backend it
|
|
9
9
|
> wraps is **live** on sandbox + production, so you can build and test against sandbox today.
|
|
10
|
-
> **
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
> requires `player_phone` (it's a session mint, not registration), send-by-phone-alone, and
|
|
15
|
-
> `get_player_balance` is email-only.
|
|
10
|
+
> Recent highlights: **2.2.0** reconciled with the backend (`mint_player_token` is a session
|
|
11
|
+
> mint — `player_phone` optional; send-by-phone-alone; email-only `get_player_balance`);
|
|
12
|
+
> **2.3.0** surfaces the claim-time phone-share `409` on `claim_transfer`/`claim_currency`
|
|
13
|
+
> (+ `err.phone_share_last4`). Full history in the [CHANGELOG](CHANGELOG.md).
|
|
16
14
|
> Canonical partner reference: **https://docs.invo.network**.
|
|
17
15
|
|
|
18
16
|
## Highlights
|
|
@@ -547,7 +545,7 @@ Classifiers:
|
|
|
547
545
|
| `.is_receiver_not_enrolled` | recipient has no passkey → switch to claim-code entry |
|
|
548
546
|
| `.is_insufficient_balance` | item purchase failed (400); `required_amount` + `current_balance` on `.body` |
|
|
549
547
|
| `.is_duplicate_request` | idempotency-keyed request was a duplicate (409) |
|
|
550
|
-
| `.is_phone_share_approval_required` |
|
|
548
|
+
| `.is_phone_share_approval_required` | phone needs owner approval — at register/mint **or** at `claim_transfer`/`claim_currency` (contested receiver phone). Not a failure: money held, phone owner texted. Show `.message` (+ `.phone_share_last4`), re-issue the same claim after approval; sender refunded on denial/expiry |
|
|
551
549
|
| `.is_phone_share_already_approved` | the phone-share (phone, requesting_email) pair was already approved |
|
|
552
550
|
| `.retry_after` | seconds to back off on a 429 throttle |
|
|
553
551
|
| `.is_enrollment_authorization_required` | first-enrollment needs the OTP grant |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "invonetwork"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "INVO Python server SDK -- currency purchase, item purchase, sends/transfers, and webhook verification for partner platforms."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -120,12 +120,14 @@ class InvoError(Exception):
|
|
|
120
120
|
def is_phone_share_approval_required(self) -> bool:
|
|
121
121
|
"""True if the call needs phone-share approval before it can complete (409).
|
|
122
122
|
|
|
123
|
-
Raised by registration/token-mint
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
the id
|
|
123
|
+
Raised by registration/token-mint AND by ``claim_transfer`` / ``claim_currency`` when
|
|
124
|
+
the receiver phone is contested (the collecting identity isn't yet authorized for it).
|
|
125
|
+
NOT a failure -- the money is held and the phone owner is texted to approve. Show
|
|
126
|
+
``err.message`` verbatim (``phone_share_last4`` names the phone), then RE-ISSUE the same
|
|
127
|
+
claim once approved (it then succeeds idempotently); on denial/expiry the sender is
|
|
128
|
+
refunded. (``phone_share_approval_id`` carries the id when the 409 came from the
|
|
129
|
+
OTP-handshake path; ``phone_share_initiate`` / ``phone_share_approve`` /
|
|
130
|
+
``phone_share_status`` drive that fallback.)
|
|
129
131
|
"""
|
|
130
132
|
return self.code == "PHONE_SHARE_APPROVAL_REQUIRED"
|
|
131
133
|
|
|
@@ -135,6 +137,12 @@ class InvoError(Exception):
|
|
|
135
137
|
v = self._body_object().get("approval_id")
|
|
136
138
|
return v if isinstance(v, str) and v else None
|
|
137
139
|
|
|
140
|
+
@property
|
|
141
|
+
def phone_share_last4(self) -> Optional[str]:
|
|
142
|
+
"""Last 4 digits of the phone that must approve (``phone_last4``), for the UI message."""
|
|
143
|
+
v = self._body_object().get("phone_last4")
|
|
144
|
+
return v if isinstance(v, str) and v else None
|
|
145
|
+
|
|
138
146
|
@property
|
|
139
147
|
def is_phone_share_already_approved(self) -> bool:
|
|
140
148
|
"""True if the phone-share (phone, requesting_email) pair was already approved."""
|
|
@@ -50,7 +50,7 @@ from .types import (
|
|
|
50
50
|
|
|
51
51
|
__all__ = ["InvoServer"]
|
|
52
52
|
|
|
53
|
-
DEFAULT_USER_AGENT = "invonetwork-python-sdk/2.
|
|
53
|
+
DEFAULT_USER_AGENT = "invonetwork-python-sdk/2.3.0 (+https://invo.network)"
|
|
54
54
|
"""Explicit, non-blocked UA. The backend 403-blocks UAs matching nikto|sqlmap|
|
|
55
55
|
python-urllib|scanner|bot|^$, so we never use urllib's default UA."""
|
|
56
56
|
|
|
@@ -1019,6 +1019,11 @@ class InvoServer:
|
|
|
1019
1019
|
``target_player_id``. Errors surface via :class:`InvoError` (404 unknown code, 403
|
|
1020
1020
|
phone mismatch, 400 not-claimable/expired/wrong-value, 409
|
|
1021
1021
|
``err.is_phone_share_approval_required``, ...).
|
|
1022
|
+
|
|
1023
|
+
The 409 ``is_phone_share_approval_required`` is NOT a failure: the receiver phone is
|
|
1024
|
+
contested, so the money is held and the phone owner is texted to approve. Show
|
|
1025
|
+
``err.message`` (``err.phone_share_last4`` names the phone), then RE-ISSUE this same
|
|
1026
|
+
claim once approved (it succeeds idempotently); on denial/expiry the sender is refunded.
|
|
1022
1027
|
"""
|
|
1023
1028
|
body: Dict[str, Any] = {
|
|
1024
1029
|
"claim_code": claim_code,
|
|
@@ -13,6 +13,32 @@ def _server(responder):
|
|
|
13
13
|
return make_server(transport, max_retries=0), calls
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def test_claim_surfaces_claim_time_phone_share_409():
|
|
17
|
+
# contested receiver phone -> money held, phone owner texted; not a failure
|
|
18
|
+
server, _ = _server(
|
|
19
|
+
lambda r, i: fake_response(
|
|
20
|
+
409,
|
|
21
|
+
{
|
|
22
|
+
"status": "approval_required",
|
|
23
|
+
"code": "PHONE_SHARE_APPROVAL_REQUIRED",
|
|
24
|
+
"message": "One more step to collect. We've texted the phone on file to confirm.",
|
|
25
|
+
"phone_last4": "6789",
|
|
26
|
+
"retry_after_approval": True,
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
with pytest.raises(InvoError) as ei:
|
|
31
|
+
server.claim_currency(
|
|
32
|
+
claim_code="CLAIM-9",
|
|
33
|
+
receiver_player_name="R",
|
|
34
|
+
receiver_player_email="r@x.com",
|
|
35
|
+
receiver_player_phone="+15555550111",
|
|
36
|
+
)
|
|
37
|
+
assert ei.value.is_phone_share_approval_required
|
|
38
|
+
assert "One more step" in ei.value.message
|
|
39
|
+
assert ei.value.phone_share_last4 == "6789"
|
|
40
|
+
|
|
41
|
+
|
|
16
42
|
def test_claim_transfer_maps_body_and_success_result():
|
|
17
43
|
server, calls = _server(
|
|
18
44
|
lambda r, i: fake_response(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|