Metadata-Version: 2.4
Name: waffo
Version: 0.2.0b1
Summary: Official Waffo Payment Platform Python SDK
Project-URL: Homepage, https://github.com/waffo-com/waffo-sdk/tree/main/packages/waffo-python
Project-URL: Repository, https://github.com/waffo-com/waffo-sdk
Project-URL: Issues, https://github.com/waffo-com/waffo-sdk/issues
Author-email: Waffo <merchant.support@waffo.com>
License-Expression: MIT
License-File: LICENSE
Keywords: payment,payment-gateway,psp,sdk,subscription,waffo
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: cryptography<47,>=42
Requires-Dist: eval-type-backport<1,>=0.2; python_version < '3.10'
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Requires-Dist: pyyaml<7,>=6
Provides-Extra: dev
Requires-Dist: mypy<2.0,>=1.8; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: e2e
Requires-Dist: playwright>=1.40; extra == 'e2e'
Requires-Dist: pyngrok>=7; extra == 'e2e'
Requires-Dist: pytest-playwright>=0.5; extra == 'e2e'
Description-Content-Type: text/markdown

# Waffo Python SDK

<!-- Synced with waffo-sdk/README.md @ commit c23c86f -->

**English** | [中文](README_CN.md)

[![PyPI version](https://img.shields.io/pypi/v/waffo.svg)](https://pypi.org/project/waffo/)
[![Python Version](https://img.shields.io/pypi/pyversions/waffo.svg)](https://pypi.org/project/waffo/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Official Python SDK for [Waffo Payment Platform](https://www.waffo.com), providing typed access to payments, refunds, subscriptions, subscription changes, merchant configuration, payment method configuration, and webhook signature verification.

## Latest Release Highlights

- **Subscription update API**: Added `subscription.update()` with `amount`, `trialPeriodAmount`, and `scheduledAmounts` support.
- **Scheduled amount models**: Added generated scheduled amount fields across subscription create, change, update, and inquiry models.
- **Contract hardening**: Added order capture timestamp auto-injection and expanded schema field coverage.

## Introduction

### Core Features

- **Global Payments**: Create, query, cancel, and refund payment orders.
- **Subscription Management**: Create, query, cancel, manage, and change subscriptions.
- **Webhook Handling**: Verify Waffo signatures and route notification callbacks.
- **Typed Models**: Pydantic v2 request/response models generated from `openapi.json`.
- **Secure Defaults**: RSA request signing, response signature verification, explicit environment selection, and configurable timeouts.

## Table of Contents

- [Latest Release Highlights](#latest-release-highlights)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [API Usage](#api-usage)
- [Webhook Handling](#webhook-handling)
- [Payment Method Types](#payment-method-types)
- [Advanced Configuration](#advanced-configuration)
- [Handling New API Fields (ExtraParams)](#handling-new-api-fields-extraparams)
- [Error Handling](#error-handling)
- [Development](#development)
- [Support](#support)
- [License](#license)

## Requirements

- Python 3.9+
- pip

### Version Compatibility

| Python Version | Support Status |
|----------------|----------------|
| 3.13 | Fully Supported |
| 3.12 | Fully Supported |
| 3.11 | Fully Supported (Recommended) |
| 3.10 | Fully Supported |
| 3.9 | Fully Supported |
| < 3.9 | Not Supported |

## Installation

```bash
python -m pip install "waffo==0.1.0b0"
# or install the latest beta/pre-release
python -m pip install --pre waffo
```

## Quick Start

### 1. Initialize the SDK

```python
from waffo import Environment, Waffo, WaffoConfig

config = WaffoConfig(
    api_key="your-api-key",
    private_key="your-base64-encoded-private-key",
    waffo_public_key="waffo-public-key",
    merchant_id="your-merchant-id",
    environment=Environment.SANDBOX,
)

waffo = Waffo(config)
```

### 2. Create a Payment Order

```python
from uuid import uuid4

payment_request_id = uuid4().hex

response = waffo.order().create(
    {
        "paymentRequestId": payment_request_id,
        "merchantOrderId": f"ORDER_{payment_request_id}",
        "orderCurrency": "HKD",
        "orderAmount": "100.00",
        "orderDescription": "Test Product",
        "notifyUrl": "https://your-site.com/webhook",
        "userInfo": {
            "userId": "user_123",
            "userEmail": "user@example.com",
            "userTerminal": "WEB",
        },
        "paymentInfo": {
            "productName": "ONE_TIME_PAYMENT",
        },
        "goodsInfo": {
            "goodsUrl": "https://your-site.com/product/001",
            "goodsName": "Test Product",
        },
    }
)

if response.is_success():
    data = response.get_data()
    print("Redirect action:", data.order_action if data else None)
    print("Acquiring Order ID:", data.acquiring_order_id if data else None)
else:
    print("Error:", response.get_message())
```

### 3. Query Order Status

```python
response = waffo.order().inquiry({"paymentRequestId": payment_request_id})

if response.is_success():
    data = response.get_data()
    print("Order Status:", data.order_status if data else None)
    print("Order Amount:", data.order_amount if data else None)
```

## Configuration

### Full Configuration Options

```python
from waffo import Environment, Waffo, WaffoConfig

waffo = Waffo(
    WaffoConfig(
        api_key="your-api-key",
        private_key="your-base64-private-key",
        waffo_public_key="waffo-public-key",
        environment=Environment.SANDBOX,
        merchant_id="your-merchant-id",
        connect_timeout=10000,
        read_timeout=30000,
    )
)
```

### Environment Variables

```bash
export WAFFO_API_KEY=your-api-key
export WAFFO_PRIVATE_KEY=your-private-key
export WAFFO_PUBLIC_KEY=waffo-public-key
export WAFFO_ENVIRONMENT=SANDBOX
export WAFFO_MERCHANT_ID=your-merchant-id
```

```python
from waffo import Waffo

waffo = Waffo.from_env()
```

### Environment URLs

| Environment | Base URL | Description |
|-------------|----------|-------------|
| `SANDBOX` | `https://api-sandbox.waffo.com` | Test environment |
| `PRODUCTION` | `https://api.waffo.com` | Production environment |

> **Important**: Environment must be explicitly specified. SDKs do not default to any environment to prevent accidental requests to wrong environments.

### Request-Level Configuration

```python
from waffo import RequestOptions

response = waffo.order().create(
    params,
    RequestOptions(connect_timeout=10000, read_timeout=30000),
)
```

## API Usage

### Order Management

| Method | Description |
|--------|-------------|
| `order().create()` | Create a payment order |
| `order().inquiry()` | Query order status |
| `order().cancel()` | Cancel an order |
| `order().refund()` | Create an order refund |
| `order().capture()` | Capture an authorized payment |

### Subscription Management

| Method | Description |
|--------|-------------|
| `subscription().create()` | Create a subscription |
| `subscription().inquiry()` | Query a subscription |
| `subscription().cancel()` | Cancel a subscription |
| `subscription().manage()` | Get a subscription management URL |
| `subscription().change()` | Upgrade or downgrade a subscription |
| `subscription().change_inquiry()` | Query subscription change status |

### Subscription Change (Upgrade/Downgrade)

Change an existing subscription to a new plan (upgrade or downgrade).

#### Change Subscription

```python
from uuid import uuid4

from waffo import WaffoUnknownStatusError

subscription_request = uuid4().hex
origin_subscription_request = "original-subscription-request-id"

try:
    response = waffo.subscription().change(
        {
            "subscriptionRequest": subscription_request,
            "originSubscriptionRequest": origin_subscription_request,
            "remainingAmount": "50.00",
            "currency": "HKD",
            "notifyUrl": "https://your-site.com/webhook/subscription",
            "productInfoList": [
                {
                    "description": "Annual Premium Subscription (12 months)",
                    "periodType": "MONTHLY",
                    "periodInterval": "12",
                    "amount": "999.00",
                }
            ],
            "userInfo": {
                "userId": "user_123",
                "userEmail": "user@example.com",
            },
            "goodsInfo": {
                "goodsId": "GOODS_PREMIUM",
                "goodsName": "Premium Plan",
            },
            "paymentInfo": {
                "productName": "SUBSCRIPTION",
            },
        }
    )

    if response.is_success():
        data = response.get_data()
        print("Change Status:", data.subscription_change_status if data else None)
        print("New Subscription ID:", data.subscription_id if data else None)
except WaffoUnknownStatusError as exc:
    print("Unknown status, query later or wait for webhook:", exc)
```

#### Subscription Change Status Values

| Status | Description |
|--------|-------------|
| `IN_PROGRESS` | Change is being processed |
| `AUTHORIZATION_REQUIRED` | User needs to authorize the change (redirect to webUrl) |
| `SUCCESS` | Change completed successfully |
| `CLOSED` | Change was closed (timeout or failed) |

#### Query Subscription Change Status

```python
response = waffo.subscription().change_inquiry(
    {
        "subscriptionRequest": "new-subscription-request-id",
        "originSubscriptionRequest": "original-subscription-request-id",
    }
)

if response.is_success():
    data = response.get_data()
    print("Change Status:", data.subscription_change_status if data else None)
    print("New Subscription ID:", data.subscription_id if data else None)
    print("Remaining Amount:", data.remaining_amount if data else None)
    print("Currency:", data.currency if data else None)
```

### Refund Query

```python
response = waffo.refund().inquiry({"refundRequestId": "refund_request_id"})
```

### Merchant Configuration

```python
response = waffo.merchant_config().inquiry({})
```

## Webhook Handling

```python
from fastapi import FastAPI, Header, Request, Response
from waffo import Waffo

app = FastAPI()
waffo = Waffo.from_env()

waffo.webhook().on_payment(
    lambda notification: print(notification.get("result", {}).get("acquiringOrderId"))
)

@app.post("/webhook")
async def webhook(request: Request, x_signature: str | None = Header(default=None)) -> Response:
    body = (await request.body()).decode("utf-8")
    result = waffo.webhook().handle_webhook(body, x_signature)
    return Response(
        content=result.response_body,
        media_type="application/json",
        headers={"X-SIGNATURE": result.response_signature},
        status_code=200 if result.success else 400,
    )
```

### Webhook Notification Types

| Event Type | Handler Method | Description |
|------------|----------------|-------------|
| `PAYMENT_NOTIFICATION` | `onPayment()` | Payment result notification (triggered on every payment attempt, including retries) |
| `REFUND_NOTIFICATION` | `onRefund()` | Refund result notification |
| `SUBSCRIPTION_STATUS_NOTIFICATION` | `onSubscriptionStatus()` | Subscription status change notification (triggered when subscription main record status changes) |
| `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | `onSubscriptionPeriodChanged()` | Subscription period change notification (final result of each period) |
| `SUBSCRIPTION_CHANGE_NOTIFICATION` | `onSubscriptionChange()` | Subscription change (upgrade/downgrade) result notification |

### Subscription Notification Types Explained

| Notification Type | Trigger Condition | Scope | Includes Retry Events | Typical Use Case |
|-------------------|-------------------|-------|----------------------|------------------|
| `SUBSCRIPTION_STATUS_NOTIFICATION` | Subscription main record status changes | Subscription level | No | Track subscription lifecycle: first payment success activation (ACTIVE), cancellation (MERCHANT_CANCELLED, CHANNEL_CANCELLED), first payment failure close (CLOSE), etc. |
| `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | Subscription period reaches final state | Period level | No (only final result) | Only need final result of each period, no intermediate retry events |
| `SUBSCRIPTION_CHANGE_NOTIFICATION` | Subscription change (upgrade/downgrade) completes | Change request level | No (only final result) | Track subscription change results: SUCCESS or CLOSED |
| `PAYMENT_NOTIFICATION` | Every payment order | Payment order level | Yes (includes all retries) | Need complete details of every payment attempt, including failure reasons, timestamps, retry details |

> **Selection Guide**:
> - If you only care about subscription activation/cancellation, use `SUBSCRIPTION_STATUS_NOTIFICATION`
> - If you only care about final renewal result of each period, use `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION`
> - If you only care about subscription change (upgrade/downgrade) final result, use `SUBSCRIPTION_CHANGE_NOTIFICATION`
> - If you need to track every payment attempt (including retries), use `PAYMENT_NOTIFICATION`

> **Subscription Payment Note**: Each period's payment (including first payment and renewals) triggers `PAYMENT_NOTIFICATION` events. You can get subscription-related info (subscriptionId, period, etc.) from `subscriptionInfo`.

> **Subscription Change (Upgrade/Downgrade) Webhook Note**:
> When a subscription change is processed, the following notifications are triggered:
> - `SUBSCRIPTION_CHANGE_NOTIFICATION`: When subscription change completes (SUCCESS or CLOSED)
> - `SUBSCRIPTION_STATUS_NOTIFICATION`: When original subscription status changes to `MERCHANT_CANCELLED`
> - `SUBSCRIPTION_STATUS_NOTIFICATION`: When new subscription status changes to `ACTIVE`
> - `PAYMENT_NOTIFICATION`: If upgrade requires additional payment (price difference)

### Webhook Notification Payload Examples

The following examples show the actual payload structure for each notification type.

#### PAYMENT_NOTIFICATION

```json
{
  "eventType": "PAYMENT_NOTIFICATION",
  "result": {
    "acquiringOrderId": "A2026xxxxxxxxxxxxxxxxxxxx",
    "orderStatus": "PAY_SUCCESS",
    "orderAmount": "109.00",
    "orderCurrency": "USD",
    "finalDealAmount": "109.00",
    "userCurrency": "USD",
    "orderDescription": "Sample Product",
    "orderRequestedAt": "2026-02-28T10:05:58.000Z",
    "orderCompletedAt": "2026-02-28T10:06:10.000Z",
    "orderUpdatedAt": "2026-02-28T10:06:10.000Z",
    "refundExpiryAt": "2026-08-26T23:59:59.999Z",
    "userInfo": {
      "userId": "user@example.com",
      "userEmail": "user@example.com"
    },
    "merchantInfo": {
      "merchantId": "YOUR_MERCHANT_ID"
    },
    "goodsInfo": {
      "goodsId": "goods_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "goodsName": "Sample Product"
    },
    "addressInfo": {},
    "paymentInfo": {
      "productName": "SUBSCRIPTION",
      "payMethodType": "CREDITCARD",
      "payMethodName": "CC_VISA",
      "payMethodProperties": "{\"cardToken\":\"CARD_TOKEN_XXXXXXXXXXXXXXXXXXXX\",\"cardTransactionType\":\"CIT\"}",
      "payMethodResponse": "{\"maskCardData\":\"XXXX42****XX4242\"}"
    },
    "subscriptionInfo": {
      "subscriptionId": "SC2026xxxxxxxxxxxxxxxxxxxxxxxxxx",
      "subscriptionRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "merchantRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "period": "1"
    },
    "cancelRedirectUrl": "https://YOUR_DOMAIN/checkout/YOUR_CHECKOUT_ID"
  }
}
```

#### SUBSCRIPTION_STATUS_NOTIFICATION / SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION

These two notification types share the same payload structure:

```json
{
  "eventType": "SUBSCRIPTION_STATUS_NOTIFICATION",
  "result": {
    "subscriptionId": "SC2026xxxxxxxxxxxxxxxxxxxxxxxxxx",
    "subscriptionRequest": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "merchantSubscriptionId": "sub_req_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "subscriptionStatus": "ACTIVE",
    "currency": "USD",
    "userCurrency": "USD",
    "amount": "109.00",
    "requestedAt": "2026-02-28T10:05:58.000Z",
    "updatedAt": "2026-02-28T10:06:10.000Z",
    "userInfo": {
      "userId": "user@example.com",
      "userEmail": "user@example.com"
    },
    "merchantInfo": {
      "merchantId": "YOUR_MERCHANT_ID"
    },
    "goodsInfo": {
      "goodsId": "goods_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "goodsName": "Sample Product"
    },
    "productInfo": {
      "periodType": "MONTHLY",
      "periodInterval": "1",
      "currentPeriod": "1",
      "startDateTime": "2026-02-28T10:06:10.000Z",
      "nextPaymentDateTime": "2026-03-28T10:06:10.000Z",
      "description": "Sample Product"
    },
    "paymentInfo": {
      "productName": "SUBSCRIPTION",
      "payMethodType": "CREDITCARD",
      "payMethodName": "CC_VISA",
      "payMethodProperties": "{}"
    },
    "paymentDetails": [
      {
        "period": "1",
        "acquiringOrderId": "A2026xxxxxxxxxxxxxxxxxxxx",
        "orderAmount": "109.00",
        "orderCurrency": "USD",
        "orderStatus": "PAY_SUCCESS",
        "orderUpdatedAt": "2026-02-28T10:06:10.000Z"
      }
    ]
  }
}
```

## Payment Method Types

### payMethodType Reference

| Type | Description | Example payMethodName |
|------|-------------|----------------------|
| `CREDITCARD` | Credit Card | CC_VISA, CC_MASTERCARD, CC_AMEX, CC_JCB, etc. |
| `DEBITCARD` | Debit Card | DC_VISA, DC_MASTERCARD, DC_ELO, etc. |
| `EWALLET` | E-Wallet | GCASH, DANA, PROMPTPAY, GRABPAY, etc. |
| `VA` | Virtual Account | BCA, BNI, BRI, MANDIRI, etc. |
| `APPLEPAY` | Apple Pay | APPLEPAY |
| `GOOGLEPAY` | Google Pay | GOOGLEPAY |

### Usage Examples

```typescript
// Specify type only, let user choose on checkout page
paymentInfo: {
  payMethodType: 'CREDITCARD',
}

// Specify exact payment method
paymentInfo: {
  payMethodType: 'CREDITCARD',
  payMethodName: 'CC_VISA',
}

// Combine multiple types
paymentInfo: {
  payMethodType: 'CREDITCARD,DEBITCARD',
}

// E-wallet with specific channel
paymentInfo: {
  payMethodType: 'EWALLET',
  payMethodName: 'GCASH',
}
```

> **Note**: For available `ProductName`, `PayMethodType`, `PayMethodName` values, merchants can log in to [Waffo Portal](https://dashboard.waffo.com) to view contracted payment methods (Home → Service → Pay-in).

## Advanced Configuration

### Timeout Configuration Recommendations

| Operation Type | Connect Timeout | Read Timeout | Notes |
|----------------|-----------------|--------------|-------|
| Create Order | 5s | 30s | Recommended |
| Create Subscription | 5s | 30s | Recommended |
| Refund Operation | 5s | 30s | Recommended |
| Query Operations | 5s | 15s | Can be shorter |

### Connection Pool Recommendations

| Scenario | Max Connections | Max Per Route | Notes |
|----------|-----------------|---------------|-------|
| Low Traffic (< 10 QPS) | 20 | 10 | Default config sufficient |
| Medium Traffic (10-100 QPS) | 50 | 20 | Consider using OkHttp |
| High Traffic (> 100 QPS) | 100-200 | 50 | Consider Apache HttpClient |

### Instance Reuse

Create one `Waffo` instance during application startup and reuse it across requests.

## Handling New API Fields (ExtraParams)

When Waffo API adds new fields that are not yet defined in the SDK, you can use the ExtraParams feature to access these fields without waiting for an SDK update.

### Reading Unknown Fields from Responses



### Sending Extra Fields in Requests



### Important Notes

> **Upgrade SDK Promptly**
>
> ExtraParams is designed as a **temporary solution** for accessing new API fields before SDK updates.
>
> **Best Practices:**
> 1. Check SDK release notes regularly for new field support
> 2. Once SDK officially supports the field, migrate from `getExtraParam("field")` to the official getter (e.g., `getField()`)
> 3. The SDK logs a warning when you use `getExtraParam()` on officially supported fields
>
> **Why migrate?**
> - Official getters provide type safety
> - Better IDE auto-completion and documentation
> - Reduced risk of typos in field names

## Error Handling

### Error Code Classification

Error codes are classified by first letter:

| Prefix | Category | Description |
|--------|----------|-------------|
| **S** | SDK Internal Error | SDK client internal error such as network timeout, signing failure, etc. |
| **A** | Merchant Related | Parameter, signature, permission, contract issues on merchant side |
| **B** | User Related | User status, balance, authorization issues |
| **C** | System Related | Waffo system or payment channel issues |
| **D** | Risk Related | Risk control rejection |
| **E** | Unknown Status | Server returned unknown status |

### Complete Error Code Table

#### SDK Internal Errors (Sxxxx)

| Code | Description | Exception Type | Handling Suggestion |
|------|-------------|----------------|---------------------|
| `S0001` | Network Error | `WaffoUnknownStatusError` | **Status unknown**, need to query order to confirm |
| `S0002` | Invalid Public Key | `WaffoError` | Check if public key is valid Base64 encoded X509 format |
| `S0003` | RSA Signing Failed | `WaffoError` | Check if private key format is correct |
| `S0004` | Response Signature Verification Failed | `ApiResponse.error()` | Check Waffo public key config, contact Waffo |
| `S0005` | Request Serialization Failed | `ApiResponse.error()` | Check request parameter format |
| `S0006` | SDK Unknown Error | `ApiResponse.error()` | Check logs, contact technical support |
| `S0007` | Invalid Private Key | `WaffoError` | Check if private key is valid Base64 PKCS8 DER or unencrypted PKCS8 PEM format |

> **Important**: `S0001` and `E0001` (returned by server) indicate **unknown status**. Do not close order directly! Should call query API or wait for webhook to confirm actual status.

#### Merchant Related Errors (Axxxxx)

| Code | Description | HTTP Status |
|------|-------------|-------------|
| `0` | Success | 200 |
| `A0001` | Invalid API Key | 401 |
| `A0002` | Invalid Signature | 401 |
| `A0003` | Parameter Validation Failed | 400 |
| `A0004` | Insufficient Permission | 401 |
| `A0005` | Merchant Limit Exceeded | 400 |
| `A0006` | Merchant Status Abnormal | 400 |
| `A0007` | Unsupported Transaction Currency | 400 |
| `A0008` | Transaction Amount Exceeded | 400 |
| `A0009` | Order Not Found | 400 |
| `A0010` | Merchant Contract Does Not Allow This Operation | 400 |
| `A0011` | Idempotent Parameter Mismatch | 400 |
| `A0012` | Merchant Account Insufficient Balance | 400 |
| `A0013` | Order Already Paid, Cannot Cancel | 400 |
| `A0014` | Refund Rules Do Not Allow Refund | 400 |
| `A0015` | Payment Channel Does Not Support Cancel | 400 |
| `A0016` | Payment Channel Rejected Cancel | 400 |
| `A0017` | Payment Channel Does Not Support Refund | 400 |
| `A0018` | Payment Method Does Not Match Merchant Contract | 400 |
| `A0019` | Cannot Refund Due to Chargeback Dispute | 400 |
| `A0020` | Payment Amount Exceeds Single Transaction Limit | 400 |
| `A0021` | Cumulative Payment Amount Exceeds Daily Limit | 400 |
| `A0022` | Multiple Products Exist, Need to Specify Product Name | 400 |
| `A0023` | Token Expired, Cannot Create Order | 400 |
| `A0024` | Exchange Rate Expired, Cannot Process Order | 400 |
| `A0026` | Unsupported Checkout Language | 400 |
| `A0027` | Refund Count Reached Limit (50 times) | 400 |
| `A0029` | Invalid Card Data Provided by Merchant | 400 |
| `A0030` | Card BIN Not Found | 400 |
| `A0031` | Unsupported Card Scheme or Card Type | 400 |
| `A0032` | Invalid Payment Token Data | 400 |
| `A0033` | Multiple Payment Methods with Same Name, Need to Specify Country | 400 |
| `A0034` | Order Expiry Time Provided by Merchant Has Passed | 400 |
| `A0035` | Current Order Does Not Support Capture Operation | 400 |
| `A0036` | Current Order Status Does Not Allow Capture Operation | 400 |
| `A0037` | User Payment Token Invalid or Expired | 400 |
| `A0038` | MIT Transaction Requires Verified User Payment Token | 400 |
| `A0039` | Order Already Refunded by Chargeback Prevention Service | 400 |
| `A0040` | Order Cannot Be Created Concurrently | 400 |
| `A0045` | MIT Transaction Cannot Process, tokenId Status Unverified | 400 |

#### User Related Errors (Bxxxxx)

| Code | Description | HTTP Status |
|------|-------------|-------------|
| `B0001` | User Status Abnormal | 400 |
| `B0002` | User Limit Exceeded | 400 |
| `B0003` | User Insufficient Balance | 400 |
| `B0004` | User Did Not Pay Within Timeout | 400 |
| `B0005` | User Authorization Failed | 400 |
| `B0006` | Invalid Phone Number | 400 |
| `B0007` | Invalid Email Format | 400 |

#### System Related Errors (Cxxxxx)

| Code | Description | HTTP Status |
|------|-------------|-------------|
| `C0001` | System Error | 500 |
| `C0002` | Merchant Contract Invalid | 500 |
| `C0003` | Order Status Invalid, Cannot Continue Processing | 500 |
| `C0004` | Order Information Mismatch | 500 |
| `C0005` | Payment Channel Rejected | 503 |
| `C0006` | Payment Channel Error | 503 |
| `C0007` | Payment Channel Under Maintenance | 503 |

#### Risk Related Errors (Dxxxxx)

| Code | Description | HTTP Status |
|------|-------------|-------------|
| `D0001` | Risk Control Rejected | 406 |

#### Unknown Status Errors (Exxxxx)

| Code | Description | HTTP Status |
|------|-------------|-------------|
| `E0001` | Unknown Status (Need to query or wait for callback) | 500 |

> **Note**: When receiving `E0001` error code, it indicates transaction status is unknown. **Do not close order directly**, should call query API to confirm actual status, or wait for webhook callback notification.

## Development

```bash
python -m pip install -e ".[dev]"
ruff check src tests examples
mypy src/waffo
pytest tests/unit -q
```

## Support

- GitHub Issues: <https://github.com/waffo-com/waffo-sdk/issues>
- PyPI: <https://pypi.org/project/waffo/>

## License

MIT
