Metadata-Version: 2.4
Name: bkash-pgw-tokenized
Version: 0.1.0
Summary: Async client for bKash Tokenized Checkout (grant/refresh, create/execute, status, search, refund) and SNS IPN verification.
Project-URL: Documentation, https://developer.bka.sh/
Author: bKash boilerplate contributors
License-Expression: MIT
Keywords: bangladesh,bkash,payment,pgw,tokenized
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42.0.0
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: ipn
Description-Content-Type: text/markdown

# bkash-pgw-tokenized

Async Python client for **bKash Tokenized Checkout**: grant/refresh token, create/execute payment, payment status, search transaction, refund, plus helpers for **SNS-signed IPN** payloads.

**Import package:** `bkash_pgw_tokenized`  
**PyPI / pip name:** `bkash-pgw-tokenized`

Install:

```bash
pip install -e "./packages/bkash_pgw_tokenized[dev]"   # from repo root (+ pytest)
# default install includes httpx + cryptography (IPN / SNS signature verification)
pip install -e "./packages/bkash_pgw_tokenized"
```

Published wheels from PyPI: `pip install bkash-pgw-tokenized` includes **cryptography** so SNS IPN verification works without extras. The legacy extra `bkash-pgw-tokenized[ipn]` is a no-op (kept for old requirement lines).

## Usage

```python
from bkash_pgw_tokenized import Bkash, MemoryTokenStore, ensure_id_token

config = {
    "app_key": "...",
    "app_secret": "...",
    "username": "...",
    "password": "...",
    "sandbox": True,  # False for live
}

client = Bkash(config)
store = MemoryTokenStore()

id_token = await ensure_id_token(store, client)

create = await client.create_payment(
    id_token,
    mode="0011",
    payer_reference="INV-001",
    callback_url="https://your.site/payments/bkash/callback",
    amount="100.00",
    currency="BDT",
    intent="sale",
    merchant_invoice_number="INV-001",
)
# create["bkashURL"], create["paymentID"], ...

exec_res = await client.execute_payment(id_token, create["paymentID"])
status = await client.payment_status(id_token, create["paymentID"])
found = await client.search_transaction(id_token, trx_id="...")
refund = await client.refund(
    id_token,
    payment_id="...",
    trx_id="...",
    amount="100.00",
    sku="sku-1",
    reason="Customer request",
)
```

Optional keys: `"sandbox"` (defaults to `True`), `"base_url"` (overrides the default host for that mode).

On HTTP error responses, the client raises `BkashHttpError` with `status_code` and `response_body`.

Default API roots are `BKASH_TOKENIZED_SANDBOX_BASE_URL` and `BKASH_TOKENIZED_LIVE_BASE_URL` (exported from `bkash_pgw_tokenized`). For static typing of `config`, use `BkashConfig` (or `BkashConfigRequired` for only the four secret fields).

**Authorization header:** the raw `id_token` is sent as `Authorization` (no `Bearer` prefix), matching bKash’s tokenized API.

`AsyncBkashClient` is an alias for `Bkash` (backward-compatible name).

## Status codes and outcomes (success, failure, cancel)

bKash surfaces outcomes in three places: the **browser callback** to your `callbackURL`, **JSON from Execute / Payment status** APIs, and **IPN** (SNS) payloads. Handle each as below.

### 1. Browser callback query parameters

After checkout, bKash redirects the customer to your `callbackURL` with query parameters (names are typically lowercase). You should read at least **`paymentID`** and **`status`**.

Treat **`status`** case-insensitively (normalize with `.strip().lower()`).

| User outcome | Typical `status` values | What to do |
|--------------|-------------------------|------------|
| **Success** | `success` | Call `await client.execute_payment(id_token, paymentID)` (or confirm with `payment_status` if Execute is unclear). Treat paid only when the JSON outcome rules below say success. |
| **Failure** | `failure`, `fail`, `failed` | Do **not** Execute. Payment did not complete; show an error and keep your order unpaid. |
| **Cancel** | `cancel`, `cancelled`, `canceled` | Do **not** Execute. User backed out; show cancelled state. |

Any other `status` value should be treated as **unknown / failure** until you confirm with bKash documentation for your integration.

If `paymentID` or `status` is missing, you cannot complete the flow safely.

### 2. Execute and Payment status API JSON

After **`status=success`** on the callback, the source of truth for money movement is the **Execute** response (and optionally **Payment status** if Execute times out or returns an unusable body).

For **Execute** and **Payment status**, treat payment as **completed** only when **all** of the following hold (same rules many merchants use in production):

| Field | Success value |
|-------|----------------|
| `statusCode` | `"0000"` |
| `statusMessage` | `"Successful"` |
| `transactionStatus` | `"Completed"` |

If `statusCode` is present and not `"0000"`, or `transactionStatus` is present and not `"Completed"`, treat as **failure**. Use `errorCode` / `statusCode` with `describe_code` from `bkash_pgw_tokenized` for human-readable messages:

```python
from bkash_pgw_tokenized import describe_code, is_success_status_code

if is_success_status_code(response.get("statusCode")):
    ...
else:
    reason = describe_code(response.get("statusCode") or response.get("errorCode"))
```

Non-success responses often include `statusMessage`, `errorMessage`, or `message`; fall back to those for display.

### 3. Create payment response

**Create** is successful at the API level when `statusCode == "0000"`. You still need `bkashURL` and `paymentID` in the payload to send the user to bKash. Any other `statusCode` means create failed; use `describe_code` and the message fields above.

### 4. IPN (SNS inner `Message` JSON)

For server-side notifications, use `ipn_inner_is_success(inner)` (see [IPN (SNS)](#ipn-sns)). In short:

- If `errorCode` / `error_code` is set and non-empty → **not** success.
- If `statusCode` / `status_code` is present → it must be `"0000"`; if `transactionStatus` / `transaction_status` is also present, it must be `"Completed"`.
- If `statusCode` is **absent** (some samples) → `transactionStatus` must be `"Completed"`.

### Reference

- Full numeric codes: [bKash error codes](https://developer.bka.sh/docs/error-codes)  
- Success API code for many operations: **`0000`**

## IPN (SNS)

Payload parsing and validation (imported from `bkash_pgw_tokenized`):

```python
from bkash_pgw_tokenized import (
    amounts_match,
    extract_inner_from_sns_envelope,
    ipn_inner_is_success,
    verify_topic_arn,
)

if not verify_topic_arn(envelope, expected_topic_arn):
    ...
inner = extract_inner_from_sns_envelope(envelope)
ok, reason = ipn_inner_is_success(inner)
```

SNS **signature verification** (depends on `cryptography`, installed by default):

```python
from bkash_pgw_tokenized.sns_verify import SnsVerificationError, verify_sns_signature

try:
    verify_sns_signature(envelope)
except SnsVerificationError:
    ...
```

## Development

```bash
cd packages/bkash_pgw_tokenized
pip install -e ".[dev]"
pytest
```

**Poetry** (2.x reads PEP 621): `cd packages/bkash_pgw_tokenized && poetry install --extras dev`.

## License

MIT
