Metadata-Version: 2.5
Name: nahupay
Version: 0.1.0
Summary: Official Python SDK for the NahuPay payment platform
Project-URL: Homepage, https://nahupay.com
Project-URL: Documentation, https://docs.nahupay.com
Project-URL: Repository, https://github.com/nahupay/nahupay-python
Author: NahuPay
License: MIT
License-File: LICENSE
Keywords: ethiopia,mpesa,nahupay,payment,telebirr
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.8
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: responses>=0.23.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# nahupay-python

Official Python SDK for the **NahuPay** payment platform.

Zero runtime dependencies beyond `requests`. Requires Python ≥ 3.8.

---

## Installation

```bash
pip install nahupay
```

---

## Quick start

```python
import os
from nahupay import NahuPay

nahupay = NahuPay(
    api_key=os.environ["NAHUPAY_SECRET_KEY"],  # sk_test_… or sk_live_…
    base_url="http://localhost:8080/api/v1",   # omit in production
)
```

---

## Payments

### Create a payment

Call this from **your server** after the customer clicks "Pay". You get back a
`checkout_url` — redirect the customer there to complete payment.

```python
payment = nahupay.payments.create(
    amount=500,                               # ETB, minimum 1.00
    customer_email="abebe@example.com",
    customer_name="Abebe Bikila",
    description="Order #1042 — 2 items",
    return_url="https://myshop.com/ty?ref={PAYMENT_REFERENCE}",
    webhook_url="https://myshop.com/webhooks/nahupay",
    metadata={"order_id": "1042", "user_id": "u_abc"},
)

# Redirect the customer to the hosted checkout page
redirect(payment.checkout_url)
```

### Retrieve a payment

```python
payment = nahupay.payments.retrieve("PAY-20260523-ABCD1234")
print(payment.status)  # "SUCCESS" | "PENDING" | "FAILED" | …
```

### List payments

```python
page = nahupay.payments.list(status="SUCCESS", size=50)
for p in page:                   # Page is iterable
    print(p.reference, p.amount)
print("Total:", page.total_elements)
```

### Cancel a payment

```python
nahupay.payments.cancel("PAY-xxx")
```

---

## Refunds

```python
# Full refund
refund = nahupay.payments.refund("PAY-xxx")

# Partial refund with reason
refund = nahupay.payments.refund("PAY-xxx", amount=100, reason="Partial return")

# List refunds for a payment
refunds = nahupay.payments.list_refunds("PAY-xxx")

# Top-level aliases
refund  = nahupay.refunds.create("PAY-xxx", amount=100)
refunds = nahupay.refunds.list("PAY-xxx")
```

---

## Webhooks

NahuPay sends a `POST` to your `webhook_url` whenever a payment status changes.
Every request is HMAC-SHA256 signed.

### ⚠️ Use the raw request body

Pass the **raw bytes** of the request body — not the already-decoded dict — or
signature verification will fail.

### Flask example

```python
import os
import json
from flask import Flask, request
from nahupay import NahuPay, NahuPayWebhookError

app = Flask(__name__)

@app.route("/webhooks/nahupay", methods=["POST"])
def nahupay_webhook():
    try:
        event = NahuPay.webhooks.verify(
            request.get_data(),                                  # raw bytes
            request.headers.get("X-NahuPay-Signature"),
            os.environ["NAHUPAY_SECRET_KEY"],
        )
    except NahuPayWebhookError as e:
        return {"error": str(e)}, 400

    if event.event == "payment.success":
        meta = json.loads(event.payment.metadata or "{}")
        print(f"✅ {event.payment.reference}  ETB {event.payment.amount}  order={meta.get('order_id')}")

    if event.event == "payment.failed":
        print(f"❌ {event.payment.reference} failed:", event.payment.failure_reason)

    return {"received": True}
```

### Django example

```python
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.http import JsonResponse
from nahupay import NahuPay, NahuPayWebhookError

@csrf_exempt
@require_POST
def nahupay_webhook(request):
    try:
        event = NahuPay.webhooks.verify(
            request.body,                                        # raw bytes
            request.headers.get("X-NahuPay-Signature"),
            os.environ["NAHUPAY_SECRET_KEY"],
        )
    except NahuPayWebhookError as e:
        return JsonResponse({"error": str(e)}, status=400)

    # handle event …
    return JsonResponse({"received": True})
```

### Webhook event reference

| Event | Trigger |
|-------|---------|
| `payment.success` | Payment completed — safe to fulfil the order |
| `payment.failed`  | Payment processing failed — notify the customer |

The `event.payment` field is a full `Payment` object.

---

## Test mode & simulation

Use a `sk_test_…` key to work in sandbox mode. No real money moves.

```python
nahupay = NahuPay(api_key="sk_test_...")

# 1. Create a payment
payment = nahupay.payments.create(
    amount=100,
    customer_email="test@example.com",
    webhook_url="http://localhost:5001/webhooks/nahupay",
)

# 2. Instantly mark it as paid (fires webhook)
paid = nahupay.payments.simulate_success(payment.reference)
print(paid.status)   # "SUCCESS"

# 3. Or mark it as failed
failed = nahupay.payments.simulate_failure(payment.reference)
print(failed.status)  # "FAILED"
```

`simulate_success` and `simulate_failure` raise `NahuPayApiError` (HTTP 403) with a `sk_live_` key.

---

## Error handling

```python
from nahupay import NahuPayApiError, NahuPayWebhookError

try:
    nahupay.payments.retrieve("PAY-does-not-exist")
except NahuPayApiError as e:
    print(e.status_code)  # 404
    print(e.code)         # "NOT_FOUND"
    print(e.message)      # "Payment not found"
```

| Exception | When raised |
|-----------|-------------|
| `NahuPayApiError` | Non-2xx HTTP or `success=false` from the API |
| `NahuPayWebhookError` | Invalid / missing webhook signature |
| `NahuPayError` | Base class — catch to handle all SDK errors |

---

## Configuration

```python
nahupay = NahuPay(
    api_key="sk_test_...",

    # API base URL (default: https://api.nahupay.com/api/v1)
    base_url="http://localhost:8080/api/v1",

    # Per-request timeout in seconds (default: 30)
    timeout=10,

    # Custom requests.Session (useful for testing or proxies)
    session=my_session,
)
```

---

## SDK architecture

| File | Role |
|------|------|
| `nahupay/client.py` | `NahuPay` main class — validates key, exposes `.payments`, `.refunds`, `.webhooks` |
| `nahupay/http_client.py` | `HttpClient` — builds URLs, sets auth headers, unwraps `ApiEnvelope` |
| `nahupay/resources/payments.py` | `PaymentsResource` — all 8 payment methods |
| `nahupay/resources/refunds.py` | `RefundsResource` — create + list |
| `nahupay/webhooks.py` | `NahuPayWebhooks.verify()` — HMAC-SHA256 with constant-time compare |
| `nahupay/errors.py` | `NahuPayError`, `NahuPayApiError`, `NahuPayWebhookError` |
| `nahupay/types.py` | `Payment`, `Refund`, `Page`, `WebhookEvent` model classes |
