Universal Billing Engine
Project description
Universal Billable Module
A Detachable Billing Engine for Django & Ninja
billable is an isolated rights management and payments accounting system designed for Django. It abstracts monetization logic (subscriptions, one-time purchases, trials, quotas) from your core application business logic.
The module provides a single API and accounting layer for different orchestrators (n8n, bots, web), so each can use the same billing flows. Designed to work seamlessly with orchestrators like n8n, and fully usable as a standalone Python service layer.
Status
Features
- Transaction-Based Ledger: All balance changes are recorded as immutable transactions (Credit/Debit).
- Offer System: Flexible product bundles with configurable expiration periods.
- FIFO Consumption: Automatic oldest-first quota consumption.
- Fraud Prevention: Abstract identity hashing for trial abuse protection.
- Admin UI: Built-in Django Admin integration with hierarchical usage reports and offer management.
- Signals & Webhooks: Event-driven architecture for integrations (e.g., n8n, Zapier).
- Detachable Architecture: No foreign keys to your business models (uses metadata).
- Idempotency: Built-in protection against double-spending and duplicate payments.
- Customer Merging: Service and API for consolidating user accounts without data loss.
- REST API: Ready-to-use Django Ninja API for frontend or external orchestrators.
- Normalization Policy: Consistent uppercase (CAPS) storage for technical identifiers (SKU, Product Key) with "silent" API normalization.
🤖 Live Demo
See Billable in action! We've built a demonstration bot using n8n and Billable API.
👉 Try the Demo Bot (@billable_demo_bot)
You can find the source workflow and setup instructions in the examples directory: 📂 View n8n Integration Example
Documentation
-
📘 Architecture & Design Deep dive into Business Processes, Order Flow, and the Transaction Engine.
-
📙 API & Models Reference Database schema, Configuration variables, and REST API specification.
-
📋 Changelog: See repository releases or git history.
Installation
Install using pip:
pip install billable
Or install directly from Git (if using a private repository):
pip install git+https://github.com/bubinez/billable.git
Configuration
1. Update settings.py
Add the app to your installed apps and configure the required settings:
INSTALLED_APPS = [
# ...
"billable",
]
# Required: Security token for the REST API
BILLABLE_API_TOKEN = env("BILLABLE_API_TOKEN", default="change-me-in-production")
# Optional: Defaults to "auth.User"
# BILLABLE_USER_MODEL = "custom_users.User"
2. Import Policy
To avoid AppRegistryNotReady errors (especially in tests), always import models and services from their respective submodules. Never import directly from the root billable package.
# Correct
from billable.models import Product, ExternalIdentity
from billable.services import TransactionService
# Incorrect - will cause AppRegistryNotReady
# from billable import Product, TransactionService
3. Configure URLs
Include billable URLs in your main urls.py:
from django.urls import path, include
urlpatterns = [
# Mounts the API at /api/v1/billing/
path("api/v1/billing/", include("billable.urls")),
]
3. Run Migrations
Create the tables prefixed with billable_:
python manage.py migrate billable
To migrate existing user identity fields (e.g. telegram_id, chat_id) into ExternalIdentity, run: python manage.py migrate_identities <field> <provider>. See Reference — Management Commands.
4. Data Normalization (CAPS Policy)
To ensure data integrity and simplify searching, billable enforces a strict normalization policy:
- SKU and Product Key: Always stored in UPPERCASE (CAPS).
- API & Services: The system is case-insensitive on input. Any string passed as a SKU or Product Key is automatically converted to uppercase before database lookup or storage ("Silent Normalization").
- Trial Hashing: Exception. To remain compatible with external standards (like Stripe or Google), user identifiers (emails, IDs) are converted to lowercase before SHA-256 hashing in
TrialHistory.
Quick Start
Python Service Layer (Internal Usage)
You can use the module directly in your views or Celery tasks without calling the HTTP API.
Checking Quota:
from billable.services import TransactionService
async def generate_pdf_report(user):
# Check if user has the technical resource "pdf_export" available
result = await TransactionService.acheck_quota(user.id, "pdf_export")
if not result["can_use"]:
raise PermissionError(f"Upgrade required: {result['message']}")
# Your logic here...
print("Generating PDF...")
# Consume 1 unit of quota (Atomic & Idempotent)
await TransactionService.aconsume_quota(
user_id=user.id,
product_key="pdf_export",
idempotency_key=f"report_{report_id}"
)
Creating a Custom Order:
from billable.services import OrderService
order = await OrderService.acreate_order(
user_id=request.user.id,
items=[
{"sku": "off_premium_pack", "quantity": 1}
],
metadata={"source": "web_checkout"}
)
Implementing Trial/Bonus Logic:
billable provides building blocks for fraud prevention and transaction management, but does NOT include business rules for promotions. Here's how to implement trial logic in your application:
Example 1: Welcome Trial (One-time bonus)
from billable.models import Offer, TrialHistory
from billable.services import TransactionService
from asgiref.sync import sync_to_async
async def claim_welcome_trial(user_id: int, telegram_id: str):
"""Example: Grant welcome trial with fraud prevention."""
# 1. Check eligibility using TrialHistory
identities = {"telegram": telegram_id}
if await TrialHistory.ahas_used_trial(identities=identities):
return {"success": False, "reason": "trial_already_used"}
# 2. Find the trial offer (create an Offer with sku="off_welcome_trial" in your DB)
offer = await Offer.objects.aget(sku="off_welcome_trial")
# 3. Grant the offer using TransactionService
batches = await sync_to_async(TransactionService.grant_offer)(
user_id=user_id,
offer=offer,
source="welcome_bonus",
metadata={"identities": identities}
)
# 4. Mark trial as used
await TrialHistory.objects.acreate(
identity_type="telegram",
identity_hash=TrialHistory.generate_identity_hash(telegram_id),
trial_plan_name="Welcome Trial"
)
return {"success": True, "batches": batches}
Example 2: Referral Bonus (Signal-based)
from django.dispatch import receiver
from billable.models import Referral, Offer
from billable.signals import order_confirmed
from billable.services import TransactionService
@receiver(order_confirmed)
def on_first_purchase(sender, order, **kwargs):
# 1. Check if it's the first purchase using your app's logic
# ...
# 2. Find referral
referral = Referral.objects.filter(referee=order.user).first()
# 3. Atomically claim the bonus (returns True only once)
if referral and referral.claim_bonus():
# 4. Grant the reward
offer = Offer.objects.get(sku="referral_reward")
TransactionService.grant_offer(
user_id=referral.referrer_id,
offer=offer,
source="referral_bonus",
metadata={
"referee_id": referral.referee_id, # Required for webhook payload
"order_id": order.id, # Required for webhook payload
}
)
Important: When creating a referral bonus transaction, always include referee_id and order_id in the metadata parameter. This ensures that webhook payloads (e.g., referral_bonus_granted events) can include referee_external_id by looking up the referee's ExternalIdentity record. Without these fields in metadata, the webhook will only contain referrer_external_id and referee_external_id will be null.
REST API Usage
If you are using n8n or a frontend:
Identify user by external identity (recommended first step):
POST /api/v1/billing/identify
Purchase Flow (Real Money):
- Create Order:
POST /api/v1/billing/ordersSupportsexternal_id+provider. Automatically creates a user if missing. - Confirm Payment:
POST /api/v1/billing/orders/{order_id}/confirmTriggered by your payment webhook. This grants products viaTransactionService.grant_offer(source="purchase").
Exchange Flow (Internal Currency):
- Exchange:
POST /api/v1/billing/exchangeAtomically spends internal currency and grants the target offer. Supportsexternal_id+provider(creates user if missing).
Get Balance:
GET /api/v1/billing/wallet (Headers: Authorization: Bearer <TOKEN>)
Lookup only: returns 404 if the external identity is not registered (no auto-creation).
Catalog:
GET /api/v1/billing/catalog— list all active offers (or filter by?sku=...&sku=...for bulk lookup)GET /api/v1/billing/catalog/{sku}— get a single offer by SKU
For full API details, see the Reference Guide.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file billable-0.2.0.tar.gz.
File metadata
- Download URL: billable-0.2.0.tar.gz
- Upload date:
- Size: 89.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73ccf84e0ed29451bc8ce396113615ce30b05855f2f69610a9f45e1723680a48
|
|
| MD5 |
d43761e0576ec5086b745ac3d742ea52
|
|
| BLAKE2b-256 |
61e516b083f03c5baccf8a7053d132e2921529e4008da82391cd746fcd8aff82
|
File details
Details for the file billable-0.2.0-py3-none-any.whl.
File metadata
- Download URL: billable-0.2.0-py3-none-any.whl
- Upload date:
- Size: 104.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfdea83b5c706a2a1a3f1d249fd7812db1aadfbcd5ff2969c3e949e668ef222f
|
|
| MD5 |
43572e6afc12f3de24756c1579633997
|
|
| BLAKE2b-256 |
cb15efd1e38c418929789eb6d8000595553184fd6c083c4350eab818f5ee4882
|