Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ Reserved headers (`Authorization`, `User-Agent`, `Accept`, `Content-Type`, `Cont
| `client.payment_method_sessions()` | Payment-method collection sessions | US only |
| `client.mandates()` | Recurring mandates | IN only |
| `client.collect()` | Virtual accounts (Collect) | IN only |
| `client.payouts()` | Payouts | All |
| `client.split_settlement()` | Transfers, transfer reversals, connected accounts | IN only |

## Examples

Expand Down
47 changes: 34 additions & 13 deletions zohopayments/_internal/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ def parse_object(text: str) -> Dict[str, Any]:
def get_object(
body: Optional[Dict[str, Any]], *keys: str
) -> Optional[Dict[str, Any]]:
"""Return the first matching JSON object value from ``body`` by key, or ``None``."""
"""Return the first matching JSON object value from ``body`` by key, or ``None``.

``keys`` = candidate keys: returns on the first key that matches; does not traverse.
"""
if not body:
return None
for key in keys:
Expand All @@ -55,19 +58,37 @@ def get_object(
def get_object_required(
body: Optional[Dict[str, Any]], *keys: str
) -> Dict[str, Any]:
obj = get_object(body, *keys)
if obj is None:
joined = ", ".join(keys)
"""Traverse ``body`` through the nested key path formed by ``keys``.

``keys`` = path segments: every key is followed in order (nested path), not a
fallback list. Raises if any segment is absent or not a JSON object.
"""
current = body
for key in keys:
if current is None:
raise ZohoPaymentsException(
f"Response body is null; expected object at key path {list(keys)}"
)
value = current.get(key)
if not isinstance(value, dict):
raise ZohoPaymentsException(
f"Response body missing expected resource key '{key}' in path {list(keys)}"
)
current = value
if current is None:
raise ZohoPaymentsException(
f"Expected JSON object under one of keys: {joined}"
f"Response body is null; expected object at key path {list(keys)}"
)
return obj
return current


def list_from_body(
body: Optional[Dict[str, Any]], *keys: str
) -> List[Any]:
"""Return the first JSON array value found under any of the candidate keys."""
"""Return the first JSON array value found under any of the candidate keys.

``keys`` = candidate keys: returns on the first key that matches; does not traverse.
"""
if not body:
return []
for key in keys:
Expand All @@ -77,14 +98,14 @@ def list_from_body(
return []


def unwrap(response_body: Dict[str, Any], type_: Type[T], *candidate_keys: str) -> T:
"""Extract the single-resource envelope and deserialize into ``type_``.
def unwrap(response_body: Dict[str, Any], type_: Type[T], *path: str) -> T:
"""Extract a resource from ``response_body`` and deserialize into ``type_``.

The response is expected to look like ``{"payment": {...}, ...}``. We take
the first key in ``candidate_keys`` that points to a JSON object and feed
that to ``type_.from_dict``.
``path`` is a nested key path (e.g. ``"data", "transfers"`` means
``response_body["data"]["transfers"]``), traversed in order, then fed to
``type_.from_dict``.
"""
inner = get_object_required(response_body, *candidate_keys)
inner = get_object_required(response_body, *path)
from_dict = getattr(type_, "from_dict", None)
if from_dict is None:
raise ZohoPaymentsException(
Expand Down
14 changes: 14 additions & 0 deletions zohopayments/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
)
from zohopayments.services.payment_service import PaymentService
from zohopayments.services.payment_session_service import PaymentSessionService
from zohopayments.services.payout_service import PayoutService
from zohopayments.services.refund_service import RefundService
from zohopayments.services.split_settlement_service import SplitSettlementService


class ZohoPaymentsClient:
Expand All @@ -47,6 +49,8 @@ def __init__(
self._payment_method_sessions = PaymentMethodSessionService(http_client)
self._mandates = MandateService(http_client)
self._collect = CollectService(http_client)
self._payouts = PayoutService(http_client)
self._split_settlement = SplitSettlementService(http_client)

self._closed = False
self._close_lock = threading.Lock()
Expand Down Expand Up @@ -99,6 +103,16 @@ def collect(self) -> CollectService:
)
return self._collect

def payouts(self) -> PayoutService:
return self._payouts

def split_settlement(self) -> SplitSettlementService:
"""Requires :attr:`Edition.IN`."""
if not self._edition.is_in():
raise NotImplementedError(
"split_settlement() is available only on Edition.IN / Edition.IN_SANDBOX"
)
return self._split_settlement

def update_token(self, new_access_token: str) -> None:
"""Replace the stored OAuth access token. Thread-safe."""
Expand Down
60 changes: 60 additions & 0 deletions zohopayments/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@
MandatePaymentMethod,
MandateUpi,
)
from zohopayments.models.payout import (
Payout,
PayoutAccountDetails,
PayoutBankAccountDetails,
PayoutComment,
PayoutDetail,
PayoutTransaction,
PayoutTransactionSummary,
PayoutTypeSummary,
)
from zohopayments.models.split_settlement import (
ConnectedAccount,
ConnectedAccountBankAccount,
ConnectedAccountPayout,
ConnectedAccountPayoutAccountDetails,
ConnectedAccountPayoutComment,
ConnectedAccountPayoutSummary,
ConnectedAccountPayoutSummaryBankAccountDetails,
ConnectedAccountPayoutTransaction,
ConnectedAccountPayoutTransactionBreakdown,
ConnectedAccountPayoutTransactionSummary,
ConnectedAccountSummary,
ConnectedAccountTransaction,
Transfer,
TransferCreateResponse,
TransferPaymentDetails,
TransferReversal,
TransferReversalCreateResponse,
TransferReversalEntry,
TransferSplitResult,
TransferSummary,
)

__all__ = [
"ListResponse",
Expand Down Expand Up @@ -63,4 +95,32 @@
"MandatePayment",
"MandatePaymentMethod",
"MandateUpi",
"Payout",
"PayoutAccountDetails",
"PayoutBankAccountDetails",
"PayoutComment",
"PayoutDetail",
"PayoutTransaction",
"PayoutTransactionSummary",
"PayoutTypeSummary",
"ConnectedAccount",
"ConnectedAccountBankAccount",
"ConnectedAccountPayout",
"ConnectedAccountPayoutAccountDetails",
"ConnectedAccountPayoutComment",
"ConnectedAccountPayoutSummary",
"ConnectedAccountPayoutSummaryBankAccountDetails",
"ConnectedAccountPayoutTransaction",
"ConnectedAccountPayoutTransactionBreakdown",
"ConnectedAccountPayoutTransactionSummary",
"ConnectedAccountSummary",
"ConnectedAccountTransaction",
"Transfer",
"TransferCreateResponse",
"TransferPaymentDetails",
"TransferReversal",
"TransferReversalCreateResponse",
"TransferReversalEntry",
"TransferSplitResult",
"TransferSummary",
]
Loading