Metadata-Version: 2.4
Name: django-amatopay
Version: 0.1.0
Summary: Django app that plugs the AmatoPay payment gateway into your project: local models, admin UI, webhook receiver and Django signals, built on the amatopay client library
Keywords: amatopay,django,payments,payment-gateway,burundi,mobile-money
Author: AmatoPay
Author-email: AmatoPay <ndiku6241@gmail.com>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
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 :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Dist: django>=4.2
Requires-Dist: amatopay>=0.1.1
Requires-Python: >=3.10
Project-URL: Homepage, https://amatopay.bi
Project-URL: Documentation, https://amatopay.bi/developers/
Description-Content-Type: text/markdown

# django-amatopay

Drop-in Django app for [AmatoPay](https://amatopay.bi): local models mirroring every AmatoPay
object, a full Django admin management UI, a webhook receiver with signature verification, and
Django signals for every event type — all built on top of the
[`amatopay`](https://pypi.org/project/amatopay/) client library.

## Install

```bash
uv add django-amatopay
# or
pip install django-amatopay
```

## Setup

1. Add to `INSTALLED_APPS`:

   ```python
   INSTALLED_APPS = [
       ...,
       "django_amatopay",
   ]
   ```

2. Configure your merchant credentials:

   ```python
   AMATOPAY_API_KEY = env("AMATOPAY_API_KEY")                # sk_live_... / sk_test_...
   AMATOPAY_WEBHOOK_SECRET = env("AMATOPAY_WEBHOOK_SECRET")   # whsec_...
   # AMATOPAY_BASE_URL = "https://api.amatopay.bi/api/v1"    # optional, this is the default
   ```

3. Run migrations:

   ```bash
   python manage.py migrate
   ```

4. Mount the webhook receiver and register the resulting URL with AmatoPay:

   ```python
   urlpatterns = [
       path("amatopay/", include("django_amatopay.urls")),
   ]
   # -> register https://yoursite.com/amatopay/webhook/ with AmatoPay
   ```

5. (Optional) backfill existing data:

   ```bash
   python manage.py sync_amatopay
   ```

That's it — `/admin/` now has an "AmatoPay" section listing every checkout session, payment,
delivery, and buyer-protection claim, kept live by incoming webhooks.

## What's included

| Piece | What it does |
| --- | --- |
| `django_amatopay.services` | Create checkout/QR sessions, verify aliases, quote fees, confirm deliveries, sync any resource — thin wrappers around `amatopay` that also upsert the local mirror. |
| `django_amatopay.models` | `AmatoCheckoutSession`, `AmatoPayment` (+`AmatoPaymentHistory`), `AmatoDelivery` (+`AmatoDeliveryConfirmation`), `AmatoProtectionClaim` (+`AmatoProtectionClaimEvidence`), `AmatoWebhookEvent`. Every row keeps the full raw API response in `.raw` so nothing is ever lost to an unmodeled field. |
| `django_amatopay.admin` | Read-only(ish) `ModelAdmin` for every model above, with search/filter/date-hierarchy and "re-sync from AmatoPay" actions. |
| `django_amatopay.views.AmatoPayWebhookView` | Verifies `AmatoPay-Signature`, de-duplicates by event id, best-effort re-syncs the affected row, and fires the matching Django signal. |
| `django_amatopay.signals` | One `django.dispatch.Signal` per AmatoPay event type (`payment_paid`, `payment_failed`, `delivery_confirmed`, `settlement_completed`, ...). |
| `manage.py sync_amatopay` | Backfill / periodic refresh of every local mirror table. |

## Usage

### Take a payment

```python
from django_amatopay import services

session = services.create_checkout_session(
    order_number="ORDER-1001",
    amount="100000.00",
    currency="BIF",
    payer_alias="+25779000000",
    return_url="https://yoursite.com/orders/1001/",
)
return redirect(session.checkout_url)
```

`session` is a saved `AmatoCheckoutSession` row — `session.payment_reference` links to the
`AmatoPayment` created once the payer completes it (kept in sync by the webhook receiver).

### React to events

```python
# yourapp/signals.py
from django.dispatch import receiver
from django_amatopay.signals import payment_paid, payment_disputed

@receiver(payment_paid)
def on_payment_paid(sender, event, data, **kwargs):
    Order.objects.filter(payment_reference=data["payment_reference"]).update(status="paid")

@receiver(payment_disputed)
def on_payment_disputed(sender, event, data, **kwargs):
    notify_ops_team(data["payment_reference"])
```

Wire it up in your app's `AppConfig.ready()` as usual.

### Manage fulfillment

```python
from django_amatopay import services

services.mark_shipped(delivery.id, tracking_number="DHL-12345")
services.mark_delivered(delivery.id)
services.confirm_delivery("AMP-PAY-...", secure_code="123456")
```

Or do all of the above from `/admin/amatopay/amatodelivery/` directly.

## Design notes

- **AmatoPay is always the source of truth.** Every model is a read-mostly cache; the admin
  disables free-text editing and instead offers "re-sync from AmatoPay" actions. Don't fork state
  locally — change it at AmatoPay and re-sync.
- **`raw` on every model** holds the full, unmodeled API response, so a field this app hasn't
  wrapped yet (or a future one AmatoPay adds) is never silently dropped.
- **Webhooks are idempotent.** Delivery is de-duplicated by event id before any handler runs, so a
  retried delivery (AmatoPay retries anything but a 2xx) is a safe no-op, not a duplicate side effect.

## Development

```bash
uv sync              # installs amatopay from ../amatopay-python (see [tool.uv.sources])
uv run pytest        # run the test suite (pytest-django + responses, sqlite in-memory)
uv run ruff check .  # lint
uv build             # build the sdist + wheel into dist/
```

Once `amatopay` is published to PyPI, remove the `[tool.uv.sources]` override in
`pyproject.toml` so this package depends on the published version instead of the sibling checkout.

## License

MIT
