Wallet System

The wallet system allows users to maintain a balance that can be used to pay for subscriptions, providing an alternative to direct Midtrans payments.

Architecture

User → Wallet (1:1)
Wallet → WalletTransaction (1:N)
Wallet → TopUp (1:N)
User → TopUp (1:N)

How It Works

  1. Create/Get Wallet — Each user gets a wallet (auto-created on first access)

  2. Top-Up — Users add funds via Midtrans payment (bank transfer, GoPay, QRIS, etc.)

  3. Credit — Once top-up payment settles, wallet is credited automatically via webhook

  4. Pay Subscription — Users can pay for subscriptions directly from wallet balance

  5. Recurring Billing — Celery task automatically debits wallet for due subscriptions

Top-Up Flow

1. Create a Top-Up

from subscriptions.services import WalletService

service = WalletService()
topup = service.create_topup(
    user=user,
    amount=100000,
    payment_type="bank_transfer",
    bank="bca",
)

# topup.payment_details contains:
# {"bank": "bca", "va_number": "1234567890"}

2. User Pays via VA/QR

The user transfers money to the virtual account or scans the QR code.

3. Webhook Processes Payment

When Midtrans sends a settlement notification for the TOPUP-xxx order:

  • Top-up status → success

  • Wallet balance is credited

  • WalletTransaction record is created

4. API Endpoint

# Create top-up
POST /api/subscriptions/topups/
{
  "amount": 100000,
  "payment_type": "bank_transfer",
  "bank": "bca"
}

# Check wallet balance
GET /api/subscriptions/wallet/me/

# List transactions
GET /api/subscriptions/wallet/transactions/

Paying Subscription from Wallet

Manual Payment

from subscriptions.services import WalletService

service = WalletService()
txn = service.pay_subscription_from_wallet(subscription)

This:

  1. Checks wallet has sufficient balance

  2. Debits the wallet

  3. Activates/renews the subscription

  4. Creates a Payment record (type: wallet)

  5. Generates an invoice

Create Subscription with Wallet

subscription = service.create_subscription_from_wallet(
    user=user,
    plan=plan,
)

Automatic Billing

The process_wallet_billing Celery task runs hourly and:

  1. Finds active wallet subscriptions due for billing

  2. Debits each user’s wallet

  3. Marks subscriptions as past_due if insufficient balance

Balance Safety

All wallet operations use select_for_update() to prevent race conditions:

# From services.py
wallet = Wallet.objects.select_for_update().get(pk=wallet.pk)

This ensures:

  • Two concurrent top-ups don’t result in incorrect balance

  • A top-up and subscription payment can’t conflict

  • Balance can never go negative (raises ValueError)

Transaction Types

Type

Description

Amount Sign

top_up

Funds added via Midtrans payment

Positive (+)

subscription_payment

Subscription charge from wallet

Negative (-)

refund

Refund credited back to wallet

Positive (+)

adjustment

Manual admin adjustment

Either

Wallet Signals

from subscriptions.signals import topup_completed, wallet_credited, wallet_debited

@receiver(topup_completed)
def on_topup(sender, instance, **kwargs):
    """Top-up payment settled."""
    print(f"Wallet top-up: {instance.amount}")

@receiver(wallet_credited)
def on_credit(sender, instance, **kwargs):
    """Any positive wallet transaction."""
    print(f"Wallet credited: +{instance.amount}")

@receiver(wallet_debited)
def on_debit(sender, instance, **kwargs):
    """Any negative wallet transaction."""
    print(f"Wallet debited: {instance.amount}")