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.
Files changed (32) hide show
  1. {invonetwork-2.2.0 → invonetwork-2.3.0}/CHANGELOG.md +18 -0
  2. {invonetwork-2.2.0 → invonetwork-2.3.0}/PKG-INFO +7 -9
  3. {invonetwork-2.2.0 → invonetwork-2.3.0}/README.md +6 -8
  4. {invonetwork-2.2.0 → invonetwork-2.3.0}/pyproject.toml +1 -1
  5. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/errors.py +14 -6
  6. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/server.py +6 -1
  7. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_claim.py +26 -0
  8. {invonetwork-2.2.0 → invonetwork-2.3.0}/.gitignore +0 -0
  9. {invonetwork-2.2.0 → invonetwork-2.3.0}/LICENSE +0 -0
  10. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/__init__.py +0 -0
  11. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/hooks.py +0 -0
  12. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/http.py +0 -0
  13. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/py.typed +0 -0
  14. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/types.py +0 -0
  15. {invonetwork-2.2.0 → invonetwork-2.3.0}/src/invonetwork/webhooks.py +0 -0
  16. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/helpers.py +0 -0
  17. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_audit_fixes.py +0 -0
  18. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_balance_identities.py +0 -0
  19. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_errors.py +0 -0
  20. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_guardian_approval.py +0 -0
  21. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_hooks.py +0 -0
  22. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_inbound_pending.py +0 -0
  23. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_item_purchase.py +0 -0
  24. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_phone_share.py +0 -0
  25. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_reads.py +0 -0
  26. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_recovery.py +0 -0
  27. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_retry.py +0 -0
  28. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_server.py +0 -0
  29. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_server_destinations.py +0 -0
  30. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_sms_verify.py +0 -0
  31. {invonetwork-2.2.0 → invonetwork-2.3.0}/tests/test_transaction_status.py +0 -0
  32. {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.2.0
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.1.1` — stable, published on PyPI (`pip install invonetwork`). The backend it
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
- > **Since 2.0.0 (breaking):** `mint_player_token` requires `player_phone` (E.164) see the
56
- > [CHANGELOG](CHANGELOG.md) migration note. **2.1.0 adds** the passkey-recovery relay
57
- > (`recovery_begin`/`recovery_complete`); **2.1.1** security hardening; **2.1.2** `expires_in_seconds`
58
- > on `create_checkout`. **2.2.0** reconciles with the backend: `mint_player_token` no longer
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` | claim needs phone-share approval before it can complete (409) run the phone-share OTP handshake |
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.1.1` — stable, published on PyPI (`pip install invonetwork`). The backend it
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
- > **Since 2.0.0 (breaking):** `mint_player_token` requires `player_phone` (E.164) see the
11
- > [CHANGELOG](CHANGELOG.md) migration note. **2.1.0 adds** the passkey-recovery relay
12
- > (`recovery_begin`/`recovery_complete`); **2.1.1** security hardening; **2.1.2** `expires_in_seconds`
13
- > on `create_checkout`. **2.2.0** reconciles with the backend: `mint_player_token` no longer
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` | claim needs phone-share approval before it can complete (409) run the phone-share OTP handshake |
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.2.0"
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 and by claims when the phone already belongs to
124
- another INVO account (same person, new email -- expected, not a failure). The phone
125
- holder gets a consent SMS; show "pending approval", then re-issue the identical call
126
- once approved (poll ``phone_share_status``, or use ``phone_share_initiate`` /
127
- ``phone_share_approve`` for the OTP fallback). ``phone_share_approval_id`` carries
128
- the id from the 409 body.
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.2.0 (+https://invo.network)"
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