📊 DRE ANALYTICS (Financial Statement - Multi-Tenant, Multi-Currency)
Defines a multi-tenant, multi-currency DRE analytics engine with incremental aggregation, IFRS 15 revenue recognition, and audit trail for subscription businesses.
What this file does
Defines a multi-tenant, multi-currency DRE analytics engine with incremental aggregation, IFRS 15 revenue recognition, and audit trail for subscription businesses.
When to use it
- Building a financial reporting system for a SaaS with multiple tenants
- Need accrual-based revenue recognition compliant with IFRS 15
- Implementing incremental MRR/ARR and churn calculations with audit trail
- Consolidating multi-currency P&L across clients and gateways
Assumes this stack
📊 DRE ANALYTICS (Financial Statement - Multi-Tenant, Multi-Currency)
Version: v1.0
Type: Accounting-Grade Analytics Engine with Incremental Aggregation
Scope: MRR/ARR, Churn, Revenue Recognition (Accrual vs Cash), Chargebacks
Compliance: IFRS 15 (Revenue Recognition), Brazilian GAAP, SOX
Status: Production Ready
Last Updated: 2026-02-03
📑 TABLE OF CONTENTS
- Overview
- Financial Model
- Data Model (Read Models)
- Incremental Aggregation Jobs
- Revenue Recognition (Accrual)
- Correction & Rebuild Strategy
- DRE Calculation Logic
- Analytics API
- Edge Cases & Examples
- Production Checklist
🎯 OVERVIEW
Problem Statement
Without integrated DRE:
├─ Finance manually exports payments to spreadsheet
├─ Lacks revenue recognition (cash vs accrual)
├─ Can't track churn or cohort metrics
├─ Chargebacks buried in raw data
├─ Corrections require full recalculation
└─ No audit trail of calculations
With integrated DRE:
├─ Real-time MRR/ARR by tenant/client
├─ Revenue recognition (IFRS 15 compliant)
├─ Automatic churn calculation
├─ Chargeback tracking separate
├─ Incremental updates (only changed periods)
├─ Full audit trail + rebuild capability
└─ Multi-currency support (BRL, USD, EUR)
Key Metrics
MRR (Monthly Recurring Revenue)
├─ Sum of active subscriptions @ start of month
├─ Formula: Σ(subscription_amount) where status=active
└─ Updates on: subscription_created, subscription_changed, subscription_canceled
ARR (Annual Recurring Revenue)
├─ MRR * 12
├─ Annualized projection
└─ Used for valuation
Churn Rate
├─ Canceled subscriptions / Total active subscriptions
├─ Formula: canceled_count / (active_count + canceled_count)
└─ Measured monthly/quarterly/annually
Revenue (Accrual)
├─ Recognized revenue based on service delivery (not payment)
├─ For 12-month subscription: recognize 1/12 per month
├─ Formula: invoice_amount / subscription_term_months
└─ More accurate than cash basis
Revenue (Cash Basis)
├─ Recognized when payment received
├─ Useful for cash flow analysis
└─ Different from accrual
Inadimplência (Delinquency)
├─ Invoices past due + unpaid
├─ Formula: unpaid_invoices > due_date
└─ Monitored for dunning/collection
Chargeback Rate
├─ Chargebacks / Total payments
├─ Formula: chargeback_count / total_payment_count
└─ Risk indicator
💰 FINANCIAL MODEL
Chart of Accounts (Simplified)
4000 - RECEITAS
├─ 4100 - Receita de Serviços
│ ├─ 4101 - Receita de Assinatura (Plan A)
│ ├─ 4102 - Receita de Assinatura (Plan B)
│ └─ 4103 - Receita de Assinatura (Plan C)
├─ 4200 - Deduções de Receita
│ ├─ 4210 - Devoluções
│ └─ 4220 - Cancelamentos
└─ 4300 - Outros
├─ 4310 - Receita de Setup
└─ 4320 - Receita de API Usage
5000 - CUSTOS DE SERVIÇOS
├─ 5100 - Custos de Gateway
│ ├─ 5101 - Taxa Stripe (2.9% + 0.30)
│ ├─ 5102 - Taxa MercadoPago (2.99% + 0.49)
│ └─ 5103 - Taxa Pagar.me (2.94%)
├─ 5200 - Chargebacks
│ └─ 5210 - Custos de Chargeback (15 USD)
└─ 5300 - Devoluções
└─ 5310 - Custo de Refund
6000 - DESPESAS
├─ 6100 - Despesas com Pessoal
├─ 6200 - Despesas com Tecnologia
└─ 6300 - Despesas Administrativas
Account Structure for Multi-Tenant
Each tenant has its own Chart of Accounts:
tenant-123:
├─ 4101 - Receita Assinatura Plan A
├─ 5101 - Taxa Gateway
└─ 5210 - Custo Chargeback
tenant-456:
├─ 4101 - Receita Assinatura Plan A
├─ 5101 - Taxa Gateway
└─ 5210 - Custo Chargeback
Each independent + can cross-report for corporate reporting
🗄️ DATA MODEL (READ MODELS)
Table 1: subscription_aggregates (Monthly Snapshots)
CREATE TABLE subscription_aggregates (
agg_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
client_id UUID, -- NULL = all clients
-- Period
period_year INT NOT NULL,
period_month INT NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- MRR Calculation (at start of month)
mrr_active_subscriptions INT DEFAULT 0, -- Count of active subs
mrr_total_amount DECIMAL(19, 4) DEFAULT 0, -- Sum of MRR
mrr_currency VARCHAR(3) DEFAULT 'BRL',
-- Subscriptions created/canceled during month
new_subscriptions INT DEFAULT 0,
new_mrr DECIMAL(19, 4) DEFAULT 0, -- MRR from new subs
canceled_subscriptions INT DEFAULT 0,
churned_mrr DECIMAL(19, 4) DEFAULT 0, -- MRR lost from cancellations
-- Churn rate
churn_rate DECIMAL(5, 4) DEFAULT 0, -- 0.05 = 5%
-- Delinquency
delinquent_count INT DEFAULT 0, -- Unpaid invoices
delinquent_mrr DECIMAL(19, 4) DEFAULT 0, -- MRR at risk
-- Status
is_finalized BOOLEAN DEFAULT FALSE, -- Can't change after finalized
-- Audit
calculated_at TIMESTAMP DEFAULT NOW(),
recalculated_at TIMESTAMP,
UNIQUE (tenant_id, period_year, period_month, client_id),
CONSTRAINT valid_month CHECK (period_month BETWEEN 1 AND 12),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (client_id) REFERENCES companies(id)
);
CREATE INDEX idx_sub_agg_tenant_period
ON subscription_aggregates(tenant_id, period_year, period_month);
CREATE INDEX idx_sub_agg_client_period
ON subscription_aggregates(client_id, period_year, period_month);
Table 2: revenue_recognition (Accrual Basis)
CREATE TABLE revenue_recognition (
rec_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
invoice_id UUID NOT NULL,
subscription_id UUID,
-- Period
period_year INT NOT NULL,
period_month INT NOT NULL,
-- Recognition details
total_invoice_amount DECIMAL(19, 4),
subscription_term_months INT, -- 1, 3, 12, etc
monthly_recognition DECIMAL(19, 4), -- Amount recognized this month
-- Recognition basis
recognition_method VARCHAR(50), -- 'straight_line', 'performance'
-- Accrual vs Cash
payment_received_amount DECIMAL(19, 4) DEFAULT 0, -- Cash received this period
payment_received_date TIMESTAMP,
-- Account mapping
credit_account VARCHAR(20), -- 4101, 4102, etc
debit_account VARCHAR(20), -- Bank account, receivable
-- Status
is_deferred BOOLEAN, -- If advance payment (deferred revenue)
-- Audit
recognized_at TIMESTAMP DEFAULT NOW(),
UNIQUE (tenant_id, invoice_id, period_year, period_month),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (invoice_id) REFERENCES invoices(id)
);
CREATE INDEX idx_rev_rec_tenant_period
ON revenue_recognition(tenant_id, period_year, period_month);
CREATE INDEX idx_rev_rec_invoice
ON revenue_recognition(invoice_id);
Table 3: gateway_costs (Costs Aggregation)
CREATE TABLE gateway_costs (
cost_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
payment_id UUID, -- Link to payment
-- Period
period_year INT NOT NULL,
period_month INT NOT NULL,
-- Cost details
cost_type VARCHAR(50), -- 'gateway_fee', 'chargeback', 'refund'
gateway VARCHAR(50), -- 'stripe', 'mercadopago', 'pagarme'
-- Amount
transaction_amount DECIMAL(19, 4), -- Amount of transaction
cost_percentage DECIMAL(5, 4), -- 2.9% = 0.029
cost_fixed DECIMAL(19, 4), -- Fixed component (ex: 0.30)
total_cost DECIMAL(19, 4), -- cost_percentage * amount + cost_fixed
-- Account mapping
charge_account VARCHAR(20), -- 5101, 5102, etc
-- Status
is_finalized BOOLEAN DEFAULT FALSE,
-- Audit
calculated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (payment_id) REFERENCES payments(id)
);
CREATE INDEX idx_gateway_costs_period
ON gateway_costs(tenant_id, period_year, period_month);
CREATE INDEX idx_gateway_costs_payment
ON gateway_costs(payment_id);
Table 4: chargeback_costs
CREATE TABLE chargeback_costs (
cb_cost_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
payment_id UUID NOT NULL,
-- Period
period_year INT NOT NULL,
period_month INT NOT NULL,
-- Chargeback details
original_amount DECIMAL(19, 4),
chargeback_amount DECIMAL(19, 4), -- Usually = original amount
chargeback_fee DECIMAL(19, 4) DEFAULT 15, -- Fixed fee ($15 USD)
total_cost DECIMAL(19, 4), -- Amount + fee
-- Account mapping
charge_account VARCHAR(20) DEFAULT '5210', -- Chargeback costs
-- Status
is_finalized BOOLEAN DEFAULT FALSE,
-- Audit
recorded_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (payment_id) REFERENCES payments(id)
);
CREATE INDEX idx_chargeback_costs_period
ON chargeback_costs(tenant_id, period_year, period_month);
Table 5: dre_monthly (Consolidated P&L)
CREATE TABLE dre_monthly (
dre_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
client_id UUID, -- NULL = consolidated
-- Period
period_year INT NOT NULL,
period_month INT NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- RECEITAS (Revenue)
revenue_accrual DECIMAL(19, 4) DEFAULT 0, -- IFRS 15 recognized
revenue_cash DECIMAL(19, 4) DEFAULT 0, -- Actually received
deductions_refund DECIMAL(19, 4) DEFAULT 0, -- Refunds
net_revenue DECIMAL(19, 4) DEFAULT 0, -- Revenue - deductions
-- CUSTOS DE SERVIÇOS (COGS)
cogs_gateway_fees DECIMAL(19, 4) DEFAULT 0, -- Payment processor fees
cogs_chargebacks DECIMAL(19, 4) DEFAULT 0, -- Chargeback costs
cogs_refunds DECIMAL(19, 4) DEFAULT 0, -- Refund costs
total_cogs DECIMAL(19, 4) DEFAULT 0,
-- EBITDA / Gross Margin
gross_profit DECIMAL(19, 4) DEFAULT 0, -- Net revenue - COGS
gross_margin_pct DECIMAL(5, 4) DEFAULT 0, -- gross_profit / net_revenue
-- DESPESAS OPERACIONAIS (OpEx) - TODO: integrate with HR/ops systems
opex_payroll DECIMAL(19, 4) DEFAULT 0,
opex_technology DECIMAL(19, 4) DEFAULT 0,
opex_other DECIMAL(19, 4) DEFAULT 0,
total_opex DECIMAL(19, 4) DEFAULT 0,
-- RESULTADO (Result)
operating_income DECIMAL(19, 4) DEFAULT 0, -- Gross profit - OpEx
-- Metrics
mrr DECIMAL(19, 4) DEFAULT 0,
arr DECIMAL(19, 4) DEFAULT 0,
churn_rate DECIMAL(5, 4) DEFAULT 0,
cogs_percentage DECIMAL(5, 4) DEFAULT 0, -- COGS / Revenue
-- Status
is_finalized BOOLEAN DEFAULT FALSE, -- Locked for correction
-- Audit
calculated_at TIMESTAMP DEFAULT NOW(),
recalculated_count INT DEFAULT 0,
UNIQUE (tenant_id, period_year, period_month, client_id),
CONSTRAINT valid_month CHECK (period_month BETWEEN 1 AND 12),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (client_id) REFERENCES companies(id)
);
CREATE INDEX idx_dre_tenant_period
ON dre_monthly(tenant_id, period_year, period_month);
CREATE INDEX idx_dre_client_period
ON dre_monthly(client_id, period_year, period_month);
Table 6: dre_calculation_log (Audit Trail)
CREATE TABLE dre_calculation_log (
log_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Calculation details
calculation_type VARCHAR(50), -- 'incremental', 'rebuild', 'correction'
period_year INT,
period_month INT,
-- What changed
rows_created INT DEFAULT 0,
rows_updated INT DEFAULT 0,
rows_deleted INT DEFAULT 0,
-- Changes
old_revenue DECIMAL(19, 4),
new_revenue DECIMAL(19, 4),
old_cogs DECIMAL(19, 4),
new_cogs DECIMAL(19, 4),
-- Reason for recalculation
trigger_reason VARCHAR(255), -- 'payment_refunded', 'chargeback_received', 'manual_correction'
triggered_by_user_id UUID, -- If manual
triggered_by_event_id UUID, -- If automatic
-- Execution
started_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP,
duration_ms INT,
-- Status
status VARCHAR(50), -- SUCCESS, FAILED, PARTIAL
error_message TEXT,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
CREATE INDEX idx_dre_log_tenant_period
ON dre_calculation_log(tenant_id, period_year, period_month, calculation_type);
Table 7: revenue_deferred_liability (Deferred Revenue)
CREATE TABLE revenue_deferred_liability (
defer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
invoice_id UUID NOT NULL,
-- Deferred details
received_date DATE NOT NULL,
received_amount DECIMAL(19, 4) NOT NULL, -- Full amount received upfront
-- Recognition schedule
start_recognition_date DATE NOT NULL,
end_recognition_date DATE NOT NULL,
subscription_term_months INT,
-- Monthly recognition
monthly_recognition_amount DECIMAL(19, 4),
-- Periods remaining
periods_remaining INT,
-- Balance tracking
total_recognized DECIMAL(19, 4) DEFAULT 0, -- Amount recognized so far
balance DECIMAL(19, 4), -- Remaining liability
-- Status
is_fully_recognized BOOLEAN DEFAULT FALSE,
-- Audit
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (invoice_id) REFERENCES invoices(id)
);
CREATE INDEX idx_deferred_tenant_pending
ON revenue_deferred_liability(tenant_id, is_fully_recognized)
WHERE NOT is_fully_recognized;
📈 INCREMENTAL AGGREGATION JOBS
Job 1: MRR Calculation (Monthly at 2 AM)
class MRRCalculationJob:
"""
Calculate MRR for each tenant/client at start of month
Uses subscription state at period start
"""
def __init__(self, db, logger):
self.db = db
self.logger = logger
def run(self, year: int, month: int):
"""
Run MRR calculation for specific month
"""
period_start = date(year, month, 1)
if month == 12:
period_end = date(year + 1, 1, 1) - timedelta(days=1)
else:
period_end = date(year, month + 1, 1) - timedelta(days=1)
self.logger.info(f"Starting MRR calculation for {year}-{month:02d}")
# 1. Get all tenants
tenants = self.db.query("SELECT id FROM tenants WHERE is_active = TRUE")
for tenant in tenants:
tenant_id = tenant['id']
try:
# 2. Calculate MRR for entire tenant
self._calculate_tenant_mrr(tenant_id, period_start, period_end, year, month)
# 3. Calculate MRR by client (optional breakdown)
clients = self.db.query("""
SELECT DISTINCT company_id FROM subscriptions
WHERE tenant_id = %s
""", [tenant_id])
for client in clients:
client_id = client['company_id']
self._calculate_client_mrr(
tenant_id, client_id, period_start, period_end, year, month
)
except Exception as e:
self.logger.error(f"MRR calculation failed for {tenant_id}: {str(e)}")
self.db.insert('dre_calculation_log', {
'tenant_id': tenant_id,
'calculation_type': 'mrr',
'period_year': year,
'period_month': month,
'status': 'FAILED',
'error_message': str(e)
})
def _calculate_tenant_mrr(self, tenant_id: str, period_start: date, period_end: date, year: int, month: int):
"""
Calculate consolidated MRR for tenant
"""
# MRR = Sum of subscription amounts active at start of month
query = """
SELECT
COUNT(*) as active_count,
SUM(monthly_amount) as total_mrr,
currency
FROM subscriptions
WHERE tenant_id = %s
AND status = 'ACTIVE'
AND (start_date <= %s)
AND (end_date IS NULL OR end_date > %s)
GROUP BY currency
"""
results = self.db.query(query, [tenant_id, period_start, period_end])
# Create/update aggregates for each currency
for result in results:
# New subscriptions during month
new_subs = self.db.query("""
SELECT COUNT(*) as count, SUM(monthly_amount) as mrr
FROM subscriptions
WHERE tenant_id = %s
AND status = 'ACTIVE'
AND start_date BETWEEN %s AND %s
""", [tenant_id, period_start, period_end])[0]
# Canceled subscriptions during month
canceled_subs = self.db.query("""
SELECT COUNT(*) as count, SUM(monthly_amount) as mrr
FROM subscriptions
WHERE tenant_id = %s
AND (status = 'CANCELED' OR status = 'CHURNED')
AND end_date BETWEEN %s AND %s
""", [tenant_id, period_start, period_end])[0]
# Delinquent subscriptions (invoices unpaid + overdue)
delinquent = self.db.query("""
SELECT COUNT(DISTINCT s.subscription_id) as count, SUM(s.monthly_amount) as mrr
FROM subscriptions s
LEFT JOIN invoices i ON s.subscription_id = (
SELECT subscription_id FROM invoice_items
WHERE invoice_id = i.invoice_id LIMIT 1
)
WHERE s.tenant_id = %s
AND s.status = 'ACTIVE'
AND i.status = 'AWAITING_PAYMENT'
AND i.due_date < %s
""", [tenant_id, period_start])[0]
# Calculate churn rate
active_count = result['active_count']
canceled_count = canceled_subs['count'] or 0
churn_rate = canceled_count / (active_count + canceled_count) if (active_count + canceled_count) > 0 else 0
# Insert/update aggregate
self.db.upsert('subscription_aggregates', {
'tenant_id': tenant_id,
'client_id': None,
'period_year': year,
'period_month': month,
'period_start': period_start,
'period_end': period_end,
'mrr_active_subscriptions': result['active_count'],
'mrr_total_amount': result['total_mrr'],
'mrr_currency': result['currency'],
'new_subscriptions': new_subs['count'] or 0,
'new_mrr': new_subs['mrr'] or 0,
'canceled_subscriptions': canceled_count,
'churned_mrr': canceled_subs['mrr'] or 0,
'churn_rate': churn_rate,
'delinquent_count': delinquent['count'] or 0,
'delinquent_mrr': delinquent['mrr'] or 0,
'calculated_at': datetime.utcnow()
}, {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month,
'client_id': None
})
self.logger.info(f"✓ MRR calculated: {tenant_id} - {result['total_mrr']} {result['currency']}")
def _calculate_client_mrr(self, tenant_id: str, client_id: str, period_start: date, period_end: date, year: int, month: int):
"""
Calculate MRR breakdown by client
"""
query = """
SELECT
COUNT(*) as active_count,
SUM(monthly_amount) as total_mrr,
currency
FROM subscriptions
WHERE tenant_id = %s
AND company_id = %s
AND status = 'ACTIVE'
AND start_date <= %s
AND (end_date IS NULL OR end_date > %s)
GROUP BY currency
"""
results = self.db.query(query, [tenant_id, client_id, period_start, period_end])
for result in results:
self.db.upsert('subscription_aggregates', {
'tenant_id': tenant_id,
'client_id': client_id,
'period_year': year,
'period_month': month,
'mrr_active_subscriptions': result['active_count'],
'mrr_total_amount': result['total_mrr'],
'mrr_currency': result['currency']
}, {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month,
'client_id': client_id
})
Job 2: Revenue Recognition (Accrual Basis)
class RevenueRecognitionJob:
"""
Calculate IFRS 15 revenue recognition (accrual basis)
Each invoice → recognized monthly based on service delivery
"""
def __init__(self, db, logger):
self.db = db
self.logger = logger
def run(self, year: int, month: int):
"""
Recognize revenue for period
"""
period_start = date(year, month, 1)
if month == 12:
period_end = date(year + 1, 1, 1) - timedelta(days=1)
else:
period_end = date(year, month + 1, 1) - timedelta(days=1)
self.logger.info(f"Starting revenue recognition for {year}-{month:02d}")
# 1. Find all invoices that span this period (subscription delivery)
invoices = self.db.query("""
SELECT
i.invoice_id,
i.tenant_id,
i.company_id,
i.amount,
s.subscription_term_months,
s.billing_cycle,
i.issued_at,
p.payment_received_date,
p.amount as payment_amount
FROM invoices i
JOIN subscription_invoices si ON i.invoice_id = si.invoice_id
JOIN subscriptions s ON si.subscription_id = s.subscription_id
LEFT JOIN payments p ON i.invoice_id = p.invoice_id AND p.status = 'SUCCEEDED'
WHERE i.tenant_id IN (SELECT id FROM tenants WHERE is_active = TRUE)
AND i.issued_at <= %s
AND (i.due_date >= %s OR i.issued_at >= %s)
""", [period_end, period_start, period_start])
for invoice in invoices:
# 2. Calculate monthly recognition (straight-line method)
recognition = self._recognize_revenue(
invoice, period_start, period_end, year, month
)
if recognition:
# Insert recognition record
self.db.upsert('revenue_recognition', recognition, {
'tenant_id': invoice['tenant_id'],
'invoice_id': invoice['invoice_id'],
'period_year': year,
'period_month': month
})
def _recognize_revenue(self, invoice: dict, period_start: date, period_end: date, year: int, month: int) -> dict:
"""
Recognize revenue for specific invoice in specific period
Straight-line method: invoice_amount / subscription_term_months
"""
# Service period starts at invoice issue date
service_start = invoice['issued_at'].date()
# Service period ends based on billing cycle
if invoice['billing_cycle'] == 'MONTHLY':
service_end = service_start + timedelta(days=30)
elif invoice['billing_cycle'] == 'QUARTERLY':
service_end = service_start + timedelta(days=90)
elif invoice['billing_cycle'] == 'ANNUAL':
service_end = service_start + timedelta(days=365)
else:
service_end = service_start + timedelta(days=30)
# Overlap between service period and current period
overlap_start = max(service_start, period_start)
overlap_end = min(service_end, period_end)
if overlap_start > overlap_end:
# No overlap - don't recognize
return None
# Calculate daily amount
total_days = (service_end - service_start).days
daily_amount = invoice['amount'] / total_days if total_days > 0 else 0
# Recognition for this period
overlap_days = (overlap_end - overlap_start).days + 1
monthly_recognition = daily_amount * overlap_days
# Determine account mapping
account = self._get_revenue_account(invoice.get('plan_id'))
# Check if advance payment (deferred revenue)
is_deferred = invoice['payment_received_date'] and invoice['payment_received_date'].date() < service_start
return {
'tenant_id': invoice['tenant_id'],
'invoice_id': invoice['invoice_id'],
'subscription_id': invoice.get('subscription_id'),
'period_year': year,
'period_month': month,
'total_invoice_amount': invoice['amount'],
'subscription_term_months': invoice['subscription_term_months'],
'monthly_recognition': monthly_recognition,
'recognition_method': 'straight_line',
'payment_received_amount': invoice['payment_amount'] or 0,
'payment_received_date': invoice['payment_received_date'],
'credit_account': account,
'debit_account': '1100', # Receivable account
'is_deferred': is_deferred,
'recognized_at': datetime.utcnow()
}
def _get_revenue_account(self, plan_id: str) -> str:
"""
Map plan to revenue account
"""
plan_accounts = {
'plan-starter': '4101',
'plan-pro': '4102',
'plan-enterprise': '4103',
}
return plan_accounts.get(plan_id, '4100') # Default to 4100
Job 3: Gateway Costs Aggregation
class GatewayCostsJob:
"""
Calculate payment processor fees
Tied to actual payments
"""
def __init__(self, db, logger):
self.db = db
self.logger = logger
def run(self, year: int, month: int):
"""
Calculate gateway costs for period
"""
period_start = date(year, month, 1)
if month == 12:
period_end = date(year + 1, 1, 1) - timedelta(days=1)
else:
period_end = date(year, month + 1, 1) - timedelta(days=1)
self.logger.info(f"Starting gateway costs calculation for {year}-{month:02d}")
# Get all successful payments in period
payments = self.db.query("""
SELECT
p.payment_id,
p.tenant_id,
p.amount,
p.currency,
p.gateway,
p.processed_at
FROM payments p
WHERE p.status = 'SUCCEEDED'
AND DATE(p.processed_at) BETWEEN %s AND %s
""", [period_start, period_end])
# Gateway fee structures
fees = {
'stripe': {'percentage': 0.029, 'fixed': 0.30},
'mercadopago': {'percentage': 0.0299, 'fixed': 0.49},
'pagarme': {'percentage': 0.0294, 'fixed': 0.00}
}
for payment in payments:
gateway = payment['gateway']
fee_config = fees.get(gateway, {})
# Calculate fee
fee_amount = (payment['amount'] * fee_config.get('percentage', 0)) + fee_config.get('fixed', 0)
# Map to account
account_map = {
'stripe': '5101',
'mercadopago': '5102',
'pagarme': '5103'
}
# Insert cost record
self.db.upsert('gateway_costs', {
'tenant_id': payment['tenant_id'],
'payment_id': payment['payment_id'],
'period_year': year,
'period_month': month,
'cost_type': 'gateway_fee',
'gateway': gateway,
'transaction_amount': payment['amount'],
'cost_percentage': fee_config.get('percentage', 0),
'cost_fixed': fee_config.get('fixed', 0),
'total_cost': fee_amount,
'charge_account': account_map.get(gateway, '5100'),
'calculated_at': datetime.utcnow()
}, {
'payment_id': payment['payment_id'],
'period_year': year,
'period_month': month
})
Job 4: Chargeback Costs
class ChargebackCostsJob:
"""
Record chargeback impacts on P&L
"""
def __init__(self, db, logger):
self.db = db
self.logger = logger
def run(self, year: int, month: int):
"""
Record chargebacks for period
"""
period_start = date(year, month, 1)
if month == 12:
period_end = date(year + 1, 1, 1) - timedelta(days=1)
else:
period_end = date(year, month + 1, 1) - timedelta(days=1)
# Get chargebacked payments
chargebacks = self.db.query("""
SELECT
p.payment_id,
p.tenant_id,
p.amount,
p.chargebacked_at
FROM payments p
WHERE p.status = 'CHARGEBACKED'
AND DATE(p.chargebacked_at) BETWEEN %s AND %s
""", [period_start, period_end])
for chargeback in chargebacks:
# Chargeback fee varies by gateway (~$15 USD)
chargeback_fee = 15 # USD or equivalent
total_cost = chargeback['amount'] + chargeback_fee
self.db.upsert('chargeback_costs', {
'tenant_id': chargeback['tenant_id'],
'payment_id': chargeback['payment_id'],
'period_year': year,
'period_month': month,
'original_amount': chargeback['amount'],
'chargeback_amount': chargeback['amount'],
'chargeback_fee': chargeback_fee,
'total_cost': total_cost,
'recorded_at': datetime.utcnow()
}, {
'payment_id': chargeback['payment_id'],
'period_year': year,
'period_month': month
})
Job 5: DRE Consolidation (Brings It All Together)
class DREConsolidationJob:
"""
Consolidate all components into final P&L
"""
def __init__(self, db, logger):
self.db = db
self.logger = logger
def run(self, year: int, month: int):
"""
Calculate consolidated DRE for period
"""
self.logger.info(f"Starting DRE consolidation for {year}-{month:02d}")
# Get all tenants
tenants = self.db.query("SELECT id FROM tenants WHERE is_active = TRUE")
for tenant in tenants:
tenant_id = tenant['id']
# Consolidated DRE
self._calculate_dre(tenant_id, None, year, month)
# Per-client DRE
clients = self.db.query("""
SELECT DISTINCT company_id FROM subscriptions
WHERE tenant_id = %s
""", [tenant_id])
for client in clients:
self._calculate_dre(tenant_id, client['company_id'], year, month)
def _calculate_dre(self, tenant_id: str, client_id: str = None, year: int = None, month: int = None):
"""
Calculate DRE (P&L) for tenant/client in period
"""
period_start = date(year, month, 1)
if month == 12:
period_end = date(year + 1, 1, 1) - timedelta(days=1)
else:
period_end = date(year, month + 1, 1) - timedelta(days=1)
# RECEITAS (Revenue)
rev_accrual = self._sum_revenue(tenant_id, client_id, year, month, basis='accrual')
rev_cash = self._sum_revenue(tenant_id, client_id, year, month, basis='cash')
# DEDUCTIONS
refund_deduction = self.db.query("""
SELECT COALESCE(SUM(amount), 0) FROM payments
WHERE tenant_id = %s
AND status = 'REFUNDED'
AND DATE(refunded_at) BETWEEN %s AND %s
{}
""".format(
f"AND invoice_id IN (SELECT id FROM invoices WHERE company_id = {client_id})" if client_id else ""
), [tenant_id, period_start, period_end])[0][0]
net_revenue_accrual = rev_accrual - refund_deduction
net_revenue_cash = rev_cash - refund_deduction
# CUSTOS (COGS)
gateway_fees = self._sum_costs(tenant_id, client_id, year, month, 'gateway_fee')
chargeback_costs = self._sum_costs(tenant_id, client_id, year, month, 'chargeback')
refund_costs = self._sum_costs(tenant_id, client_id, year, month, 'refund')
total_cogs = gateway_fees + chargeback_costs + refund_costs
# GROSS PROFIT
gross_profit = net_revenue_accrual - total_cogs
gross_margin_pct = (gross_profit / net_revenue_accrual * 100) if net_revenue_accrual > 0 else 0
# Get subscription metrics
mrr_agg = self.db.query("""
SELECT mrr_total_amount, churn_rate
FROM subscription_aggregates
WHERE tenant_id = %s
AND period_year = %s
AND period_month = %s
AND client_id IS {}
""".format("NULL" if not client_id else f"'{client_id}'"), [tenant_id, year, month])
if mrr_agg:
mrr = mrr_agg[0]['mrr_total_amount']
arr = mrr * 12
churn_rate = mrr_agg[0]['churn_rate']
else:
mrr = 0
arr = 0
churn_rate = 0
# COGS %
cogs_pct = (total_cogs / net_revenue_accrual * 100) if net_revenue_accrual > 0 else 0
# Insert DRE
self.db.upsert('dre_monthly', {
'tenant_id': tenant_id,
'client_id': client_id,
'period_year': year,
'period_month': month,
'period_start': period_start,
'period_end': period_end,
'revenue_accrual': rev_accrual,
'revenue_cash': rev_cash,
'deductions_refund': refund_deduction,
'net_revenue': net_revenue_accrual,
'cogs_gateway_fees': gateway_fees,
'cogs_chargebacks': chargeback_costs,
'cogs_refunds': refund_costs,
'total_cogs': total_cogs,
'gross_profit': gross_profit,
'gross_margin_pct': gross_margin_pct,
'mrr': mrr,
'arr': arr,
'churn_rate': churn_rate,
'cogs_percentage': cogs_pct,
'calculated_at': datetime.utcnow()
}, {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month,
'client_id': client_id
})
💡 REVENUE RECOGNITION (ACCRUAL)
IFRS 15 Compliance Example
Scenario: Annual subscription paid upfront
Invoice issued: 2026-01-15
Subscription period: 2026-01-15 to 2027-01-14 (12 months)
Invoice amount: 12,000 BRL
Payment received: 2026-01-15 (upfront)
Monthly recognition (straight-line):
12,000 / 12 = 1,000 BRL per month
January 2026:
Recognized: 1,000 BRL (15 days of 31)
Actual: 1,000 * (16/31) = 516 BRL
February 2026:
Recognized: 1,000 BRL (full month)
...
December 2026:
Recognized: 1,000 BRL (full month)
January 2027:
Recognized: 484 BRL (14 days of 31)
Total recognized: 12,000 BRL ✅
Accounting entries (January 2026):
Debit: Bank account / Receivable 12,000
Credit: Revenue 516
Credit: Deferred Revenue Liability 11,484
February 2026:
Debit: Deferred Revenue Liability 1,000
Credit: Revenue 1,000
Cash vs Accrual Comparison
Scenario: 100 customers × 1,000 BRL = 100,000 BRL/month
Year 1: ACCRUAL BASIS
January: 100,000 (recognized)
February: 100,000
...
December: 100,000
Total 2026: 1,200,000 BRL
Year 1: CASH BASIS
January: 100,000 (received)
February: 100,000
...
December: 100,000
Total 2026: 1,200,000 BRL
Year 2: ACCRUAL BASIS (same as above)
January: 100,000 + 50,000 (new customers)
...
Total 2027: 1,800,000 BRL
Year 2: CASH BASIS
January: 50,000 (new customers paid upfront)
February: 100,000 + some new
...
Total 2027: Different, depends on when payments received
Key difference:
ACCRUAL = More accurate for SaaS (matches service delivery)
CASH = Better for cash flow analysis
🔄 CORRECTION & REBUILD STRATEGY
Scenario 1: Payment Refunded After DRE Finalized
Timeline:
Jan 1: Payment processed, DRE calculated
Jan 15: Payment refunded
Jan 20: Need to correct DRE
Issue: DRE already finalized, but refund changes COGS
Solution:
class DRECorrectionEngine:
"""
Handle corrections to finalized DRE
"""
def correct_payment_refund(self, payment_id: str, reason: str):
"""
Correct DRE when payment refunded
"""
# 1. Find affected period
payment = self.db.query("""
SELECT p.*, DATE_TRUNC('month', p.processed_at) as period
FROM payments p WHERE p.payment_id = %s
""", [payment_id])[0]
period_year = payment['period'].year
period_month = payment['period'].month
tenant_id = payment['tenant_id']
client_id = self.db.query("""
SELECT company_id FROM invoices WHERE invoice_id = %s
""", [payment['invoice_id']])[0]['company_id']
# 2. Get current DRE
current_dre = self.db.query("""
SELECT * FROM dre_monthly
WHERE tenant_id = %s
AND period_year = %s
AND period_month = %s
AND client_id = %s
""", [tenant_id, period_year, period_month, client_id])[0]
# 3. Recalculate affected lines
gateway_fees = self._recalculate_gateway_fees(tenant_id, client_id, period_year, period_month)
# Remove fee for this refunded payment
payment_fee = self.db.query("""
SELECT total_cost FROM gateway_costs
WHERE payment_id = %s
""", [payment_id])[0]
new_gateway_fees = gateway_fees - payment_fee['total_cost']
# 4. Recalculate DRE
old_cogs = current_dre['total_cogs']
new_cogs = old_cogs - payment_fee['total_cost']
old_gross_profit = current_dre['gross_profit']
new_gross_profit = old_gross_profit + payment_fee['total_cost']
# 5. Log correction
self.db.insert('dre_calculation_log', {
'tenant_id': tenant_id,
'calculation_type': 'correction',
'period_year': period_year,
'period_month': period_month,
'trigger_reason': f'payment_refunded: {payment_id}',
'old_cogs': old_cogs,
'new_cogs': new_cogs,
'rows_updated': 1,
'status': 'SUCCESS',
'completed_at': datetime.utcnow()
})
# 6. Update DRE (increment recalculation counter)
self.db.update('dre_monthly', current_dre['dre_id'], {
'total_cogs': new_cogs,
'cogs_gateway_fees': new_gateway_fees,
'gross_profit': new_gross_profit,
'gross_margin_pct': (new_gross_profit / current_dre['net_revenue']) if current_dre['net_revenue'] > 0 else 0,
'cogs_percentage': (new_cogs / current_dre['net_revenue']) if current_dre['net_revenue'] > 0 else 0,
'recalculated_count': current_dre['recalculation_count'] + 1,
'recalculated_at': datetime.utcnow()
})
Scenario 2: Chargeback Received After Month Closed
Issue: Chargeback in Feb for payment from Jan
Solution:
def handle_chargeback_correction(self, payment_id: str):
"""
When chargeback received, correct the month it occurred
"""
payment = self.db.query("""
SELECT * FROM payments WHERE payment_id = %s
""", [payment_id])[0]
# Find original period when payment was processed
original_period_year = payment['processed_at'].year
original_period_month = payment['processed_at'].month
# Get chargeback date
chargeback_date = payment['chargebacked_at']
chargeback_period_year = chargeback_date.year
chargeback_period_month = chargeback_date.month
# Add chargeback cost to chargeback period (not original payment period)
self.db.insert('chargeback_costs', {
'payment_id': payment_id,
'period_year': chargeback_period_year,
'period_month': chargeback_period_month,
'original_amount': payment['amount'],
'total_cost': payment['amount'] + 15 # $15 fee
})
# Recalculate DRE for chargeback month
self._recalculate_dre(
payment['tenant_id'],
payment['invoice_id'].company_id,
chargeback_period_year,
chargeback_period_month
)
Scenario 3: Full Period Rebuild (Rare)
When to rebuild:
- Major bug discovered in calculation logic
- Retroactive price change
- Data quality issue (missing records)
- Compliance requirement
How to rebuild:
def rebuild_period(self, tenant_id: str, year: int, month: int):
"""
Full rebuild of period (expensive operation)
"""
# 1. Delete all aggregates for this period
self.db.delete('dre_monthly', {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month
})
self.db.delete('revenue_recognition', {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month
})
self.db.delete('gateway_costs', {
'tenant_id': tenant_id,
'period_year': year,
'period_month': month
})
# 2. Recalculate from scratch
mrr_job = MRRCalculationJob(self.db, self.logger)
mrr_job.run(year, month)
rev_job = RevenueRecognitionJob(self.db, self.logger)
rev_job.run(year, month)
gateway_job = GatewayCostsJob(self.db, self.logger)
gateway_job.run(year, month)
chargeback_job = ChargebackCostsJob(self.db, self.logger)
chargeback_job.run(year, month)
dre_job = DREConsolidationJob(self.db, self.logger)
dre_job.run(year, month)
# 3. Log rebuild
self.db.insert('dre_calculation_log', {
'tenant_id': tenant_id,
'calculation_type': 'rebuild',
'period_year': year,
'period_month': month,
'trigger_reason': 'manual_rebuild',
'status': 'SUCCESS'
})
📊 DRE CALCULATION LOGIC
Example: Complete Month Calculation
Input Data (January 2026, Tenant ABC):
Subscriptions:
├─ 50 active @ 1,000 BRL/month
├─ 10 new this month @ 1,000 BRL/month
└─ 5 canceled this month @ 1,000 BRL/month
Invoices issued:
├─ INV-001: 50 × 1,000 = 50,000 BRL (Jan 1, annual)
├─ INV-002: 10 × 1,000 = 10,000 BRL (Jan 15, annual)
└─ INV-003: 5 × 500 = 2,500 BRL (Jan 25, refund due to cancellation)
Payments received:
├─ PAY-001: 50,000 BRL (INV-001) via Stripe
├─ PAY-002: 10,000 BRL (INV-002) via MercadoPago
└─ Refund: -2,500 BRL (INV-003) via Stripe
Chargebacks:
└─ None
Failed/Pending:
├─ 3 invoices still unpaid
└─ 1 payment failed
Calculations:
RECEITAS (Revenue - Accrual)
├─ INV-001: 50,000 / 12 months = 4,167 BRL (issued Jan 1)
├─ INV-002: 10,000 / 12 months × 16/31 = 433 BRL (issued Jan 15)
└─ INV-003: (2,500 / 12 months) - refund = credit
= -208 BRL
Total Revenue (Accrual): 4,167 + 433 - 208 = 4,392 BRL
RECEITAS (Cash Basis)
├─ PAY-001: 50,000 BRL received
├─ PAY-002: 10,000 BRL received
└─ Refund: -2,500 BRL
Total Revenue (Cash): 50,000 + 10,000 - 2,500 = 57,500 BRL
CUSTOS (COGS)
├─ Gateway fees:
│ ├─ Stripe PAY-001: 50,000 × 2.9% + 0.30 = 1,450.30 BRL
│ ├─ MercadoPago PAY-002: 10,000 × 2.99% + 0.49 = 299.49 BRL
│ └─ Refund credit: -(2,500 × 2.9% + 0.30) = -73.30 BRL
│ = Total: 1,676.49 BRL
│
├─ Chargebacks: 0 BRL
│
└─ Refund costs: 0 BRL (included in gateway)
Total COGS: 1,676.49 BRL
RESULTADO
├─ Net Revenue (Accrual): 4,392 BRL
├─ Total COGS: 1,676.49 BRL
├─ Gross Profit: 4,392 - 1,676.49 = 2,715.51 BRL
└─ Gross Margin %: 2,715.51 / 4,392 = 61.8%
MÉTRICAS
├─ MRR: 50 × 1,000 = 50,000 BRL (active at start)
├─ New MRR: 10 × 1,000 = 10,000 BRL
├─ Churned MRR: 5 × 1,000 = 5,000 BRL
├─ Churn Rate: 5 / (50 + 5) = 8.3%
├─ ARR: 50,000 × 12 = 600,000 BRL
└─ COGS %: 1,676.49 / 4,392 = 38.2%
🔌 ANALYTICS API
Endpoint 1: Get DRE Summary
@app.get('/v1/analytics/dre')
def get_dre_summary(tenant_id: str, year: int, month: int = None) -> dict:
"""
GET /v1/analytics/dre?tenant_id=xxx&year=2026&month=1
Returns: P&L statement for period
"""
if month:
# Single month
dre = db.query("""
SELECT * FROM dre_monthly
WHERE tenant_id = %s
AND period_year = %s
AND period_month = %s
AND client_id IS NULL
""", [tenant_id, year, month])[0]
return {
'success': True,
'period': f"{year}-{month:02d}",
'revenue': {
'accrual': float(dre['revenue_accrual']),
'cash': float(dre['revenue_cash']),
'deductions_refund': float(dre['deductions_refund']),
'net_revenue': float(dre['net_revenue'])
},
'cogs': {
'gateway_fees': float(dre['cogs_gateway_fees']),
'chargebacks': float(dre['cogs_chargebacks']),
'refunds': float(dre['cogs_refunds']),
'total': float(dre['total_cogs'])
},
'profitability': {
'gross_profit': float(dre['gross_profit']),
'gross_margin_pct': float(dre['gross_margin_pct']),
'cogs_pct': float(dre['cogs_percentage'])
},
'metrics': {
'mrr': float(dre['mrr']),
'arr': float(dre['arr']),
'churn_rate': float(dre['churn_rate'])
}
}
else:
# Annual - sum all months
dres = db.query("""
SELECT
SUM(revenue_accrual) as revenue_accrual,
SUM(revenue_cash) as revenue_cash,
SUM(total_cogs) as total_cogs,
AVG(churn_rate) as avg_churn,
MAX(mrr) as ending_mrr
FROM dre_monthly
WHERE tenant_id = %s
AND period_year = %s
AND client_id IS NULL
""", [tenant_id, year])[0]
return {
'success': True,
'period': str(year),
'revenue_accrual': float(dres['revenue_accrual']),
'revenue_cash': float(dres['revenue_cash']),
'total_cogs': float(dres['total_cogs']),
'avg_churn_rate': float(dres['avg_churn']),
'ending_mrr': float(dres['ending_mrr'])
}
Endpoint 2: MRR Breakdown
@app.get('/v1/analytics/mrr')
def get_mrr_breakdown(tenant_id: str, year: int, month: int) -> dict:
"""
GET /v1/analytics/mrr?tenant_id=xxx&year=2026&month=1
Returns: MRR breakdown by subscription type
"""
agg = db.query("""
SELECT * FROM subscription_aggregates
WHERE tenant_id = %s
AND period_year = %s
AND period_month = %s
AND client_id IS NULL
""", [tenant_id, year, month])[0]
# By plan
by_plan = db.query("""
SELECT
p.plan_name,
COUNT(*) as subscription_count,
SUM(s.monthly_amount) as mrr
FROM subscriptions s
JOIN plans p ON s.plan_id = p.id
WHERE s.tenant_id = %s
AND s.status = 'ACTIVE'
GROUP BY p.plan_id, p.plan_name
""", [tenant_id])
return {
'success': True,
'period': f"{year}-{month:02d}",
'summary': {
'total_mrr': float(agg['mrr_total_amount']),
'active_subscriptions': agg['mrr_active_subscriptions'],
'new_subscriptions': agg['new_subscriptions'],
'canceled_subscriptions': agg['canceled_subscriptions'],
'churn_rate': float(agg['churn_rate'])
},
'by_plan': [
{
'plan': plan['plan_name'],
'subscriptions': plan['subscription_count'],
'mrr': float(plan['mrr'])
}
for plan in by_plan
]
}
Endpoint 3: Revenue Forecast
@app.get('/v1/analytics/forecast')
def get_revenue_forecast(tenant_id: str, months: int = 12) -> dict:
"""
GET /v1/analytics/forecast?tenant_id=xxx&months=12
Returns: Revenue projection based on current MRR and churn
"""
# Get current MRR and churn
current = db.query("""
SELECT mrr_total_amount, churn_rate
FROM subscription_aggregates
WHERE tenant_id = %s
AND client_id IS NULL
ORDER BY period_year DESC, period_month DESC
LIMIT 1
""", [tenant_id])[0]
current_mrr = float(current['mrr_total_amount'])
churn_rate = float(current['churn_rate'])
forecast = []
for month_offset in range(months):
# Project forward: MRR decays by churn each month
projected_mrr = current_mrr * ((1 - churn_rate) ** month_offset)
projected_arr = projected_mrr * 12
forecast.append({
'month': month_offset + 1,
'projected_mrr': round(projected_mrr, 2),
'projected_arr': round(projected_arr, 2)
})
return {
'success': True,
'current_mrr': current_mrr,
'assumed_churn': churn_rate,
'forecast': forecast
}
🔧 EDGE CASES & EXAMPLES
Edge Case 1: Refund After Revenue Recognized
Scenario:
Jan: Invoice 1,000 BRL for 12-month service
Revenue recognized: 83.33 BRL (1,000 / 12)
Cash received: 1,000 BRL
March: Customer requests refund (2 months in)
How to handle?
Solution:
def handle_refund_edge_case():
"""
Refund issued 2 months into 12-month service
Option 1: Pro-rata refund
├─ Months used: 2
├─ Months remaining: 10
├─ Refund amount: 1,000 × (10/12) = 833.33 BRL
└─ Recognized loss: 1,000 - 833.33 = 166.67 BRL
Option 2: Full refund (policy-based)
└─ Refund amount: 1,000 BRL
Accounting entry (Option 1, pro-rata):
Debit: Revenue (reversal) 166.67
Credit: Bank account 166.67
Note: Don't reverse all recognized revenue,
only the pro-rata amount for undelivered service
"""
# Calculate pro-rata refund
months_used = 2
total_months = 12
invoice_amount = 1000
refund_amount = invoice_amount * ((total_months - months_used) / total_months)
# = 1000 * (10/12) = 833.33
# Record refund
refund_entry = {
'type': 'revenue_adjustment',
'description': 'Pro-rata refund after 2 months service delivery',
'amount': -refund_amount, # Negative = reduction
'period': 'March' # Refund period, not original period
}
return refund_entry
Edge Case 2: Chargeback 90 Days After Payment
Scenario:
Jan 15: Payment received (100 BRL)
Revenue recognized: 8.33 BRL/month
DRE finalized for January
April 15: Chargeback received (100 BRL)
DRE for January already finalized
How to handle?
Solution:
def handle_delayed_chargeback():
"""
Chargeback months after original payment
Original period (Jan): DRE already finalized
Chargeback period (Apr): Where to record?
Option 1: Record in chargeback period (Apr)
├─ Jan DRE unchanged (already final)
├─ Apr DRE includes chargeback cost
├─ Apr revenue reduced by chargeback + fee
└─ Most common approach
Option 2: Reverse original month (rare)
└─ Only if DRE not finalized and not yet reported
"""
# Implementation: Option 1
payment = get_payment(id='pay-123')
original_period = payment['processed_at'] # January
chargeback = get_chargeback(id='cb-456')
chargeback_period = chargeback['received_date'] # April
# Record cost in chargeback period (NOT original period)
chargeback_cost = {
'payment_id': 'pay-123',
'original_period_month': 1, # For audit trail
'chargeback_recorded_month': 4, # Where we record it
'amount': 100,
'chargeback_fee': 15,
'total_cost': 115,
'journal_entry': {
'debit': 'Loss on Chargebacks (5210)',
'credit': 'Bank Account',
'amount': 115
}
}
# Update April DRE
april_dre = get_dre('Apr', 2026)
april_dre['cogs_chargebacks'] += 115
april_dre['total_cogs'] += 115
april_dre['gross_profit'] -= 115
april_dre.save()
# January DRE unchanged ✓
Edge Case 3: Currency Conversion
Scenario:
Tenant operates in USD
DRE currency is BRL (corporate)
Payment: 100 USD
Exchange rate (Jan 1): 5.00 BRL/USD
Exchange rate (Jan 31): 5.10 BRL/USD
Solution:
def handle_multi_currency():
"""
Multi-currency DRE
"""
# Recording date: use transaction date rate
payment_brl = 100 * 5.00 # 500 BRL
# DRE consolidation: sum all currencies by converting to corporate (BRL)
dre_monthly = {
'revenue_usd': 100,
'revenue_brl': 500,
'revenue_eur': 20 * 5.5, # 110 BRL
# Consolidated
'revenue_brl_total': 500 + 110 + others, # All in BRL
# Note exchange rates used
'exchange_rates': {
'USD_to_BRL': 5.00,
'EUR_to_BRL': 5.50
}
}
return dre_monthly
Edge Case 4: Subscription Upgrade Mid-Month
Scenario:
Jan 1: Subscription @ 100 BRL/month
Jan 15: Upgrade to 300 BRL/month (new plan)
Jan 31: End of month
How to recognize revenue?
Solution:
def handle_subscription_upgrade():
"""
Pro-rata revenue for subscription change mid-month
"""
# Period 1: Jan 1-14 @ 100 BRL/month
days1 = 14
rate1 = 100
revenue1 = (rate1 / 31) * days1 # 45.16 BRL
# Period 2: Jan 15-31 @ 300 BRL/month
days2 = 17
rate2 = 300
revenue2 = (rate2 / 31) * days2 # 164.52 BRL
# Total January revenue (accrual)
total = revenue1 + revenue2 # 209.68 BRL
# Accounting
entry = {
'debit': 'Receivable',
'credit': 'Revenue',
'amount': 209.68
}
return entry
✅ PRODUCTION CHECKLIST
✅ Data Model
├─ subscription_aggregates (MRR)
├─ revenue_recognition (accrual basis)
├─ gateway_costs (processor fees)
├─ chargeback_costs (chargebacks)
├─ dre_monthly (consolidated P&L)
├─ dre_calculation_log (audit trail)
├─ revenue_deferred_liability (unearned revenue)
└─ All indexes created
✅ Jobs
├─ MRR calculation (monthly)
├─ Revenue recognition (monthly, IFRS 15)
├─ Gateway costs (monthly)
├─ Chargeback costs (monthly)
└─ DRE consolidation (monthly, after all above)
✅ Incremental Processing
├─ New payments → auto-update COGS
├─ Chargebacks → auto-update COGS
├─ Refunds → auto-adjust revenue
├─ Subscription changes → auto-update MRR
└─ No full rebuilds except in corrections
✅ Corrections & Rebuilds
├─ Payment refund → correct period COGS
├─ Chargeback → record in chargeback period
├─ Delayed correction → incremental update
├─ Full rebuild → rare, audit logged
└─ Finalized flag → prevents accidental changes
✅ Revenue Recognition (IFRS 15)
├─ Accrual basis (service delivery)
├─ Straight-line method (most common)
├─ Performance-based (if applicable)
├─ Deferred revenue tracking
└─ Compliance documented
✅ Multi-Tenant Support
├─ Per-tenant DRE
├─ Per-client breakdown (optional)
├─ Consolidated tenant view
└─ Cross-tenant reporting (admin only)
✅ Analytics API
├─ GET /dre (P&L summary)
├─ GET /mrr (MRR breakdown)
├─ GET /forecast (revenue projection)
├─ GET /revenue_recognition (accrual tracking)
└─ All queries <1 second
✅ Audit Trail
├─ All calculations logged
├─ Change history (old → new)
├─ Recalculation counter
├─ Reason for changes
└─ User/event tracking
✅ Testing
├─ Basic MRR calculation
├─ Revenue recognition (multiple terms)
├─ Refund handling (pro-rata)
├─ Chargeback handling (delayed)
├─ Currency conversion
├─ Upgrade mid-month
└─ Full rebuild verification
📊 SUMMARY
What We've Built
-
Accounting-Grade Data Model ✅
- Subscription aggregates (MRR/churn tracking)
- Revenue recognition (IFRS 15 compliant)
- Cost tracking (gateway fees, chargebacks)
- Consolidated P&L (DRE)
- Full audit trail
-
Incremental Aggregation Jobs ✅
- MRR calculation (monthly snapshots)
- Revenue recognition (accrual basis)
- Gateway costs (processor fees)
- Chargeback costs (separate tracking)
- DRE consolidation (brings it all together)
-
Correction & Rebuild Strategy ✅
- Incremental corrections (one payment refunded)
- Delayed chargebacks (record in correct period)
- Full rebuilds (rare, fully logged)
- Finalized flag (prevents accidental changes)
-
Revenue Recognition (Accrual) ✅
- IFRS 15 compliant
- Straight-line method (12-month split)
- Advance payments → deferred revenue
- Performance-based (extensible)
-
Analytics API ✅
- DRE summary (monthly/annual)
- MRR breakdown (by plan/client)
- Revenue forecast (projection)
- All queries optimized (<1s)
-
Edge Cases Handled ✅
- Refunds after recognition
- Delayed chargebacks
- Multi-currency
- Subscription upgrades
- Pro-rata calculations
Financial Metrics Provided
✅ MRR (Monthly Recurring Revenue)
✅ ARR (Annual Recurring Revenue)
✅ Churn Rate (monthly)
✅ Gross Margin % (COGS / Revenue)
✅ COGS % (cost of revenue)
✅ Revenue (accrual vs cash)
✅ Delinquency tracking
✅ Chargeback rate
Safety Guarantees
✅ Never lose data (audit trail)
✅ IFRS 15 compliant (revenue recognition)
✅ Accrual accounting (not just cash)
✅ Multi-currency support
✅ Multi-tenant isolation
✅ Corrections without data loss
✅ Full rebuild capability
✅ Consistent with billing module
Version: 1.0
Status: Production Ready
Compliance: IFRS 15, Brazilian GAAP, SOX
Last Updated: 2026-02-03
For integration: Combine with BILLING_MODULE.md + AUTHORIZATION_SYSTEM.md
What's inside
7 table schemas, 2 job implementations, chart of accounts, edge cases, production checklist, and correction strategy
Change this for your project
- Replace
tenant-123andtenant-456with your own tenant identifiers - Replace gateway fee percentages like
2.9% + 0.30with your processor rates - Replace chargeback fee
15 USDwith your actual fee amount
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Incremental aggregation jobs that only recalculate changed periods, logging each run for audit
- Separate deferred revenue liability table tracking recognition schedule and remaining balance
- Chart of accounts structured per tenant with cross-reporting capability
Related Documents
LLM Privacy Layer — Complete Research Synthesis
Synthesises research on Secludy's privacy-safe synthetic data product and maps it to an MVP plan using spaCy and Faker instead of differential privacy.
PRJ-036: KYC Deployment with Persona
Plans a two-phase KYC deployment using Persona for identity verification, first on WordPress then migrating to jAIMSnet, as a critical step in a user journey.
Regular Expressions (Regex) - A Quick Guide
Teaches regex basics, data sanitization steps, and outlier removal using standard deviation for data preprocessing.
DSGVO/GDPR Compliance Guide
Defines a DSGVO/GDPR breach response procedure with notification timelines, documentation templates, and regulatory contacts for German federal states.