Metadata-Version: 2.4
Name: sofizpay-python-sdk
Version: 1.1.0
Summary: Professional Python SDK for SofizPay payments using Stellar blockchain
Home-page: https://github.com/SofizPay/sofizpay-python-sdk
Author: SofizPay Team
Author-email: SofizPay Team <support@sofizpay.com>
Maintainer-email: SofizPay Team <support@sofizpay.com>
License: MIT
Project-URL: Homepage, https://sofizpay.com
Project-URL: Documentation, https://github.com/SofizPay/sofizpay-python-sdk#readme
Project-URL: Repository, https://github.com/SofizPay/sofizpay-python-sdk
Project-URL: Bug Reports, https://github.com/SofizPay/sofizpay-python-sdk/issues
Project-URL: Changelog, https://github.com/SofizPay/sofizpay-python-sdk/blob/main/CHANGELOG.md
Keywords: stellar,payment,blockchain,cryptocurrency,DZT,sofizpay,fintech
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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 :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stellar-sdk>=8.0.0
Requires-Dist: requests>=2.25.0
Requires-Dist: asyncio-throttle>=1.0.0
Requires-Dist: websockets>=10.0
Requires-Dist: cryptography>=3.4.0
Requires-Dist: pycryptodome>=3.15.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: flake8>=6.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: coverage>=7.0.0; extra == "dev"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

<div align="center">
  <img src="https://github.com/SofizPay/sofizpay-python-sdk/blob/main/assets/sofizpay-logo.png?raw=true" alt="SofizPay Logo" width="200" />

  <h2>SofizPay Python SDK</h2>
  <p><strong>The official Python SDK for secure digital payments on the SofizPay platform.</strong></p>

  [![PyPI version](https://badge.fury.io/py/sofizpay-python-sdk.svg)](https://pypi.org/project/sofizpay-python-sdk/)
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  [![Python](https://img.shields.io/badge/Python-3.8%2B-blue.svg)](https://www.python.org/)
</div>

---

## 📋 Table of Contents

- [Overview](#overview)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Methods](#core-methods)
- [Digital Services (Missions)](#digital-services-missions)
- [Bank Integration (CIB)](#bank-integration-cib)
- [Real-time Transaction Streaming](#real-time-transaction-streaming)
- [Response Format](#response-format)
- [Security Best Practices](#security-best-practices)
- [Support](#support)

---

## 🌟 Overview

The SofizPay Python SDK provides a complete `async` interface for integrating **DZT digital payments** into Python applications, Django backends, and FastAPI services. It offers high-level abstractions for Stellar payments, transaction history, and digital service recharges.

**Key Benefits:**
- ⚡ **Fully Async:** Built on `aiohttp` for non-blocking I/O
- 🌍 **Framework Agnostic:** Works perfectly with Django, FastAPI, Flask, or CLI tools
- 📊 **Exhaustive History:** Captures Path Payments, Trustlines, and account events
- 🏦 **CIB/Dahabia:** Simple bank deposit link generation
- 📱 **Missions:** Phone, Internet, and Game top-ups via Mission APIs

---

## 📦 Installation

### pip

```bash
pip install sofizpay-python-sdk
```

**Requirements:** 
- Python `>= 3.8`
- Dependencies: `aiohttp`, `stellar-sdk`

---

## 🚀 Quick Start

```python
import asyncio, os
from sofizpay.client import SofizPayClient

async def main():
    # Initialize the async client
    client = SofizPayClient()

    # 1. Check DZT balance
    balance = await client.get_balance('YOUR_PUBLIC_KEY')
    if balance['success']:
        print(f"💰 Balance: {balance['balance']} DZT")

    # 2. Send a DZT payment
    result = await client.send_payment(
        source_secret='YOUR_SECRET_KEY',           # 56-char Stellar seed starting with 'S'
        destination_public_key='RECIPIENT_PUBLIC_KEY', # Recipient's public key
        amount='100',                                  # Amount as string
        memo='Invoice #1234'                           # Optional memo (max 28 chars)
    )

    if result.get('success'):
        print(f"✅ Payment sent! Hash: {result['transactionHash']}")
    else:
        print(f"❌ Error: {result.get('error')}")

if __name__ == "__main__":
    # Run the async main function
    asyncio.run(main())
```

---

## 🔧 Core Methods

### `get_balance(public_key)`

Returns the current **DZT** balance for a given Stellar account.

```python
# Fetch balance for a specific public key
result = await client.get_balance('GCAZI...YOUR_PUBLIC_KEY')

# Response
{
  'success':      True,
  'balance':      '1500.0000000',
  'publicKey':    'GCAZI...',
  'asset_code':   'DZT',
  'asset_issuer': 'GCAZI7YBLIDJWIVEL7ETNAZGPP3LC24NO6KAOBWZHUERXQ7M5BC52DLV',
  'timestamp':    '2025-07-28T10:30:00Z'
}
```

---

### `send_payment(source_secret, destination_public_key, amount, memo?)`

Submits a DZT payment to the Stellar network.

```python
result = await client.send_payment(
    source_secret='SXXX...YOUR_SECRET',           # 56-char Stellar seed starting with 'S'
    destination_public_key='GXXX...RECIPIENT',    # Recipient's public key
    amount='250.50',                              # Amount in DZT
    memo='Order #5567'                            # Optional memo (max 28 chars)
)

# Success Response
{
  'success':            True,
  'transactionId':      'abc123...hash',
  'transactionHash':    'abc123...hash',
  'amount':             '250.50',
  'memo':               'Order #5567',
  'destinationPublicKey': 'GXXX...',
  'timestamp':          '2025-07-28T10:30:00Z'
}
```

> ⚠️ **Memo Truncation:** Memos longer than 28 characters are automatically truncated.

---

### `get_transactions(public_key, limit?)`

Fetches **exhaustive transaction history** via Stellar. Includes payments, trustlines, and account creation.

```python
# Fetch up to 100 recent transactions
transactions = await client.get_transactions('YOUR_PUBLIC_KEY', 100)

for tx in transactions:
    # Print details for each captured operation
    print(f"[{tx['created_at']}] {tx['type']} — {tx['amount']} DZT")
```

---

### `search_transactions_by_memo(public_key, memo, limit?)`

Performs a case-insensitive search over recent transactions.

```python
# Perform case-insensitive search over user's recent transactions
results = await client.search_transactions_by_memo('YOUR_PUBLIC_KEY', 'Order #1234', 10)
if results['success']:
    print(f"Found {results['totalFound']} matches")
```

---

## 📱 Digital Services (Missions)

Mission APIs allow users to spend DZT on real-world digital services. All calls require `encrypted_sk`.

### Phone Recharge

```python
result = await client.recharge_phone({
    'encrypted_sk': 'USER_ENCRYPTED_SECRET_KEY', # User's encrypted secret key
    'phone':        '0661000000',               # Recipient's phone number
    'operator':     'mobilis',                  # 'mobilis' | 'djezzy' | 'ooredoo'
    'amount':       '100',                      # Recharge amount
    'offer':        'Top'                       # Offer type (e.g., 'Top', 'Pix')
})

if result['success']:
    # Process successful recharge response
    print('✅ Phone recharged:', result['data'])
```

### Internet Recharge (Idoom 4G)

```python
result = await client.recharge_internet({
    'encrypted_sk': 'USER_ENCRYPTED_SECRET_KEY', # User's encrypted secret key
    'phone':        '0661000000',               # Account phone number
    'operator':     'algerie-telecom',           # Network provider
    'amount':       '2000',                     # Recharge amount
    'offer':        'prepaid'                    # Offer type (e.g., 'prepaid', 'postpaid')
})
```

### Game Top-up (FreeFire, PUBG)

```python
result = await client.recharge_game({
    'encrypted_sk': 'USER_ENCRYPTED_SECRET_KEY',
    'operator':     'freefire',   # e.g., 'freefire', 'pubg'
    'playerId':     '123456789',
    'amount':       '500',        # 'amount' from getProducts()
    'offer':        'diamonds'    # 'name' from getProducts()
})
```

### Bill Payment

```python
result = await client.pay_bill({
    'encrypted_sk': 'USER_ENCRYPTED_SECRET_KEY',
    'operator':     'sonelgaz', # e.g., 'sonelgaz', 'ade'
    'bill_id':      'BILL_999',
    'amount':       '1500'
})
```

### Get Available Products

Fetches the list of available services, operators, and their associated offers (products).

```python
products = await client.get_products()
if products['success']:
    # Use products['data'] to populate Missions 'offer' and 'amount'
    print('Available services:', products['data'])
```

> [!TIP]
> Each product in the `data` array usually contains `name` and `amount`. When calling recharge methods, use the product's `name` for the `offer` field and the product's `amount` for the `amount` field.

### Operation History & Details

```python
# Recent operations (paginated)
history = await client.get_operation_history('USER_ENCRYPTED_SK', limit=10, offset=0)
if history['success']:
    print('Last 10 operations:', history['data'])

# Details of a specific operation
details = await client.get_operation_details('OPERATION_ID', 'USER_ENCRYPTED_SK')
```

### Get Available Products

```python
# Fetch available charging services and products
products = await client.get_products()
if products.get('success'):
    # Iterate through available products to find names and amounts
    print('Available services:', products['data'])
```

> [!TIP]
> Use the product `name` for the `offer` field and the product `amount` for the `amount` field.

---

## 🏦 Bank Integration (CIB & EDAHABIA)

Generate secure Dahabia/CIB bank payment links with optional real-time webhook callbacks.

```python
result = await client.make_cib_transaction({
    'account':     'YOUR_STELLAR_PUBLIC_KEY',       # Your merchant Stellar public key
    'amount':      2500,                            # Amount in DZD (Algerian Dinars)
    'full_name':   'Ahmed Benali',                  # Customer's full name
    'phone':       '0661234567',                    # Customer's phone number
    'email':       'ahmed@example.com',              # Customer's email
    'memo':        'Order #789',                    # Internal order reference
    'return_url':  'https://yoursite.com/callback', # Redirect after payment
    'webhook_url': 'https://yoursite.com/api/cib-webhook', # Asynchronous Webhook callback
    'language':    'ar',                            # 'ar', 'fr', or 'en'
    'redirect':    'no'                             # 'yes' for auto-redirect
})

if result.get('success'):
    # Top-level direct properties
    payment_url = result['payment_url']
    order_id = result['cib_transaction_id']
    print(f"Redirect customer to: {payment_url}")
```

---

## 🧪 Sandbox Environment & Mock Cards

The SofizPay Sandbox simulates the SATIM CIB/EDAHABIA payment system without connecting to real bank accounts.

```python
# Initialize client in Sandbox mode (or pass 'is_sandbox': True)
sandbox_client = SofizPayClient(sandbox=True)

result = await sandbox_client.make_sandbox_cib_transaction({
    'account':     'YOUR_PUBLIC_KEY',
    'amount':      150.0,
    'full_name':   'Sandbox Tester',
    'phone':       '0555000000',
    'email':       'sandbox@example.com',
    'webhook_url': 'https://yoursite.com/api/cib-webhook',
    'return_url':  'https://yoursite.com/callback'
})

if result.get('success'):
    print(f"Sandbox Payment URL: {result['payment_url']}")
    cib_id = result['cib_transaction_id']

    # Check sandbox order status
    status = await sandbox_client.check_sandbox_cib_status(cib_id)
    print("Order Status:", status.get('data'))
```

### Sandbox Test Cards

Use the following test credentials on the sandbox payment page:

| Card Number | Expiry | CVV | Cardholder Name | Result | Note |
|-------------|--------|-----|-----------------|--------|------|
| `6280581001234567` | `12/28` | `123` | `TEST SUCCESS` | ✅ Approved | Use OTP `123456` |
| `6280581009876543` | `12/29` | `456` | `TEST SUCCESS TWO` | ✅ Approved | Use OTP `123456` |
| `6280581005555555` | `06/28` | `999` | `TEST DECLINED` | ❌ Declined by issuer | Tests card refusal |
| `6280581004444444` | `09/28` | `444` | `TEST INSUFFICIENT` | ❌ Insufficient funds | Tests insufficient balance |
| `6280581003333333` | `03/21` | `333` | `TEST EXPIRED` | ❌ Expired card | Tests expired card |
| `6280581002222222` | `11/28` | `222` | `TEST TIMEOUT` | ❌ Timeout | Tests transaction timeout |

> [!TIP]
> **OTP Code:** Always enter `123456` on the OTP page for approved tests. Entering any other OTP triggers an `Invalid OTP` error.

---

## 🔔 Webhooks & Signature Verification

When a payment succeeds or fails, SofizPay delivers a server-to-server asynchronous HTTP `POST` event notification (`cib.payment.updated`) to your registered `webhook_url`.

### Webhook Event Payload

```json
{
  "event": "cib.payment.updated",
  "payment_status": "success",
  "transaction_id": "bb979126-4e55-49dd-8ecc-a1d59867d2b0",
  "cib_transaction_id": "5965287001",
  "order_number": 5965287001,
  "order_status": 2,
  "amount": 150000,
  "currency": "012",
  "resp_code": "00",
  "resp_code_desc": "Approved",
  "message": "https://yoursite.com/api/cib-webhook/success1500005965287001",
  "signature": "MEQCIFz8v982hfkjdsf983274hfkjsdf...=="
}
```

### Verifying Signatures (`verify_webhook_signature`)

Always verify the RSA SHA-256 signature before processing payments in your database:

```python
from fastapi import FastAPI, Request, HTTPException
from sofizpay.client import SofizPayClient

app = FastAPI()
client = SofizPayClient()

@app.post("/api/cib-webhook")
async def handle_cib_webhook(request: Request):
    payload = await request.json()

    # 1. Validate official digital signature
    if not client.verify_webhook_signature(payload):
        raise HTTPException(status_code=400, detail="Invalid digital signature")

    # 2. Process order upon confirmation
    if payload.get("payment_status") == "success":
        order_number = payload.get("order_number") or payload.get("cib_transaction_id")
        amount_in_dzd = (payload.get("amount") or 0) / 100
        
        # ✅ Mark order as PAID in your database:
        # await db.orders.update({"order_id": order_number}, {"status": "PAID", "amount": amount_in_dzd})
        print(f"Payment approved for order {order_number} ({amount_in_dzd} DZD)")

    return {"status": "OK"}
```

---

## 🔄 Return URL vs. Webhook Notifications

| Feature | Return URL (`return_url`) | Webhook (`webhook_url`) |
| :--- | :--- | :--- |
| **Trigger mechanism** | Client browser redirect | Server-to-server direct HTTP POST |
| **Reliability** | Can fail if user closes browser / network drops | Guaranteed server delivery |
| **Security** | Susceptible to tampering | Cryptographically signed (RSA-2048) |
| **Recommended use** | Display "Thank you" screen | Update database, activate subscriptions |

---

## 🔴 Real-time Transaction Streaming

The Python SDK uses polling to monitor accounts for new transactions.

### `setup_transaction_stream(public_key, callback, from_now?, check_interval?)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `public_key` | `str` | required | Stellar account to monitor |
| `callback` | `async function` | required | Called for each new transaction |
| `from_now` | `bool` | `True` | `True`: new txs only; `False`: load history first |
| `check_interval` | `int` | `30` | Polling interval in seconds |

```python
async def on_tx(tx):
    print(f"New {tx['type']}: {tx['amount']} DZT — memo: {tx['memo']}")

stream_id = await client.setup_transaction_stream(
    'YOUR_PUBLIC_KEY', on_tx, from_now=True, check_interval=15
)
```

---

## 📤 Response Format

All async methods return a dictionary with a `success` flag:

```python
# ✅ Success
{
  'success':   True,
  'data':      {...}, # method-specific
  'timestamp': '2025-07-28T10:30:00Z'
}

# ❌ Failure
{
  'success':   False,
  'error':     'Error message',
  'timestamp': '2025-07-28T10:30:00Z'
}
```

---

## 🛡️ Security Best Practices

- ❌ Never expose secret keys in public repositories or client-side code.
- ✅ Use environment variables (`os.getenv('SOFIZPAY_SECRET')`).
- ✅ Keep `encrypted_sk` secure in your backend database.
- ✅ Use `encrypted_sk` for all Mission API calls (never the raw secret).
- ✅ Run payment logic on backend only.

---

## 📞 Support

- 🌐 **Website**: [SofizPay.com](https://sofizpay.com)
- 📚 **Full Docs**: [docs.sofizpay.com](https://docs.sofizpay.com)
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/kenandarabeh/sofizpay-python-sdk/issues)

---

## License

MIT © [SofizPay Team](https://github.com/kenandarabeh)

**Built with ❤️ for Python developers | Version `1.1.0`**

