Metadata-Version: 2.4
Name: valoris-payment-gateway
Version: 0.1.0
Summary: Unified payment gateway library supporting eSewa, Khalti, ConnectIPS and more
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: connectips
Requires-Dist: cryptography>=41.0.0; extra == "connectips"
Provides-Extra: all
Requires-Dist: cryptography>=41.0.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: cryptography>=41.0.0; extra == "dev"

# Valoris Payment Gateway

A unified, reusable Python payment gateway library supporting eSewa, Khalti, ConnectIPS, and FonePay. Built for FastAPI apps but framework-agnostic.

## Installation

```bash
# Base (eSewa, Khalti, FonePay)
pip install git+https://github.com/Techore/valoris-payment-gateway.git

# With Connect IPS support (requires cryptography for RSA signing)
pip install "valoris-payment-gateway[connectips] @ git+https://github.com/Techore/valoris-payment-gateway.git"

# All providers
pip install "valoris-payment-gateway[all] @ git+https://github.com/Techore/valoris-payment-gateway.git"
```

---

## Supported Providers

| Provider   | Status | Signature Method |
|------------|--------|-----------------|
| eSewa      | Ready  | HMAC-SHA256     |
| Khalti     | Ready  | API Key Header  |
| ConnectIPS | Ready  | RSA (SHA256withRSA) |
| FonePay    | Ready  | HMAC-SHA512     |

---

## Provider Configuration Reference

### eSewa

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `secret_key` | str | Yes | HMAC secret key from eSewa merchant dashboard |
| `product_code` | str | Yes | Merchant product code (e.g. `EPAYTEST` for sandbox) |
| `sandbox` | bool | No | Use sandbox environment (default: `True`) |

**Environment variables (recommended):**
```env
ESEWA_SECRET_KEY=8gBm/:&EnhH.1/q
ESEWA_PRODUCT_CODE=EPAYTEST
```

**Sandbox test credentials:**
- eSewa IDs: `9806800001` - `9806800005`
- Password: `Nepal@123`
- MPIN: `1122`
- OTP: `123456`

**Verify kwargs:** `total_amount` (float, NPR)

```python
provider = create_provider(
    "esewa",
    secret_key=os.getenv("ESEWA_SECRET_KEY"),
    product_code=os.getenv("ESEWA_PRODUCT_CODE"),
    sandbox=True,
)
```

---

### Khalti

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `secret_key` | str | Yes | Live secret key from Khalti admin dashboard |
| `sandbox` | bool | No | Use sandbox environment (default: `True`) |
| `website_url` | str | No | Your website URL (required by Khalti API) |

**Environment variables (recommended):**
```env
KHALTI_SECRET_KEY=your_secret_key_here
KHALTI_WEBSITE_URL=https://yourapp.com
```

**Sandbox:** Get test keys from [test-admin.khalti.com](https://test-admin.khalti.com)
**Production:** Get live keys from [admin.khalti.com](https://admin.khalti.com)

**Sandbox test credentials:**
- Khalti IDs: `9800000000` - `9800000005`
- Password: (not needed for ePayment)
- MPIN: `1111`
- OTP: `987654`

**Note:** Amount in `PaymentRequest` is in NPR. The library auto-converts to paisa (x100) for the Khalti API.

**Verify kwargs:** `pidx` (str, from Khalti initiation response)

```python
provider = create_provider(
    "khalti",
    secret_key=os.getenv("KHALTI_SECRET_KEY"),
    website_url=os.getenv("KHALTI_WEBSITE_URL"),
    sandbox=True,
)
```

---

### ConnectIPS

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `merchant_id` | str | Yes | Merchant ID from NCHL |
| `app_id` | str | Yes | Application ID from NCHL |
| `app_name` | str | Yes | Application display name |
| `app_password` | str | Yes | Password for validation API (Basic Auth) |
| `pfx_path` | str | Yes | Path to CREDITOR.pfx certificate file |
| `pfx_password` | str | Yes | Password for the .pfx file |
| `sandbox` | bool | No | Use sandbox environment (default: `True`) |

**Environment variables (recommended):**
```env
CONNECTIPS_MERCHANT_ID=550
CONNECTIPS_APP_ID=MER-550-APP-1
CONNECTIPS_APP_NAME=YourAppName
CONNECTIPS_APP_PASSWORD=your_password
CONNECTIPS_PFX_PATH=/path/to/CREDITOR.pfx
CONNECTIPS_PFX_PASSWORD=your_pfx_password
```

**Important:** Requires `cryptography` package. Install with `pip install valoris-payment-gateway[connectips]`.

**Note:**
- CREDITOR.pfx is provided by NCHL for testing. You get your own certificate for production.
- Success/failure URLs must be pre-configured with NCHL support (not passed in the API).
- Amount is in NPR — auto-converted to paisa (x100) for the API.

**Verify kwargs:** `amount` (float, NPR)

```python
provider = create_provider(
    "connectips",
    merchant_id=os.getenv("CONNECTIPS_MERCHANT_ID"),
    app_id=os.getenv("CONNECTIPS_APP_ID"),
    app_name=os.getenv("CONNECTIPS_APP_NAME"),
    app_password=os.getenv("CONNECTIPS_APP_PASSWORD"),
    pfx_path=os.getenv("CONNECTIPS_PFX_PATH"),
    pfx_password=os.getenv("CONNECTIPS_PFX_PASSWORD"),
    sandbox=True,
)
```

---

### FonePay

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `merchant_code` | str | Yes | Merchant ID/PID from FonePay (via your bank) |
| `secret_key` | str | Yes | HMAC secret key from FonePay (via your bank) |
| `sandbox` | bool | No | Use sandbox environment (default: `True`) |

**Environment variables (recommended):**
```env
FONEPAY_MERCHANT_CODE=your_merchant_code
FONEPAY_SECRET_KEY=your_secret_key
```

**Note:** Contact your bank to obtain merchant credentials. Separate keys for dev/production.

**Sandbox test credentials:**
- Mobile: `9801111111`
- Password: `123`
- MPIN: `123`

**Verify kwargs:** `amount` (float), `bid` (str, bank/transaction ID), `uid` (str, FonePay unique ID)

```python
provider = create_provider(
    "fonepay",
    merchant_code=os.getenv("FONEPAY_MERCHANT_CODE"),
    secret_key=os.getenv("FONEPAY_SECRET_KEY"),
    sandbox=True,
)
```

---

## Usage

### Common Flow (all providers)

```python
import os
from valoris_payment import create_provider, PaymentRequest

# 1. Create provider
provider = create_provider("esewa", secret_key="...", product_code="...", sandbox=True)

# 2. Initiate payment
request = PaymentRequest(
    amount=1000.0,                                        # NPR
    transaction_id="TXN-001",                             # your unique ID
    product_name="Premium Plan",
    success_url="https://yourapp.com/payment/success",
    failure_url="https://yourapp.com/payment/failure",
)
result = await provider.initiate(request)
# result.payment_url  -> redirect user here
# result.raw          -> form data (for providers that need POST form)

# 3. Handle callback (on redirect back)
callback = await provider.handle_callback(query_params)
# callback.status, callback.transaction_id, callback.provider_ref

# 4. Verify payment (server-side confirmation)
verification = await provider.verify("TXN-001", total_amount=1000.0)
# verification.status -> PaymentStatus.COMPLETED / FAILED / etc.
```

### FastAPI Integration Example

```python
from fastapi import FastAPI, Query, Request
from fastapi.responses import RedirectResponse
from valoris_payment import create_provider, PaymentRequest, PaymentStatus
import os

app = FastAPI()

esewa = create_provider(
    "esewa",
    secret_key=os.getenv("ESEWA_SECRET_KEY"),
    product_code=os.getenv("ESEWA_PRODUCT_CODE"),
    sandbox=True,
)

khalti = create_provider(
    "khalti",
    secret_key=os.getenv("KHALTI_SECRET_KEY"),
    website_url=os.getenv("KHALTI_WEBSITE_URL", "https://yourapp.com"),
    sandbox=True,
)

@app.post("/payments/initiate")
async def initiate_payment(amount: float, transaction_id: str, provider_name: str = "esewa"):
    providers = {"esewa": esewa, "khalti": khalti}
    provider = providers[provider_name]

    request = PaymentRequest(
        amount=amount,
        transaction_id=transaction_id,
        product_name="Order Payment",
        success_url="https://yourapp.com/payments/success",
        failure_url="https://yourapp.com/payments/failure",
    )
    result = await provider.initiate(request)
    return {"payment_url": result.payment_url, "provider_ref": result.provider_ref}

@app.get("/payments/success")
async def payment_success(request: Request):
    params = dict(request.query_params)
    # Detect provider from callback params and verify
    if "data" in params:  # eSewa
        callback = await esewa.handle_callback(params)
    elif "pidx" in params:  # Khalti
        callback = await khalti.handle_callback(params)
    return {"status": callback.status, "transaction_id": callback.transaction_id}
```

---

## PaymentRequest Extra Fields

Provider-specific fields can be passed via the `extra` dict:

### eSewa
```python
PaymentRequest(
    ...,
    extra={
        "tax_amount": "50",
        "product_service_charge": "10",
        "product_delivery_charge": "20",
    }
)
```

### Khalti
```python
PaymentRequest(
    ...,
    extra={
        "website_url": "https://yourapp.com",
        "customer_info": {"name": "John", "email": "john@example.com", "phone": "9800000000"},
    }
)
```

### ConnectIPS
```python
PaymentRequest(
    ...,
    extra={
        "currency": "NPR",
        "reference_id": "REF-001",
        "remarks": "Order payment",
        "particulars": "Product purchase",
    }
)
```

### FonePay
```python
PaymentRequest(
    ...,
    extra={
        "currency": "NPR",
        "r1": "Additional info",
        "r2": "More info",
    }
)
```

---

## Adding a Custom Provider

```python
from valoris_payment import PaymentProvider, register_provider

class MyProvider(PaymentProvider):
    name = "myprovider"

    async def initiate(self, request):
        ...

    async def verify(self, transaction_id, **kwargs):
        ...

    async def handle_callback(self, payload):
        ...

register_provider("myprovider", MyProvider)
```

---

## Development

```bash
git clone git@github.com:Techore/valoris-payment-gateway.git
cd valoris-payment-gateway
pip install -e ".[dev]"
pytest
```
