Metadata-Version: 2.4
Name: django-subscription-midtrans
Version: 0.1.0
Summary: Django subscription management with Midtrans payment gateway integration
Author-email: Danang Hari Setiawan <dananghariss@gmail.com>
License: MIT License
        
        Copyright (c) 2024 Danang Hari Setiawan
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/danangharissetiawan/django-subscription-midtrans
Project-URL: Documentation, https://django-subscription-midtrans.readthedocs.io
Project-URL: Repository, https://github.com/danangharissetiawan/django-subscription-midtrans
Project-URL: Issues, https://github.com/danangharissetiawan/django-subscription-midtrans/issues
Project-URL: Changelog, https://github.com/danangharissetiawan/django-subscription-midtrans/blob/main/docs/changelog.md
Keywords: django,subscription,midtrans,payment,recurring-billing,saas,indonesia
Classifier: Development Status :: 4 - Beta
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: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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 :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=4.2
Requires-Dist: djangorestframework>=3.14
Requires-Dist: django-filter>=23.0
Requires-Dist: celery>=5.3
Requires-Dist: django-celery-beat>=2.5
Requires-Dist: redis>=5.0
Requires-Dist: requests>=2.31
Requires-Dist: httpx>=0.25
Requires-Dist: python-decouple>=3.8
Requires-Dist: uuid7>=0.1.0
Provides-Extra: admin
Requires-Dist: django-unfold>=0.40.0; extra == "admin"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: furo>=2024.0; extra == "docs"
Requires-Dist: sphinxcontrib-mermaid>=0.9; extra == "docs"
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
Requires-Dist: myst-parser>=2.0; extra == "docs"
Provides-Extra: dev
Requires-Dist: django-subscription-midtrans[admin,docs]; extra == "dev"
Requires-Dist: factory-boy>=3.3; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-django>=4.5; extra == "dev"
Dynamic: license-file

<![CDATA[<div align="center">

# 💳 Django Subscription Midtrans

**A production-ready Django package for subscription billing with Midtrans Core API integration.**

[![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)](https://python.org)
[![Django](https://img.shields.io/badge/Django-4.2%2B-green?logo=django&logoColor=white)](https://djangoproject.com)
[![DRF](https://img.shields.io/badge/DRF-3.14%2B-red?logo=django&logoColor=white)](https://django-rest-framework.org)
[![Celery](https://img.shields.io/badge/Celery-5.3%2B-green?logo=celery&logoColor=white)](https://docs.celeryq.dev)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/django-subscription-midtrans.svg)](https://pypi.org/project/django-subscription-midtrans/)

[Features](#-features) · [Quick Start](#-quick-start) · [Documentation](#-documentation) · [API Reference](#-api-reference) · [Architecture](#-architecture)

</div>

---

## 📋 Overview

**django-subscription-midtrans** is a comprehensive, plug-and-play Django package for subscription management with [Midtrans Core API](https://docs.midtrans.com/) payment gateway integration. It provides a complete subscription billing system including recurring payments, invoicing, wallet top-ups, webhook handling, and admin management.

> **Important**: This package uses the **Midtrans Core API** (direct server-side charge via `/v2/charge`, `/v1/subscriptions`, `/v1/invoices`), **NOT** Midtrans Snap.

---

## ✨ Features

| Category | Details |
|----------|---------|
| **Subscription Plans** | Configurable interval billing (daily/weekly/monthly), trial periods, max billing cycles, feature flags per plan |
| **Payment Methods** | Credit Card, Bank Transfer (BCA/BNI/BRI/Permata/CIMB), GoPay, ShopeePay, QRIS, E-Channel (Mandiri Bill), Wallet |
| **Automatic Billing** | Celery Beat periodic tasks handle recurring charges, payment retries, grace periods, expiration |
| **Invoice System** | Auto-generated invoices with line items, tax/discount support, PDF-ready data |
| **Wallet & Top-Up** | Internal wallet balance system with top-up via any payment method |
| **Webhook Processing** | Secure Midtrans notification handler with SHA-512 signature verification, duplicate detection |
| **Admin Dashboard** | Django Unfold admin with status badges, inline editing, bulk actions, Midtrans sync |
| **Notification System** | Email, webhook, and in-app notification logging with queued delivery |
| **Access Middleware** | Path-based subscription enforcement with grace period support |
| **Django Signals** | 20+ signals for all subscription/payment lifecycle events |
| **REST API** | Full DRF ViewSet API for plans, subscriptions, payments, invoices, wallet, notifications |
| **Testing** | Comprehensive test suite with factories, mocked Midtrans responses |

---

## 🏗 Architecture

```
┌──────────────────────────────────────────────────────────────────┐
│                     Django Unfold Admin UI                        │
├──────────────────────────────────────────────────────────────────┤
│                  REST API (DRF ViewSets + Serializers)            │
├──────────────────────────────────────────────────────────────────┤
│              Service Layer (SubscriptionService, WalletService)   │
├──────────────┬────────────────┬──────────────────────────────────┤
│  Models/ORM  │ MidtransClient │  Celery Tasks (Beat Scheduler)   │
├──────────────┴────────────────┴──────────────────────────────────┤
│           Midtrans Core API (v2/charge, v1/subscriptions)        │
└──────────────────────────────────────────────────────────────────┘
```

For detailed architecture diagrams, see [docs/diagrams/](docs/diagrams/).

---

## 🚀 Quick Start

### Installation

**From PyPI:**

```bash
pip install django-subscription-midtrans
```

**From source (development):**

```bash
git clone https://github.com/rissets/django-subscription-midtrans.git
cd django-subscription-midtrans
pip install -e ".[dev]"
```

### 1. Add to `INSTALLED_APPS`

```python
INSTALLED_APPS = [
    # Django Unfold (must be before django.contrib.admin)
    "unfold",
    "unfold.contrib.filters",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Third-party
    "rest_framework",
    "django_celery_beat",

    # Subscription package
    "subscriptions",
]
```

### 2. Configure Environment Variables

Create a `.env` file in your project root:

```env
# Django
DJANGO_SECRET_KEY=
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=*,localhost,127.0.0.1

# Database (default: SQLite, configure for PostgreSQL in production)
# DB_ENGINE=django.db.backends.postgresql
# DB_NAME=subscription_db
# DB_USER=
# DB_PASSWORD=
# DB_HOST=localhost
# DB_PORT=5432

# Celery / Redis
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0

# Midtrans Credentials (get from https://dashboard.midtrans.com)
MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=
MIDTRANS_MERCHANT_ID=
MIDTRANS_IS_PRODUCTION=False

# Webhook URL (must be publicly accessible)
# For local dev, use ngrok: ngrok http 8000
MIDTRANS_NOTIFICATION_URL=
```

### 3. Add Settings

```python
# settings.py
from decouple import config

# Midtrans
MIDTRANS_SERVER_KEY = config("MIDTRANS_SERVER_KEY", default="")
MIDTRANS_CLIENT_KEY = config("MIDTRANS_CLIENT_KEY", default="")
MIDTRANS_MERCHANT_ID = config("MIDTRANS_MERCHANT_ID", default="")
MIDTRANS_IS_PRODUCTION = config("MIDTRANS_IS_PRODUCTION", default=False, cast=bool)
MIDTRANS_API_BASE_URL = (
    "https://api.midtrans.com" if MIDTRANS_IS_PRODUCTION
    else "https://api.sandbox.midtrans.com"
)
MIDTRANS_NOTIFICATION_URL = config("MIDTRANS_NOTIFICATION_URL", default="")

# Celery
CELERY_BROKER_URL = config("CELERY_BROKER_URL", default="redis://localhost:6379/0")
CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0")
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"

# REST Framework
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.BasicAuthentication",
    ],
}

# Subscription Settings (all optional, these are defaults)
SUBSCRIPTION_SETTINGS = {
    "PROTECTED_PATHS": [],
    "SUBSCRIPTION_REQUIRED_REDIRECT": "/pricing/",
    "GRACE_PERIOD_DAYS": 3,
    "PAYMENT_EXPIRY_MINUTES": 60,
    "AUTO_GENERATE_INVOICE": True,
    "AUTO_RETRY_FAILED_PAYMENTS": True,
    "MAX_RETRY_ATTEMPTS": 3,
    "RETRY_INTERVAL_DAYS": 1,
    "INVOICE_NUMBER_PREFIX": "INV",
    "SEND_EMAIL_NOTIFICATIONS": True,
    "EXPIRY_REMINDER_DAYS": [7, 3, 1],
}
```

### 4. Include URLs

```python
# urls.py
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/subscriptions/", include("subscriptions.urls")),
]
```

### 5. Run Migrations & Setup

```bash
# Apply database migrations
python manage.py migrate

# Create periodic Celery tasks
python manage.py setup_periodic_tasks

# Seed sample subscription plans (optional, for development)
python manage.py seed_plans

# Create admin superuser
python manage.py createsuperuser
```

### 6. Start Services

You need **4 processes** running:

```bash
# Terminal 1: Redis (message broker)
redis-server

# Terminal 2: Django development server
python manage.py runserver

# Terminal 3: Celery worker (processes async tasks)
celery -A config worker -l info

# Terminal 4: Celery beat (schedules periodic tasks)
celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
```

---

## 🔧 Development Setup

### Prerequisites

- Python 3.10+
- Redis server
- Git

### Clone & Install

```bash
git clone https://github.com/rissets/django-subscription-midtrans.git
cd django-subscription-midtrans

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # macOS/Linux
# .venv\Scripts\activate   # Windows

# Install dependencies
pip install -r requirements.txt

# Copy environment file
cp .env.example .env
# Edit .env with your Midtrans sandbox credentials
```

### Midtrans Sandbox Setup

1. Register at [https://dashboard.sandbox.midtrans.com](https://dashboard.sandbox.midtrans.com)
2. Get your **Server Key** and **Client Key** from Settings → Access Keys
3. Add them to your `.env` file
4. For webhooks, expose your local server:
   ```bash
   # Install ngrok: https://ngrok.com
   ngrok http 8000
   # Copy the HTTPS URL and set MIDTRANS_NOTIFICATION_URL in .env
   ```

### Run the Example App

The project includes a full example application demonstrating all features:

```bash
# Apply migrations
python manage.py migrate

# Seed plans and periodic tasks
python manage.py seed_plans --reset
python manage.py setup_periodic_tasks --reset

# Create a superuser for admin access
python manage.py createsuperuser

# Start all services (in separate terminals)
redis-server
python manage.py runserver
celery -A config worker -l info
celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
```

Then visit:
- **Example App**: [http://localhost:8000/example/](http://localhost:8000/example/)
- **Admin Dashboard**: [http://localhost:8000/admin/](http://localhost:8000/admin/)
- **API Root**: [http://localhost:8000/api/subscriptions/](http://localhost:8000/api/subscriptions/)

### Test Credentials (Sandbox)

| Payment Method | Test Details |
|----------------|-------------|
| Credit Card | Card: `4811 1111 1111 1114`, Exp: `01/29`, CVV: `123`, OTP: `112233` |
| BCA VA | Use the VA number from the response, simulate payment in Midtrans dashboard |
| BNI VA | Use the VA number from the response |
| GoPay | Use the QR code or deeplink from the response |
| QRIS | Scan the QR code with any QRIS-compatible app (sandbox mode) |

### Running Tests

```bash
# Run all subscription tests
python manage.py test subscriptions

# Run specific test module
python manage.py test subscriptions.tests.test_models
python manage.py test subscriptions.tests.test_services
python manage.py test subscriptions.tests.test_views
python manage.py test subscriptions.tests.test_client
python manage.py test subscriptions.tests.test_middleware

# Run with verbosity
python manage.py test subscriptions -v 2
```

---

## 📖 Documentation

Full Sphinx documentation is available in the `docs/` directory:

```bash
cd docs
pip install -r requirements.txt
make html
# Open docs/_build/html/index.html
```

---

## 🌐 API Reference

### Plans

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/plans/` | No | List all active plans |
| `GET` | `/api/subscriptions/plans/{slug}/` | No | Get plan details by slug |

### Subscriptions

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/subscriptions/` | Yes | List user's subscriptions |
| `POST` | `/api/subscriptions/subscriptions/` | Yes | Create subscription |
| `GET` | `/api/subscriptions/subscriptions/{id}/` | Yes | Get subscription detail |
| `GET` | `/api/subscriptions/subscriptions/active/` | Yes | Get active subscription |
| `POST` | `/api/subscriptions/subscriptions/{id}/cancel/` | Yes | Cancel subscription |
| `POST` | `/api/subscriptions/subscriptions/{id}/pause/` | Yes | Pause subscription |
| `POST` | `/api/subscriptions/subscriptions/{id}/resume/` | Yes | Resume subscription |
| `POST` | `/api/subscriptions/subscriptions/{id}/change-plan/` | Yes | Change subscription plan |
| `GET` | `/api/subscriptions/subscriptions/{id}/sync/` | Yes | Sync with Midtrans |

### Payments

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/payments/` | Yes | List user's payments |
| `GET` | `/api/subscriptions/payments/{id}/` | Yes | Get payment detail |
| `POST` | `/api/subscriptions/payments/charge/` | Yes | Create charge for subscription |
| `POST` | `/api/subscriptions/payments/{id}/refund/` | Yes | Refund a payment |
| `GET` | `/api/subscriptions/payments/{id}/check-status/` | Yes | Check Midtrans status |

### Invoices

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/invoices/` | Yes | List user's invoices |
| `GET` | `/api/subscriptions/invoices/{id}/` | Yes | Get invoice detail |

### Wallet

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/wallet/me/` | Yes | Get user's wallet |
| `GET` | `/api/subscriptions/wallet/transactions/` | Yes | List wallet transactions |

### Top-Ups

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/topups/` | Yes | List user's top-ups |
| `POST` | `/api/subscriptions/topups/` | Yes | Create a top-up |
| `GET` | `/api/subscriptions/topups/{id}/` | Yes | Get top-up detail |

### Notifications

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `GET` | `/api/subscriptions/notifications/` | Yes | List notifications |
| `GET` | `/api/subscriptions/notifications/unread/` | Yes | Get unread notifications |
| `POST` | `/api/subscriptions/notifications/{id}/mark-read/` | Yes | Mark notification as read |

### Webhook

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `POST` | `/api/subscriptions/webhook/notification/` | No | Midtrans webhook handler |

### API Usage Examples

<details>
<summary><b>Create Subscription (Bank Transfer - BCA)</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "plan_id": "<plan-uuid>",
    "payment_type": "bank_transfer",
    "customer_details": {
      "first_name": "John",
      "last_name": "Doe",
      "email": "john@example.com",
      "phone": "081234567890"
    }
  }'
```

**Response:**
```json
{
  "id": "uuid",
  "plan": { "name": "Pro Monthly", "price": "299000.00" },
  "status": "pending",
  "payment_type": "bank_transfer",
  "payments": [{
    "order_id": "SUB-xxxxx",
    "status": "pending",
    "payment_details": {
      "bank": "bca",
      "va_number": "9898123456789"
    }
  }]
}
```
</details>

<details>
<summary><b>Create Subscription (GoPay)</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "plan_id": "<plan-uuid>",
    "payment_type": "gopay"
  }'
```

**Response includes deeplink/QR for GoPay payment.**
</details>

<details>
<summary><b>Create Subscription (QRIS)</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "plan_id": "<plan-uuid>",
    "payment_type": "qris"
  }'
```

**Response includes QR code URL for scanning.**
</details>

<details>
<summary><b>Create Subscription (Credit Card)</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "plan_id": "<plan-uuid>",
    "payment_type": "credit_card",
    "token": "<card-token-from-midtrans-js>"
  }'
```
</details>

<details>
<summary><b>Create Subscription (Wallet)</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "plan_id": "<plan-uuid>",
    "payment_type": "wallet"
  }'
```
</details>

<details>
<summary><b>Cancel Subscription</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/subscriptions/<uuid>/cancel/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{"reason": "Switching provider", "immediate": false}'
```
</details>

<details>
<summary><b>Top Up Wallet</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/topups/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{
    "amount": 500000,
    "payment_type": "bank_transfer",
    "bank": "bca"
  }'
```
</details>

<details>
<summary><b>Refund Payment</b></summary>

```bash
curl -X POST http://localhost:8000/api/subscriptions/payments/<uuid>/refund/ \
  -H "Content-Type: application/json" \
  -b "sessionid=<your-session-id>" \
  -d '{"amount": 150000, "reason": "Partial refund"}'
```
</details>

---

## ⚙️ Configuration Reference

All settings are configured via `SUBSCRIPTION_SETTINGS` dict in your Django settings:

```python
SUBSCRIPTION_SETTINGS = {
    # Access Control
    "PROTECTED_PATHS": ["/api/premium/", "/pro-features/"],
    "SUBSCRIPTION_REQUIRED_REDIRECT": "/pricing/",
    
    # Grace Period
    "GRACE_PERIOD_DAYS": 3,  # Days after failed payment to keep access
    
    # Payment
    "PAYMENT_EXPIRY_MINUTES": 60,  # Payment token/VA expiry time
    
    # Invoicing
    "AUTO_GENERATE_INVOICE": True,  # Auto-create invoice on charge
    "INVOICE_NUMBER_PREFIX": "INV",  # Invoice number format: INV-2026-0001
    
    # Retry Logic
    "AUTO_RETRY_FAILED_PAYMENTS": True,
    "MAX_RETRY_ATTEMPTS": 3,
    "RETRY_INTERVAL_DAYS": 1,
    
    # Notifications
    "SEND_EMAIL_NOTIFICATIONS": True,
    "EXPIRY_REMINDER_DAYS": [7, 3, 1],  # Days before expiry to send reminders
}
```

---

## 🔄 Subscription Lifecycle

```
  ┌───────────┐
  │  PENDING   │──── payment success ────► ACTIVE ◄──── resume ────┐
  └───────────┘                              │                      │
       │                                     │                      │
  payment expired                    ┌───────┴───────┐         ┌───────┐
       │                             │               │         │ PAUSED │
       ▼                        period ends     user pauses    └───────┘
  ┌───────────┐                      │               │
  │  EXPIRED   │◄── grace over ──┌───────────┐       │
  └───────────┘                  │ PAST_DUE  │       │
                                 └───────────┘       │
                                      │              │
                                 retry success       │
                                      │              │
                                      ▼              │
                                   ACTIVE ──── user cancels ────► CANCELLED
```

### Two Subscription Modes

| Mode | Payment Types | How It Works |
|------|---------------|-------------|
| **Native Midtrans Subscription** | Credit Card, GoPay (with account_id) | Uses `/v1/subscriptions` API — Midtrans handles automatic recurring billing |
| **Manual Charge Subscription** | Bank Transfer, QRIS, ShopeePay, E-Channel, Wallet | Each billing cycle creates a new `/v2/charge` — managed by Celery Beat |

---

## 🕐 Celery Periodic Tasks

| Task | Schedule | Description |
|------|----------|-------------|
| `process_due_subscriptions` | Every 1 hour | Charges subscriptions with `next_billing_date <= now` |
| `process_expired_subscriptions` | Every 6 hours | Cancels subscriptions marked `cancel_at_period_end` |
| `process_past_due_expiration` | Every 12 hours | Expires subscriptions past grace period |
| `retry_failed_payments` | Every 4 hours | Retries failed charges (up to `MAX_RETRY_ATTEMPTS`) |
| `process_wallet_billing` | Every 1 hour | Deducts wallet balance for wallet-based subscriptions |
| `mark_overdue_invoices` | Daily 1:00 AM | Marks unpaid invoices as overdue |
| `send_expiry_reminders` | Daily 9:00 AM | Sends reminders N days before subscription expiry |
| `sync_midtrans_subscriptions` | Every 6 hours | Syncs native Midtrans subscription statuses |
| `send_pending_email_notifications` | Every 5 min | Processes queued email notifications |

---

## 📡 Webhook Setup

### 1. Configure Midtrans Dashboard

1. Log in at [Midtrans Dashboard](https://dashboard.midtrans.com) (or sandbox)
2. Go to **Settings → Configuration**
3. Set **Payment Notification URL**: `https://yourdomain.com/api/subscriptions/webhook/notification/`
4. Enable notifications for all transaction statuses

### 2. Local Development (ngrok)

```bash
ngrok http 8000
# Example output: https://abc123.ngrok-free.app

# Set in .env:
# MIDTRANS_NOTIFICATION_URL=https://abc123.ngrok-free.app/api/subscriptions/webhook/notification/
```

### 3. Signature Verification

All incoming webhooks are verified using SHA-512:

```
SHA512(order_id + status_code + gross_amount + server_key) == signature_key
```

Invalid signatures are rejected and logged. Duplicate notifications are detected and skipped.

---

## 📣 Django Signals

Connect to subscription events in your app:

```python
from django.dispatch import receiver
from subscriptions.signals import (
    subscription_activated,
    payment_success,
    payment_failed,
    subscription_plan_changed,
)

@receiver(subscription_activated)
def on_subscription_activated(sender, subscription, **kwargs):
    # Grant access to premium features
    pass

@receiver(payment_success)
def on_payment_success(sender, payment, **kwargs):
    # Send receipt email
    pass

@receiver(payment_failed)
def on_payment_failed(sender, payment, **kwargs):
    # Notify user of failed payment
    pass

@receiver(subscription_plan_changed)
def on_plan_changed(sender, subscription, old_plan, new_plan, **kwargs):
    # Adjust user features
    pass
```

### All Available Signals

| Signal | Arguments | Triggered When |
|--------|-----------|---------------|
| `subscription_created` | `subscription` | New subscription created |
| `subscription_activated` | `subscription` | Subscription becomes active |
| `subscription_cancelled` | `subscription` | Subscription cancelled |
| `subscription_paused` | `subscription` | Subscription paused |
| `subscription_resumed` | `subscription` | Subscription resumed |
| `subscription_expired` | `subscription` | Subscription expired |
| `subscription_renewed` | `subscription` | Billing cycle renewed |
| `subscription_plan_changed` | `subscription`, `old_plan`, `new_plan` | Plan changed |
| `payment_created` | `payment` | New payment created |
| `payment_success` | `payment` | Payment settled |
| `payment_failed` | `payment` | Payment failed |
| `payment_refunded` | `payment` | Payment refunded |
| `invoice_created` | `invoice` | Invoice generated |
| `invoice_paid` | `invoice` | Invoice paid |
| `invoice_overdue` | `invoice` | Invoice past due date |
| `invoice_voided` | `invoice` | Invoice voided |
| `webhook_received` | `webhook_log` | Midtrans webhook received |
| `webhook_processed` | `webhook_log` | Webhook processed |
| `topup_completed` | `topup` | Wallet top-up completed |
| `wallet_credited` | `transaction` | Wallet balance increased |
| `wallet_debited` | `transaction` | Wallet balance decreased |

---

## 🛡 Middleware

Protect routes based on subscription status:

```python
# settings.py
MIDDLEWARE = [
    # ... other middleware
    "subscriptions.middleware.SubscriptionAccessMiddleware",
]

SUBSCRIPTION_SETTINGS = {
    "PROTECTED_PATHS": ["/api/premium/", "/dashboard/pro/"],
    "SUBSCRIPTION_REQUIRED_REDIRECT": "/pricing/",
}
```

**Behavior:**
- **API requests** without active subscription → `403 JSON` response
- **HTML requests** without active subscription → redirect to pricing page
- **Staff users** always bypass the check
- **Grace period** is respected (users with `PAST_DUE` within grace period still have access)

---

## 🎨 Admin Dashboard

The package uses [Django Unfold](https://github.com/unfoldadmin/django-unfold) for a modern admin interface:

- **Plan Management** — Create/edit plans, activate/deactivate, view subscriber counts
- **Subscription Management** — Status badges, pause/resume/cancel actions, sync from Midtrans
- **Payment Tracking** — View payments with fraud status, check/cancel/expire from admin
- **Invoice Management** — View invoices with line items, mark paid, void
- **Webhook Logs** — Audit trail of all Midtrans notifications
- **Wallet & Top-Up** — View user wallets and transaction history

Access at: `http://localhost:8000/admin/`

---

## 🧩 Extending the Package

### Custom Signal Handlers

```python
# your_app/signals.py
from django.dispatch import receiver
from subscriptions.signals import subscription_activated

@receiver(subscription_activated)
def grant_premium_access(sender, subscription, **kwargs):
    user = subscription.user
    user.profile.is_premium = True
    user.profile.save()
```

### Override Service Methods

```python
from subscriptions.services import SubscriptionService

class CustomSubscriptionService(SubscriptionService):
    def activate_subscription(self, subscription):
        result = super().activate_subscription(subscription)
        send_welcome_package(subscription.user)
        return result
```

### Proxy Models

```python
from subscriptions.models import Subscription

class PremiumSubscription(Subscription):
    class Meta:
        proxy = True

    def has_feature(self, feature_name):
        return self.plan.features.get(feature_name, False)
```

---

## 🚀 Production Deployment

### Environment Variables

```env
DJANGO_SECRET_KEY=<strong-random-key>
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

# PostgreSQL (recommended for production)
DB_ENGINE=django.db.backends.postgresql
DB_NAME=subscription_db
DB_USER=
DB_PASSWORD=
DB_HOST=localhost
DB_PORT=5432

# Redis
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0

# Midtrans Production
MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=
MIDTRANS_MERCHANT_ID=
MIDTRANS_IS_PRODUCTION=True
MIDTRANS_NOTIFICATION_URL=https://yourdomain.com/api/subscriptions/webhook/notification/
```

### Production Checklist

- [ ] Set `DEBUG=False`
- [ ] Use PostgreSQL (not SQLite)
- [ ] Set `MIDTRANS_IS_PRODUCTION=True`
- [ ] Use production Midtrans keys (not sandbox)
- [ ] Configure `MIDTRANS_NOTIFICATION_URL` to your public HTTPS URL
- [ ] Run `python manage.py collectstatic`
- [ ] Run `python manage.py migrate`
- [ ] Run `python manage.py setup_periodic_tasks`
- [ ] Use a process manager (systemd, supervisor) for Celery workers
- [ ] Configure HTTPS (required by Midtrans for production)
- [ ] Set up monitoring for Celery tasks and webhook logs

### Systemd Services

<details>
<summary><b>Celery Worker</b></summary>

```ini
# /etc/systemd/system/celery-worker.service
[Unit]
Description=Celery Worker
After=network.target redis.service

[Service]
Type=simple
User=www-data
WorkingDirectory=/path/to/project
ExecStart=/path/to/venv/bin/celery -A config worker -l info
Restart=always

[Install]
WantedBy=multi-user.target
```
</details>

<details>
<summary><b>Celery Beat</b></summary>

```ini
# /etc/systemd/system/celery-beat.service
[Unit]
Description=Celery Beat Scheduler
After=network.target redis.service

[Service]
Type=simple
User=www-data
WorkingDirectory=/path/to/project
ExecStart=/path/to/venv/bin/celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
Restart=always

[Install]
WantedBy=multi-user.target
```
</details>

### Docker Compose

<details>
<summary><b>docker-compose.yml</b></summary>

```yaml
version: "3.8"

services:
  web:
    build: .
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    env_file: .env
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${DB_NAME:-subscription_db}
      POSTGRES_USER: ${DB_USER:-dbuser}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

  celery-worker:
    build: .
    command: celery -A config worker -l info
    env_file: .env
    depends_on:
      - db
      - redis

  celery-beat:
    build: .
    command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
    env_file: .env
    depends_on:
      - db
      - redis

volumes:
  pgdata:
```
</details>

---

## 📁 Project Structure

```
django-subscription-midtrans/
├── config/                     # Django project configuration
│   ├── __init__.py             # Celery app import
│   ├── celery.py               # Celery configuration
│   ├── settings.py             # Django settings
│   ├── urls.py                 # Root URL routing
│   └── wsgi.py                 # WSGI entry point
├── subscriptions/              # Main reusable package
│   ├── __init__.py             # App config
│   ├── admin.py                # Django Unfold admin
│   ├── apps.py                 # AppConfig
│   ├── client.py               # Midtrans Core API client
│   ├── middleware.py            # Subscription access middleware
│   ├── models.py               # Database models (10 models)
│   ├── serializers.py          # DRF serializers
│   ├── services.py             # Business logic layer
│   ├── signals.py              # Django signals (20+ events)
│   ├── tasks.py                # Celery periodic tasks
│   ├── urls.py                 # API URL routing
│   ├── views.py                # DRF ViewSets
│   ├── management/commands/
│   │   ├── seed_plans.py       # Create sample plans
│   │   └── setup_periodic_tasks.py
│   ├── migrations/
│   └── tests/
│       ├── factories.py
│       ├── test_client.py
│       ├── test_middleware.py
│       ├── test_models.py
│       ├── test_services.py
│       └── test_views.py
├── example/                    # Example Django app (demo)
│   ├── views.py                # Template-based views
│   ├── urls.py                 # Example URL routing
│   └── templatetags/
│       └── subscription_tags.py
├── templates/example/          # HTML templates (Tailwind CSS)
├── docs/                       # Sphinx documentation
├── pyproject.toml              # PyPI package config
├── requirements.txt            # Dependencies
├── .env.example                # Environment template
└── README.md
```

---

## 📄 License

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

---

## 🤝 Contributing

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Commit your changes: `git commit -am 'Add my feature'`
4. Push to the branch: `git push origin feature/my-feature`
5. Open a Pull Request

---

## 📚 Resources

- [Midtrans Core API Documentation](https://docs.midtrans.com/)
- [Midtrans Sandbox Dashboard](https://dashboard.sandbox.midtrans.com)
- [Django REST Framework](https://www.django-rest-framework.org/)
- [Celery Documentation](https://docs.celeryq.dev/)
- [Django Unfold Admin](https://github.com/unfoldadmin/django-unfold)
]]>
