Skip to main content

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

Status Python Django

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):

  1. Create Order: POST /api/v1/billing/orders Supports external_id + provider. Automatically creates a user if missing.
  2. Confirm Payment: POST /api/v1/billing/orders/{order_id}/confirm Triggered by your payment webhook. This grants products via TransactionService.grant_offer(source="purchase").

Exchange Flow (Internal Currency):

  1. Exchange: POST /api/v1/billing/exchange Atomically spends internal currency and grants the target offer. Supports external_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

billable-0.2.0.tar.gz (89.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

billable-0.2.0-py3-none-any.whl (104.9 kB view details)

Uploaded Python 3

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

Hashes for billable-0.2.0.tar.gz
Algorithm Hash digest
SHA256 73ccf84e0ed29451bc8ce396113615ce30b05855f2f69610a9f45e1723680a48
MD5 d43761e0576ec5086b745ac3d742ea52
BLAKE2b-256 61e516b083f03c5baccf8a7053d132e2921529e4008da82391cd746fcd8aff82

See more details on using hashes here.

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

Hashes for billable-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cfdea83b5c706a2a1a3f1d249fd7812db1aadfbcd5ff2969c3e949e668ef222f
MD5 43572e6afc12f3de24756c1579633997
BLAKE2b-256 cb15efd1e38c418929789eb6d8000595553184fd6c083c4350eab818f5ee4882

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page