Metadata-Version: 2.4
Name: lmbtech
Version: 1.0.0
Summary: Python SDK for the LMBTech payment gateway. Supports MTN MoMo, Airtel Money, and card payments in Rwanda.
Author-email: IRANKUNDA Elyssa <elyssa001ely@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ielyssa/lmbtech-payment-gateway
Project-URL: Repository, https://github.com/ielyssa/lmbtech-payment-gateway
Project-URL: Bug Tracker, https://github.com/ielyssa/lmbtech-payment-gateway/issues
Project-URL: Documentation, https://github.com/ielyssa/lmbtech-payment-gateway#readme
Keywords: lmbtech,payment,momo,mtn,airtel,rwanda,mobile money,pesapal,gateway
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Office/Business :: Financial
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: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: django
Requires-Dist: django>=4.2; extra == "django"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# lmbtech

Python SDK for the [LMBTech](https://pay.lmbtech.rw) payment gateway.
Supports **MTN MoMo**, **Airtel Money**, and **Card payments** in Rwanda.

Built and maintained by [ATAS — Alliance for Transformative AI Systems](https://github.com/ielyssa).

---

## Why this SDK?

LMBTech's official documentation is minimal and inconsistent. This SDK
abstracts all the undocumented behavior discovered through live testing:

- The collect endpoint regularly times out but the payment **is** initiated
- Cancellations take **12-15 minutes** to appear as `failed` — not seconds
- `callback_url` is required by the API validator but **never called**
- Reference IDs are **permanently consumed** even on payment failure
- The minimum payment amount is **100 RWF**

The SDK handles all of this transparently so you never have to.

---

## Installation
```bash
pip install lmbtech
```

For Django integration:
```bash
pip install lmbtech[django]
```

**Requirements:** Python 3.8+, `requests>=2.28.0`

---

## Quick Start
```python
from lmbtech import LMBTechClient
from lmbtech.momo import generate_reference_id

client = LMBTechClient(
    app_key="your_app_key",
    secret_key="your_secret_key"
)

# Initiate a MoMo payment
result = client.momo.collect(
    phone="0788123456",
    amount=1000,
    reference_id=generate_reference_id("ORD"),
    email="customer@example.com",
    name="Jane Uwimana",
    callback_url="https://yourapp.rw/payments/",
    service_paid="Subscription"
)

if result.success:
    print("USSD prompt sent to customer's phone")
    print("Reference:", result.reference_id)
else:
    print("Failed:", result.error_message)
```

---

## Fee Handling

LMBTech charges a **5% fee** on every transaction. The SDK gives you
two options:
```python
from lmbtech.momo import calculate_total_with_fee, calculate_amount_to_send

# Option 1 (default) — customer pays amount + 5% fee
# You request 1000 RWF → customer pays 1050 RWF
result = client.momo.collect(amount=1000, ...)

# Show customer what they will pay before initiating:
info = calculate_total_with_fee(1000)
print(f"Customer pays: {info['total_amount']} RWF")

# Option 2 — customer pays exactly the amount you specify
# You want customer to pay 1000 RWF → SDK sends 952 RWF → customer pays 1000 RWF
result = client.momo.collect(amount=1000, absorb_fee=True, ...)

# Calculate what you will receive:
info = calculate_amount_to_send(1000)
print(f"You receive: {info['amount_to_send']} RWF")
```

---

## Check Payment Status
```python
status = client.momo.status("ORD-20260315-A1B2C3D4")

if status.is_paid:
    fulfill_order(status.reference_id)
elif status.is_failed:
    notify_customer_failed(status.reference_id)
elif status.is_pending:
    # Still waiting — poll again later
    pass
```

---

## Card Payments
```python
result = client.card.initiate(
    phone="0788123456",
    amount=5000,
    reference_id=generate_reference_id("ORD"),
    email="customer@example.com",
    name="Jane Uwimana",
    card_redirect_url="https://yourapp.rw/payment/complete",
    service_paid="Subscription"
)

if result.success:
    # Send redirect_url to your frontend
    # React: window.location.href = result.redirect_url
    print("Redirect URL:", result.redirect_url)
```

---

## Django Integration

### 1. Add your payment model
```python
# myapp/models.py
from django.db import models
from lmbtech.django.mixins import LMBTechPaymentMixin

class Payment(LMBTechPaymentMixin):
    # All SDK fields inherited — add your own below
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
```

### 2. Configure in AppConfig
```python
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from django.conf import settings
        from lmbtech.django import LMBTechConfig
        from myapp.handlers import on_success, on_failed, on_expired

        LMBTechConfig.setup(
            app_key=settings.LMBTECH_APP_KEY,
            secret_key=settings.LMBTECH_SECRET_KEY,
            payment_model="myapp.Payment",
            on_payment_success=on_success,
            on_payment_failed=on_failed,
            on_payment_expired=on_expired,
        )
```

### 3. Define your handlers
```python
# myapp/handlers.py
from lmbtech.django.results import (
    PaymentSuccessResult,
    PaymentFailedResult,
    PaymentExpiredResult,
)

def on_success(result: PaymentSuccessResult) -> None:
    # Fulfill the order — allocate credits, activate subscription, etc.
    order = Order.objects.get(reference_id=result.reference_id)
    order.fulfill(transaction_id=result.transaction_id)

def on_failed(result: PaymentFailedResult) -> None:
    # Notify the customer
    pass

def on_expired(result: PaymentExpiredResult) -> None:
    # Payment could not be confirmed after 20 minutes
    pass
```

### 4. Add Celery tasks
```python
# myapp/tasks.py
from celery import shared_task
from lmbtech.django.tasks import (
    poll_payment_task_logic,
    background_poll_task_logic,
)

@shared_task(bind=True, max_retries=0, soft_time_limit=55, time_limit=60)
def poll_lmbtech_payment(self, reference_id: str) -> None:
    poll_payment_task_logic(
        reference_id=reference_id,
        schedule_active=lambda ref, cd: (
            poll_lmbtech_payment.apply_async(args=[ref], countdown=cd)
        ),
        schedule_background=lambda ref, cd: (
            background_lmbtech_payment.apply_async(args=[ref], countdown=cd)
        ),
    )

@shared_task(bind=True, max_retries=0, soft_time_limit=55, time_limit=60)
def background_lmbtech_payment(self, reference_id: str) -> None:
    background_poll_task_logic(
        reference_id=reference_id,
        schedule_task=lambda ref, cd: (
            background_lmbtech_payment.apply_async(args=[ref], countdown=cd)
        ),
    )
```

---

## Payment Lifecycle

The SDK uses a two-phase polling design based on confirmed LMBTech behavior:
```
Phase 1 — Active (0 to 5 minutes)
    Poll every 15 seconds while user is on payment page.
    Catches approvals (~30 seconds typical).
    After 5 minutes → transition to Phase 2.

Phase 2 — Background (5 to 20 minutes)
    Poll every 2 minutes after user leaves page.
    Catches cancellations (~12-15 minutes typical).
    After 20 minutes → expire permanently.
```

Payment status flow:
```
pending → success    Payment approved by customer
pending → failed     Payment rejected (rare in first 5 min)
pending → timeout    Active window ended, background running
timeout → failed     Cancellation confirmed by LMBTech (~12 min)
timeout → success    Rare — approval during background phase
timeout → expired    20 minutes passed, no confirmation
```

---

## Error Handling
```python
from lmbtech import (
    LMBTechError,          # base — catch all SDK errors
    LMBTechValidationError, # bad input — fix your code
    LMBTechNetworkError,    # could not reach LMBTech — check status before failing
    LMBTechAPIError,        # LMBTech returned an error response
)

try:
    result = client.momo.collect(...)
except LMBTechValidationError as e:
    # Fix your input — do not retry
    print("Input error:", e)
except LMBTechNetworkError as e:
    # IMPORTANT: payment may have been initiated before the error
    # Check status before marking as failed
    status = client.momo.status(reference_id)
except LMBTechAPIError as e:
    print(f"API error {e.status_code}:", e)
    print("Response:", e.raw_response)
```

---

## Phone Number Formats

The SDK accepts any Rwandan phone format and normalizes automatically:
```python
from lmbtech.momo import normalize_phone, detect_network

normalize_phone("0788123456")    # → "+250788123456"
normalize_phone("+250788123456") # → "+250788123456"
normalize_phone("250788123456")  # → "+250788123456"

detect_network("0788123456")  # → "MTN"
detect_network("0725000000")  # → "Airtel"
```

Valid prefixes:
- **MTN Rwanda:** 078, 079
- **Airtel Rwanda:** 072, 073

---

## Reference ID Format
```python
from lmbtech.momo import generate_reference_id

ref = generate_reference_id("ORD")  # → "ORD-20260315-A1B2C3D4"
ref = generate_reference_id("PAY")  # → "PAY-20260315-F2E1D3C4"
```

**Important:** Reference IDs are permanently consumed by LMBTech even
if the payment fails. Never reuse a reference ID.

---

## Requirements

- Python 3.8+
- `requests >= 2.28.0`
- Django 3.2+ (only for `lmbtech.django`)
- Celery + Redis (only for background polling)

---

## License

MIT License — see [LICENSE](LICENSE)

**Author:** IRANKUNDA Elyssa /
[ATAS — Alliance for Transformative AI Systems](https://github.com/ielyssa)

**Repository:** https://github.com/ielyssa/lmbtech-payment-gateway
