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¶
Create/Get Wallet — Each user gets a wallet (auto-created on first access)
Top-Up — Users add funds via Midtrans payment (bank transfer, GoPay, QRIS, etc.)
Credit — Once top-up payment settles, wallet is credited automatically via webhook
Pay Subscription — Users can pay for subscriptions directly from wallet balance
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 →
successWallet balance is credited
WalletTransactionrecord 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:
Checks wallet has sufficient balance
Debits the wallet
Activates/renews the subscription
Creates a
Paymentrecord (type:wallet)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:
Finds active wallet subscriptions due for billing
Debits each user’s wallet
Marks subscriptions as
past_dueif 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 |
|---|---|---|
|
Funds added via Midtrans payment |
Positive (+) |
|
Subscription charge from wallet |
Negative (-) |
|
Refund credited back to wallet |
Positive (+) |
|
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}")