> ## Documentation Index
> Fetch the complete documentation index at: https://cona.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Deferred Revenue Recognition for SaaS Subscriptions

> RFC for implementing proper accrual accounting for prepaid subscriptions with automated revenue recognition schedules compliant with German HGB and IFRS 15

## Executive Summary

As CONA expands to serve SaaS businesses with subscription billing (Paddle, Stripe, Chargebee), we need proper **accrual accounting** for prepaid revenue. This RFC proposes a deferred revenue recognition system that:

**Key Goals:**

* Correctly recognize revenue over the service period (not when payment received)
* Comply with German HGB (§252 Abs. 1 Nr. 5) and IFRS 15 requirements
* Provide clear visibility into deferred revenue liabilities
* Handle cancellations and refunds gracefully
* Automate monthly revenue recognition via Temporal workflows

**Architecture Highlights:**

* 🎯 **Document-based**: Integrations populate `deferred_revenue_start_date`, `deferred_revenue_end_date`, `deferred_revenue_period` on invoices
* ⚙️ **Posting Matrix Interception**: System **intercepts** posting matrix execution for prepaid invoices and redirects revenue → PRAP account
* 📅 **Schedule-based**: One schedule per invoice, generates monthly recognition entries
* 🔄 **Automated**: Monthly Temporal workflow recognizes revenue without manual intervention

**Not Goals:**

* Revenue recognition for usage-based billing (separate feature)
* Multi-element arrangements (split between services)
* Contract modifications mid-period (v2 feature)

***

## How It Works: End-to-End Flow

```
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 1: Integration Import                                          │
│ (Paddle/Stripe/Chargebee imports annual subscription)              │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                 ┌────────────────────────┐
                 │ Create Invoice Doc     │
                 │ amount: €1,200         │
                 │ date: 2024-01-01       │
                 │                        │
                 │ ✅ Deferred fields:    │
                 │ start: 2024-01-01     │
                 │ end: 2024-12-31       │
                 │ period: MONTHLY        │
                 └────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 2: Execute Posting Matrix                                      │
│ Posting Matrix says: DR 1200 (Debitor), CR 8400 (Revenue)          │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                 ┌────────────────────────┐
                 │ System detects:        │
                 │ Document has           │
                 │ deferred_revenue_*     │
                 │ fields set!            │
                 └────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 3: ✅ INTERCEPT Posting Matrix Result                         │
│                                                                      │
│ Original (from posting matrix):                                     │
│   DR: 1200 (Debitor) €1,200                                        │
│   CR: 8400 (Revenue) €1,200                                        │
│                                                                      │
│ ⚡ System intercepts and modifies credit side:                      │
│   DR: 1200 (Debitor) €1,200                                        │
│   CR: 2610 (PRAP) €1,200  ← System replaces Revenue with PRAP     │
│                                                                      │
│ Create deferred_revenue_schedule:                                   │
│   total: €1,200                                                     │
│   original_revenue_account_id: 8400 (for future recognition)       │
│   deferred_revenue_account_id: 2610                                │
│   status: active                                                    │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STEP 4: Monthly Workflow (Runs Jan 31, Feb 28, etc.)               │
│                                                                      │
│ For each active schedule:                                           │
│   1. Calculate monthly amount (€1,200 / 12 = €100)                 │
│   2. ✅ CREATE GENERAL LEDGER ENTRIES (attached to sales invoice):  │
│      DR: 2610 (PRAP) €100                                          │
│      CR: 8400 (Revenue) €100  ← From schedule.target_revenue_*     │
│      document_id: source_document_id (the sales invoice)           │
│   3. Update schedule.recognized_amount += €100                     │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                 ┌────────────────────────┐
                 │ After 12 months:       │
                 │ - 12 GL entries on     │
                 │   the sales invoice   │
                 │ - All €1,200 recognized│
                 │ - Schedule: completed  │
                 │ - PRAP balance: €0     │
                 └────────────────────────┘
```

***

## Problem Statement

### Current State

CONA currently records subscription payments as immediate revenue:

```
Customer pays €1,200 for annual SaaS subscription (Jan 1)
Current behavior:
  DR: Bank Account €1,200
  CR: Revenue €1,200  ❌ INCORRECT - not compliant

Problem: Revenue shown in January P&L, but service spans 12 months
```

**This violates:**

* **HGB §252 (1) Nr. 5**: Realization principle - revenue only when earned
* **IFRS 15**: Revenue from contracts with customers over performance period
* **Matching Principle**: Revenue must match period when service delivered

### German Accounting Requirements

**Rechnungsabgrenzungsposten (ARAP)** - Required by German law:

* **Passive RAP (2610)**: Prepayments received for future services = **Liability**
* Must be amortized over service period (monthly for subscriptions)
* SKR03/SKR04: Account 2610 "Passive Rechnungsabgrenzungsposten"

### Business Impact

**Current Problems:**

1. **Inflated Revenue**: Annual subscriptions show 12x revenue in month 1
2. **Incorrect P\&L**: January shows €100k revenue, Feb-Dec show €0
3. **Missing Liabilities**: Balance sheet doesn't show obligation to deliver services
4. **Tax Issues**: Advance VAT recognition without service delivery
5. **Cancellation Chaos**: No clear way to handle mid-year cancellations

***

## Proposed Solution

### Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                    PADDLE TRANSACTION                            │
│  Transaction ID: txn_abc123                                      │
│  Amount: €1,200                                                  │
│  Billing Period: Jan 1 - Dec 31, 2024                          │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│                    SALES INVOICE (Document)                      │
│  ID: doc_001                                                     │
│  Type: sales-invoice                                             │
│  Amount: €1,200                                                  │
│  Status: completed                                               │
│  Date: 2024-01-01                                                │
│                                                                  │
│  ✅ Deferred Revenue Fields (set by integration):               │
│    deferred_revenue_start_date: 2024-01-01                      │
│    deferred_revenue_end_date: 2024-12-31                        │
│    deferred_revenue_period: MONTHLY                             │
│    deferred_revenue_schedule_id: null (not yet created)         │
│                                                                  │
│  Custom Properties:                                              │
│    paddle_transaction_id: txn_abc123                            │
│    paddle_subscription_id: sub_xyz                              │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼ Accounting Impact Created
┌─────────────────────────────────────────────────────────────────┐
│                    GENERAL LEDGER ENTRY                          │
│  DR: 1800 (Bank) €1,200                                         │
│  CR: 2610 (Deferred Revenue) €1,200 ← Account from            │
│       tools.settings.prap_account_id                            │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼ System detects deferred revenue via tool settings
                           ▼ Reads document.deferred_revenue_* fields
                           ▼ Auto-creates schedule!
┌─────────────────────────────────────────────────────────────────┐
│              DEFERRED REVENUE SCHEDULE (Tracking)                │
│  ID: drs_001                                                     │
│  source_document_id: doc_001                                     │
│  deferred_revenue_account_id: acc_2610                          │
│  target_revenue_account_id: acc_8401                            │
│                                                                  │
│  Total: €1,200        Recognized: €300 (25%)                    │
│  Remaining: €900                                                 │
│  Status: active       Period: Jan 1 - Dec 31, 2024             │
│                                                                  │
│  Recognition: monthly (€100/month)                              │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         │ Monthly workflow creates GL entries
                         │ (attached to the sales invoice)
                         │
                         ├──────────────┬──────────────┬──────────┐
                         ▼              ▼              ▼          ▼
         ┌─────────────────────────────────────────────────────────┐
         │     GENERAL LEDGER ENTRIES (Created Monthly)             │
         │     document_id → doc_001 (sales invoice)                │
         ├─────────────────────────────────────────────────────────┤
         │ Jan: GL entry (€100) ✓  → DR: 2610 CR: 8401            │
         │                                                          │
         │ Feb: GL entry (€100) ✓  → DR: 2610 CR: 8401            │
         │                                                          │
         │ Mar: GL entry (€100) ✓  → DR: 2610 CR: 8401            │
         │                                                          │
         │ Apr: (pending - runs Apr 30)                            │
         │ May: (pending)                                           │
         │ ...                                                      │
         └─────────────────────────────────────────────────────────┘
```

### Data Model

#### 1. Extend Documents Table

Add new columns for deferred revenue configuration and schedule link:

```prisma theme={null}
enum DEFERRED_REVENUE_PERIOD {
  DAILY     // Daily recognition (no proration needed)
  WEEKLY    // Weekly recognition using ISO weeks (Mon-Sun), prorated for partial weeks
  MONTHLY   // Monthly recognition (calendar months), prorated for partial months
  QUARTERLY // Quarterly recognition, prorated for partial quarters
  YEARLY    // Yearly recognition, prorated for partial years
}

model documents {
  id                           String    @id @default(cuid())
  // ... existing fields ...

  // NEW: Deferred revenue fields (set by integration when importing)
  deferred_revenue_start_date  DateTime?                // Service period start
  deferred_revenue_end_date    DateTime?                // Service period end
  deferred_revenue_period      DEFERRED_REVENUE_PERIOD? // Recognition frequency (type-safe enum)

  // NEW: Link to generated schedule (set when accounting impact created)
  deferred_revenue_schedule_id String?   @unique
  deferred_revenue_schedule    deferred_revenue_schedules? @relation(fields: [deferred_revenue_schedule_id], references: [id])

  // Integration-specific metadata stays in custom_properties
  custom_properties            Json?
}
```

**Document Example:**

```typescript theme={null}
import { DEFERRED_REVENUE_PERIOD } from '@cona/database';

// Sales Invoice Document (created by Paddle integration)
{
  id: "doc_001",
  type: "sales-invoice",
  amount: 1200,
  status: "completed",
  date: "2024-01-01",

  // ✅ Deferred revenue fields (populated by integration)
  deferred_revenue_start_date: "2024-01-01",
  deferred_revenue_end_date: "2024-12-31",
  deferred_revenue_period: DEFERRED_REVENUE_PERIOD.MONTHLY, // Type-safe enum

  // ✅ Schedule link (null until accounting impact created)
  deferred_revenue_schedule_id: null, // Will be set when posted to deferred revenue account

  // ✅ Integration metadata in custom_properties
  custom_properties: {
    paddle_transaction_id: "txn_abc123",
    paddle_subscription_id: "sub_xyz",
  }
}
```

**Why this design?**

* ✅ **Type-safe ENUM**: Prevents typos and invalid values at compile time
* ✅ **Deferred revenue data on document**: Integration sets it once during import
* ✅ **Account-based trigger**: Posting to deferred revenue account automatically creates schedule
* ✅ **Flexible**: Works for any integration (Paddle, Stripe, Chargebee, manual invoices)
* ✅ **Fallback-friendly**: If fields empty, can use document date + default duration
* ✅ **Proration built-in**: First/last periods automatically prorated by days

#### 2. Deferred Revenue Tool Configuration

Use the existing `tools` table to store deferred revenue configuration:

```prisma theme={null}
model tools {
  id           String       @id @default(cuid())
  tool_slug    String       // "deferred_revenue"
  settings     Json         // Tool-specific configuration
  org_id       String
  created_at   DateTime     @default(now())
  updated_at   DateTime     @updatedAt
  organization organization @relation(fields: [org_id], references: [id])

  @@unique([tool_slug, org_id])
  @@index([org_id])
}
```

**Deferred Revenue Tool Record:**

```typescript theme={null}
// tools record
{
  org_id: "org_123",
  tool_slug: "deferred_revenue",
  settings: {
    prap_account_id: "acc_2610",     // Which account is PRAP (2610)
    default_period: "MONTHLY"        // Fallback if document.deferred_revenue_period empty
  }
}
```

**Integration requirements:**

* Integrations **must** set `deferred_revenue_start_date` and `deferred_revenue_end_date`
* These come from the subscription billing period (always available from Paddle/Stripe/Chargebee)
* If missing, system will **reject** the document (not silently fallback)
* Only `deferred_revenue_period` can use fallback (defaults to MONTHLY)

**Revenue recognition:**

* Monthly Temporal workflow automatically processes all **active** schedules
* Schedule status controls whether revenue is recognized:
  * `active` → Recognized automatically by workflow
  * `completed` → All revenue already recognized
  * `cancelled` → No further recognition (e.g., customer refunded)
  * `suspended` → Temporarily paused (e.g., payment issues)

**Recognition timing:**

* Revenue is always recognized at the **end of each period**
* **MONTHLY**: Last day of month (Jan 31, Feb 28/29, Mar 31, etc.)
* **WEEKLY**: Last day of ISO week (Sunday)
* **DAILY**: Each day
* **QUARTERLY**: Last day of quarter (Mar 31, Jun 30, Sep 30, Dec 31)
* **YEARLY**: Last day of year (Dec 31)

This is standard accounting practice and matches regulatory requirements.

````

**Why this design?**
- ✅ **Uses existing infrastructure**: No new table needed
- ✅ **Separation of concerns**: Chart of Accounts stays pure accounting
- ✅ **Flexible configuration**: Can add/change settings without schema migrations
- ✅ **Consistent with CONA architecture**: Tools already used for integrations
- ✅ **Per-organization**: Each org can configure independently
- ✅ **UI-friendly**: Settings managed in existing "Tools" section

**Accessing the configuration:**

```typescript
// Fetch deferred revenue tool settings
const deferredRevenueTool = await prisma.tools.findUnique({
  where: {
    tool_slug_org_id: {
      tool_slug: "deferred_revenue",
      org_id: organizationId
    }
  }
});

const settings = deferredRevenueTool?.settings as {
  prap_account_id: string;
  default_period: DEFERRED_REVENUE_PERIOD;
};

// Get PRAP account
const prapAccount = await prisma.chart_of_accounts.findUnique({
  where: { id: settings.prap_account_id }
});
````

#### 3. New Table: `deferred_revenue_schedules`

```sql theme={null}
CREATE TABLE deferred_revenue_schedules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,

  -- Link to source invoice document
  source_document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,

  -- Account references (for automatic document creation)
  deferred_revenue_account_id UUID NOT NULL REFERENCES chart_of_accounts(id), -- 2610
  target_revenue_account_id UUID NOT NULL REFERENCES chart_of_accounts(id),   -- 8401

  -- Financial tracking
  total_amount NUMERIC(15,2) NOT NULL CHECK (total_amount > 0),
  recognized_amount NUMERIC(15,2) NOT NULL DEFAULT 0 CHECK (recognized_amount >= 0),
  remaining_amount NUMERIC(15,2) GENERATED ALWAYS AS (total_amount - recognized_amount) STORED,
  currency_code TEXT NOT NULL DEFAULT 'EUR',

  -- Schedule configuration
  start_date DATE NOT NULL,
  end_date DATE NOT NULL,
  recognition_frequency TEXT NOT NULL CHECK (recognition_frequency IN ('DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'YEARLY')),

  -- Status
  status TEXT NOT NULL CHECK (status IN ('active', 'completed', 'cancelled', 'suspended')),
  cancelled_at TIMESTAMP WITH TIME ZONE,
  cancellation_reason TEXT,
  cancellation_document_id UUID REFERENCES documents(id), -- links to credit note document

  -- Metadata
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  created_by UUID REFERENCES users(id),

  CONSTRAINT valid_date_range CHECK (end_date > start_date),
  CONSTRAINT valid_recognized_amount CHECK (recognized_amount <= total_amount),
  CONSTRAINT unique_source_document UNIQUE (source_document_id)
);

-- Indexes
CREATE INDEX idx_drs_org_status ON deferred_revenue_schedules(org_id, status);
CREATE INDEX idx_drs_accounts ON deferred_revenue_schedules(deferred_revenue_account_id, target_revenue_account_id);
CREATE INDEX idx_drs_recognition_dates ON deferred_revenue_schedules(start_date, end_date) WHERE status = 'active';
CREATE INDEX idx_drs_source_doc ON deferred_revenue_schedules(source_document_id);
```

**Key Fields:**

* `deferred_revenue_account_id`: The liability account (2610) - used as debit account for monthly recognition GL entries
* `target_revenue_account_id`: The revenue account (8401) - captured from posting matrix at interception, used as credit account for monthly recognition GL entries
* `cancellation_document_id`: Links to credit note document

````

#### 4. New Table: `revenue_recognition_entries`

```sql
CREATE TABLE revenue_recognition_entries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  schedule_id UUID NOT NULL REFERENCES deferred_revenue_schedules(id) ON DELETE CASCADE,

  -- Link to general_ledger entry (GL entry attached to source sales invoice)
  general_ledger_id UUID REFERENCES general_ledger(id) ON DELETE SET NULL,

  -- Recognition details
  recognition_date DATE NOT NULL,
  amount NUMERIC(15,2) NOT NULL CHECK (amount > 0),
  period TEXT NOT NULL, -- 'YYYY-MM' format for easy grouping

  -- Status
  status TEXT NOT NULL CHECK (status IN ('pending', 'recognized', 'cancelled', 'failed')),
  error_message TEXT, -- if status = 'failed'

  -- Metadata
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  recognized_at TIMESTAMP WITH TIME ZONE, -- when GL entry was created

  CONSTRAINT unique_schedule_period UNIQUE (schedule_id, period)
);

-- Indexes
CREATE INDEX idx_rre_schedule ON revenue_recognition_entries(schedule_id, recognition_date);
CREATE INDEX idx_rre_status_date ON revenue_recognition_entries(status, recognition_date);
CREATE INDEX idx_rre_period ON revenue_recognition_entries(period) WHERE status = 'pending';
CREATE INDEX idx_rre_general_ledger ON revenue_recognition_entries(general_ledger_id);
````

**Key Design:**

* GL entries are created directly and attached to the **source sales invoice** (`document_id` on `general_ledger` = source invoice)
* No new document type; recognition entries link to `general_ledger` for audit trail
* Accounts come from the schedule: `deferred_revenue_account_id` (DR) and `target_revenue_account_id` (CR)

***

## Implementation Details

### 1. Transaction Import Flow (Paddle)

**Step 1: Integration populates deferred revenue fields on document**

```typescript theme={null}
// packages/temporal-workflows/src/activities/paddle/process-transaction.ts

async function processPaddleTransactionActivity(transaction: PaddleTransaction) {
  const { items, billing_period, customer_id, total } = transaction;

  // 1. Extract billing cycle from price
  const price = items[0].price;
  const billingCycle = price.billing_cycle; // { interval: "year", frequency: 1 }

  // 2. Determine if should defer revenue
  const shouldDefer = shouldDeferRevenue(billingCycle); // true for annual/quarterly

  // 3. Create sales invoice document with deferred revenue fields
  const invoice = await createDocument({
    type: "sales-invoice",
    amount: total,
    currency: transaction.currency_code,
    customer_id: customer_id,
    date: transaction.created_at,

    // ✅ NEW: Set deferred revenue fields (type-safe with enum)
    deferred_revenue_start_date: shouldDefer ? billing_period?.starts_at : null,
    deferred_revenue_end_date: shouldDefer ? billing_period?.ends_at : null,
    deferred_revenue_period: shouldDefer ? DEFERRED_REVENUE_PERIOD.MONTHLY : null,

    custom_properties: {
      paddle_transaction_id: transaction.id,
      paddle_subscription_id: transaction.subscription_id,
    },
  });

  // 4. Create accounting impact
  // The system will automatically detect deferred revenue account
  // and create schedule based on document fields
  await createAccountingImpact({
    documentId: invoice.id,
    entries: shouldDefer
      ? [
          { account: "1800", debit: total }, // Bank Account
          { account: "2610", credit: total }, // Deferred Revenue ← Triggers schedule!
        ]
      : [
          { account: "1800", debit: total }, // Bank Account
          { account: "8401", credit: total }, // SaaS Revenue
        ],
  });
}

function shouldDeferRevenue(billingCycle: BillingCycle): boolean {
  if (billingCycle.interval === "year") return true;
  if (billingCycle.interval === "month" && billingCycle.frequency >= 3) return true;
  return false;
}
```

**Step 2: System intercepts posting matrix for prepaid invoices**

```typescript theme={null}
// @cona/core/accounting/create-accounting-impact.ts

// Helper: Get Deferred Revenue Configuration
async function getDeferredRevenueConfig(organizationId: string) {
  return await prisma.tools.findUnique({
    where: {
      tool_slug_org_id: {
        tool_slug: "deferred_revenue",
        org_id: organizationId,
      },
    },
  });
}

// Type-safe settings access
type DeferredRevenueSettings = {
  prap_account_id: string;
  default_period: DEFERRED_REVENUE_PERIOD;
};

export async function createAccountingImpact({ documentId }: CreateAccountingImpactParams) {
  const document = await getDocument(documentId);

  // 1. Execute posting matrix to get the accounting entries
  const postingMatrixResult = await executePostingMatrix({
    documentType: document.type,
    document,
  });

  // postingMatrixResult = [
  //   { account: "1200", debit: 1200 },  // Debitor
  //   { account: "8400", credit: 1200 }  // Revenue (from posting matrix)
  // ]

  // 2. ✅ CHECK: Is this a prepaid invoice?
  const isPrepaidInvoice =
    document.deferred_revenue_start_date && document.deferred_revenue_end_date;

  let finalEntries = postingMatrixResult;
  let originalRevenueAccount = null;

  if (isPrepaidInvoice) {
    // 3. ✅ INTERCEPT: Redirect revenue to PRAP account
    const deferredRevenueTool = await getDeferredRevenueConfig(organizationId);

    if (!deferredRevenueTool?.settings?.prap_account_id) {
      throw new Error(
        "PRAP account not configured. Please configure deferred revenue tool settings."
      );
    }

    const prapAccount = await prisma.chart_of_accounts.findUnique({
      where: { id: deferredRevenueTool.settings.prap_account_id },
    });

    if (!prapAccount) {
      throw new Error(`PRAP account ${deferredRevenueTool.settings.prap_account_id} not found`);
    }

    // Find the revenue (credit) entry from posting matrix
    const revenueEntry = postingMatrixResult.find((e) => e.credit && e.credit > 0);
    originalRevenueAccount = revenueEntry?.account;

    // Replace revenue account with PRAP account
    finalEntries = postingMatrixResult.map((entry) => {
      if (entry.credit && entry.credit > 0) {
        // This is the revenue entry - replace with PRAP
        return {
          ...entry,
          account: prapAccount.account_nr, // 2610 instead of 8400
          _originalAccount: originalRevenueAccount, // Store for logging
        };
      }
      return entry;
    });

    console.log(
      `Prepaid invoice detected: Redirecting ${originalRevenueAccount} → ${prapAccount.account_nr} (PRAP)`
    );
  }

  // 4. Create GL entries with (potentially modified) accounts
  for (const entry of finalEntries) {
    await createGeneralLedger({
      documentId,
      accountCode: entry.account,
      debit: entry.debit,
      credit: entry.credit,
    });
  }

  // 5. ✅ LOG INTERCEPTION to audit logs
  if (isPrepaidInvoice) {
    await log({
      message: "Deferred revenue interception applied",
      level: "info",
      metadata: {
        documentId: document.id,
        documentNumber: document.nr,
        interceptionType: "prepaid_invoice",
        originalAccount: originalRevenueAccount,
        modifiedAccount: prapAccount.account_nr,
        amount: postingMatrixResult.find((e) => e.credit)?.credit || 0,
        deferralPeriod: {
          start: document.deferred_revenue_start_date,
          end: document.deferred_revenue_end_date,
          frequency: document.deferred_revenue_period,
        },
        reason: "Prepaid subscription - revenue will be recognized monthly",
      },
    });

    // Also create activity log entry visible in document history
    await createActivityLog({
      action: "deferred_revenue_interception",
      documentId: document.id,
      actorId: "system", // System action
      details: `Posting matrix intercepted: Revenue account ${originalRevenueAccount} → PRAP account ${prapAccount.account_nr}. Revenue will be recognized monthly from ${format(document.deferred_revenue_start_date, "MMM yyyy")} to ${format(document.deferred_revenue_end_date, "MMM yyyy")}.`,
      metadata: {
        interception: {
          originalAccount: originalRevenueAccount,
          prapAccount: prapAccount.account_nr,
          scheduleId: null, // Will be set after schedule creation
        },
      },
    });
  }

  // 6. ✅ CREATE SCHEDULE if prepaid
  if (isPrepaidInvoice && originalRevenueAccount) {
    const schedule = await createDeferredRevenueSchedule({
      document,
      prapAccount,
      originalRevenueAccount,
      amount: postingMatrixResult.find((e) => e.credit)?.credit || 0,
    });

    // Update activity log with schedule ID
    await updateActivityLog({
      documentId: document.id,
      action: "deferred_revenue_interception",
      updateMetadata: {
        "interception.scheduleId": schedule.id,
      },
    });
  }
}

async function createDeferredRevenueSchedule({
  document,
  prapAccount,
  originalRevenueAccount,
  amount,
  deferredRevenueTool,
}: CreateScheduleParams) {
  // 1. Get period and dates from document fields (set by integration)
  const period = document.deferred_revenue_period || deferredRevenueTool.settings.default_period;
  const startDate = document.deferred_revenue_start_date;
  const endDate = document.deferred_revenue_end_date;

  // 2. VALIDATION: Dates are required - integration must provide them
  if (!startDate || !endDate) {
    throw new Error(
      `Document ${document.id} missing required deferred revenue dates. ` +
        `Integration must set deferred_revenue_start_date and deferred_revenue_end_date ` +
        `from subscription billing period.`
    );
  }

  if (!document.deferred_revenue_period) {
    console.warn(
      `Document ${document.id} missing deferred_revenue_period. ` +
        `Using fallback: ${period} from tool settings`
    );
  }

  // 3. Create deferred revenue schedule
  const schedule = await prisma.deferred_revenue_schedules.create({
    data: {
      source_document_id: document.id,
      org_id: document.org_id,

      total_amount: amount,
      currency_code: document.currency,

      start_date: startDate,
      end_date: endDate,
      recognition_frequency: period, // Already DEFERRED_REVENUE_PERIOD enum value

      deferred_revenue_account_id: prapAccount.id, // 2610 (PRAP)
      target_revenue_account_id: originalRevenueAccount, // 8400 (from posting matrix)
      status: "active",
    },
  });

  // 4. Link schedule back to document
  await prisma.documents.update({
    where: { id: document.id },
    data: {
      deferred_revenue_schedule_id: schedule.id,
    },
  });

  console.log(
    `Created deferred revenue schedule ${schedule.id}: ` +
      `${amount} from ${prapAccount.account_nr} → ${originalRevenueAccount} ` +
      `over ${differenceInMonths(endDate, startDate)} months`
  );
}
```

```typescript theme={null}
// packages/temporal-workflows/src/activities/paddle/process-transaction.ts

async function processPaddleTransactionActivity(transaction: PaddleTransaction) {
  const { items, billing_period, customer_id, total } = transaction;

  // 1. Extract billing cycle from price
  const price = items[0].price;
  const billingCycle = price.billing_cycle; // { interval: "year", frequency: 1 }

  // 2. Create sales invoice document
  const invoice = await createDocument({
    type: "sales-invoice",
    amount: total,
    currency: transaction.currency_code,
    customer_id: customer_id,
    date: transaction.created_at,

    custom_properties: {
      paddle_transaction_id: transaction.id,
      paddle_subscription_id: transaction.subscription_id,
      paddle_billing_period_start: billing_period?.starts_at,
      paddle_billing_period_end: billing_period?.ends_at,
    },
  });

  // 3. Create deferred revenue schedule (if applicable)
  if (shouldDeferRevenue(billingCycle)) {
    const schedule = await createDeferredRevenueSchedule({
      source_document_id: invoice.id,
      integration_slug: "paddle",
      integration_id: integrationId,
      external_transaction_id: transaction.id,
      external_subscription_id: transaction.subscription_id,

      total_amount: total,
      currency_code: transaction.currency_code,

      start_date: billing_period.starts_at,
      end_date: billing_period.ends_at,
      recognition_frequency: DEFERRED_REVENUE_PERIOD.MONTHLY,
      status: "active",
    });

    // Update invoice with schedule reference (proper FK)
    await updateDocument(invoice.id, {
      deferred_revenue_schedule_id: schedule.id,
    });
  }

  // 4. Create accounting impact (initial booking)
  await createAccountingImpact({
    documentId: invoice.id,
    entries: shouldDeferRevenue(billingCycle)
      ? [
          // Prepayment booking
          { account: "1800", debit: total }, // Bank Account
          { account: "2610", credit: total }, // Deferred Revenue (Liability)
        ]
      : [
          // Immediate recognition (monthly subscriptions)
          { account: "1800", debit: total }, // Bank Account
          { account: "8401", credit: total }, // SaaS Revenue
        ],
  });
}

function shouldDeferRevenue(billingCycle: BillingCycle): boolean {
  // Defer revenue for:
  // - Annual subscriptions
  // - Quarterly subscriptions (3+ months)
  // - Multi-year subscriptions

  if (billingCycle.interval === "year") return true;
  if (billingCycle.interval === "month" && billingCycle.frequency >= 3) return true;

  return false;
}
```

### 2. Monthly Revenue Recognition Workflow

**Creates general ledger entries attached to the sales invoice**

```typescript theme={null}
// packages/temporal-workflows/src/workflows/revenue-recognition-workflow.ts

export async function monthlyRevenueRecognitionWorkflow() {
  const today = getCurrentUtcTime();
  const lastDayOfMonth = getLastDayOfMonth(today);
  const period = format(today, "yyyy-MM");

  // 1. Fetch active schedules that need recognition this month
  const schedules = await fetchSchedulesForRecognition({
    recognitionDate: lastDayOfMonth,
    status: "active",
  });

  console.log(`Processing ${schedules.length} deferred revenue schedules for ${period}`);

  // 2. Process each schedule - CREATE GL ENTRIES attached to source invoice
  const results = await Promise.allSettled(
    schedules.map((schedule) =>
      recognizeRevenueForSchedule({
        schedule,
        recognitionDate: lastDayOfMonth,
        period,
      })
    )
  );

  // 3. Log results
  const succeeded = results.filter((r) => r.status === "fulfilled").length;
  const failed = results.filter((r) => r.status === "rejected").length;

  console.log(`Revenue recognition complete: ${succeeded} succeeded, ${failed} failed`);

  return {
    totalSchedules: schedules.length,
    succeeded,
    failed,
    period,
  };
}

async function recognizeRevenueForSchedule({
  schedule,
  recognitionDate,
  period,
}: RecognizeRevenueParams) {
  // 1. Calculate amount to recognize this month
  const monthlyAmount = calculateMonthlyRecognitionAmount(schedule);

  // 2. Check if already recognized for this period
  const existing = await findRevenueRecognitionEntry({
    schedule_id: schedule.id,
    period,
  });

  if (existing) {
    console.log(`Already recognized for schedule ${schedule.id} in ${period}`);
    return existing;
  }

  // 3. ✅ CREATE GL ENTRY attached to source sales invoice
  //    Accounts from schedule: DR deferred_revenue_account_id, CR target_revenue_account_id
  const glEntry = await prisma.general_ledger.create({
    data: {
      document_id: schedule.source_document_id, // ← Attached to sales invoice
      debit_account_id: schedule.deferred_revenue_account_id,
      credit_account_id: schedule.target_revenue_account_id,
      amount: monthlyAmount,
      currency: schedule.currency_code,
      transaction_date: recognitionDate,
      label: `Deferred revenue recognition ${period}`,
      org_id: schedule.org_id,
      // ... accounting_period_id, dimensions, etc.
    },
  });

  // 4. Create tracking entry
  const recognitionEntry = await prisma.revenue_recognition_entries.create({
    data: {
      schedule_id: schedule.id,
      general_ledger_id: glEntry.id,
      recognition_date: recognitionDate,
      amount: monthlyAmount,
      period,
      status: "recognized",
      recognized_at: new Date(),
    },
  });

  // 5. Update schedule totals
  const newRecognizedAmount = schedule.recognized_amount + monthlyAmount;
  const isComplete = newRecognizedAmount >= schedule.total_amount;

  await prisma.deferred_revenue_schedules.update({
    where: { id: schedule.id },
    data: {
      recognized_amount: newRecognizedAmount,
      status: isComplete ? "completed" : "active",
      updated_at: new Date(),
    },
  });

  console.log(
    `Created GL entry ${glEntry.id} for schedule ${schedule.id}: ${monthlyAmount} (${period})`
  );

  return recognitionEntry;
}

function calculateMonthlyRecognitionAmount(schedule: DeferredRevenueSchedule): number {
  const totalMonths =
    differenceInMonths(new Date(schedule.end_date), new Date(schedule.start_date)) + 1; // +1 because inclusive

  const monthlyAmount = schedule.total_amount / totalMonths;

  // Handle rounding - last month gets remainder
  const recognizedMonths = Math.floor(schedule.recognized_amount / monthlyAmount);
  const isLastMonth = recognizedMonths === totalMonths - 1;

  if (isLastMonth) {
    return schedule.total_amount - schedule.recognized_amount;
  }

  return monthlyAmount;
}
```

### 3. Accounting Logic for Recognition

**GL entries use accounts from the schedule:**

* **Debit**: `deferred_revenue_account_id` (PRAP, e.g. 2610) from the schedule
* **Credit**: `target_revenue_account_id` (e.g. 8401) from the schedule — stored when the posting matrix intercepts the original invoice

No posting matrix is used for recognition; accounts are determined at schedule creation time from the intercepted invoice's posting matrix result.

### 4. Cancellation Handling

```typescript theme={null}
// packages/temporal-workflows/src/activities/paddle/handle-subscription-cancellation.ts

export async function handleSubscriptionCancellationActivity({
  paddleSubscriptionId,
  cancellationDate,
  refundAmount,
}: CancellationParams) {
  // 1. Find active deferred revenue schedule
  const schedule = await findDeferredRevenueSchedule({
    external_subscription_id: paddleSubscriptionId,
    status: "active",
  });

  if (!schedule) {
    console.log(`No active deferred revenue schedule for subscription ${paddleSubscriptionId}`);
    return;
  }

  // 2. Calculate what happens to remaining balance
  const remainingAmount = schedule.remaining_amount;

  // 3. Create credit note for unearned portion
  const creditNote = await createDocument({
    type: "sales-credit-note",
    amount: refundAmount || remainingAmount,
    currency: schedule.currency_code,
    date: cancellationDate,

    // Link to original invoice
    references: [
      {
        document_id: schedule.source_document_id,
        reference_type: "credits",
      },
    ],

    custom_properties: {
      paddle_subscription_id: paddleSubscriptionId,
      paddle_adjustment_id: null, // Set when Paddle creates adjustment
      cancellation_reason: "subscription_cancelled",
    },
  });

  // 4. Close the schedule
  await updateDeferredRevenueSchedule(schedule.id, {
    status: "cancelled",
    cancelled_at: new Date(cancellationDate),
    cancellation_reason: "subscription_cancelled",
    cancellation_adjustment_id: creditNote.id,
  });

  // 5. Create accounting impact for cancellation
  await createAccountingImpact({
    documentId: creditNote.id,
    entries:
      refundAmount > 0
        ? [
            // With refund
            { account: "2610", debit: remainingAmount }, // Clear deferred revenue
            { account: "1800", credit: refundAmount }, // Refund from bank
          ]
        : [
            // No refund (pro-rated to cancellation date)
            { account: "2610", debit: remainingAmount }, // Clear deferred revenue
            { account: "6900", credit: remainingAmount }, // Cancellation adjustment
          ],
  });

  return {
    schedule_id: schedule.id,
    credit_note_id: creditNote.id,
    remaining_amount: remainingAmount,
    refund_amount: refundAmount,
  };
}
```

***

## UI/UX Implementation

### 1. Chart of Accounts Configuration

**Setup page: `/setup/chart-of-accounts`**

```tsx theme={null}
// apps/webapp/app/(pages)/(settings)/setup/chart-of-accounts/page.tsx

// When editing account 2610
<AccountEditForm account={account}>
  {/* Existing fields */}
  <Input label="Account Number" value="2610" />
  <Input label="Label" value="Passive Rechnungsabgrenzungsposten" />
  <Select label="Account Type" value="LIABILITY" />

  {/* NEW: Deferred Revenue Toggle */}
  <div className="space-y-4 mt-6 p-4 border rounded-lg">
    <div className="flex items-center justify-between">
      <div>
        <Label>Deferred Revenue Account</Label>
        <p className="text-sm text-muted-foreground">
          Automatically create revenue recognition schedules when posting to this account
        </p>
      </div>
      <Switch checked={isDeferredRevenueAccount} onCheckedChange={setIsDeferredRevenueAccount} />
    </div>

    {isDeferredRevenueAccount && (
      <div className="space-y-4 pl-4 border-l-2 border-blue-500">
        {/* Target Revenue Account */}
        <div>
          <Label>Target Revenue Account *</Label>
          <Select value={targetAccountId}>
            <option value="">Select account...</option>
            <option value="acc_8401">8401 - Erlöse aus Software-Abonnements</option>
            <option value="acc_8400">8400 - Allgemeine Erlöse</option>
          </Select>
          <p className="text-sm text-muted-foreground">
            Revenue will be recognized to this account monthly
          </p>
        </div>

        {/* Default Duration (Fallback) */}
        <div>
          <Label>Default Duration (Fallback)</Label>
          <Input
            type="number"
            value={defaultMonths}
            onChange={(e) => setDefaultMonths(e.target.value)}
            min={1}
            max={60}
          />
          <p className="text-sm text-muted-foreground">
            Used when invoice doesn't specify deferred revenue dates (typically from integrations)
          </p>
        </div>

        {/* Info Alert */}
        <Alert className="bg-blue-50 border-blue-200">
          <InfoIcon className="h-4 w-4 text-blue-600" />
          <AlertTitle>How it works</AlertTitle>
          <AlertDescription className="text-sm">
            <ol className="list-decimal list-inside space-y-1 mt-2">
              <li>
                Integration imports invoice with <code>deferred_revenue_start_date</code> and{" "}
                <code>deferred_revenue_end_date</code>
              </li>
              <li>Invoice posts to this account (2610)</li>
              <li>System automatically creates revenue recognition schedule</li>
              <li>Monthly workflow recognizes revenue to target account (8401)</li>
            </ol>
          </AlertDescription>
        </Alert>
      </div>
    )}
  </div>
</AccountEditForm>
```

### 2. Sales Invoice Document View

```tsx theme={null}
// apps/webapp/app/(pages)/(documents)/documents/[id]/page.tsx

export default async function DocumentPage({ params }) {
  const document = await getDocument(params.id);
  const hasDeferredRevenue = !!document.deferred_revenue_schedule_id;

  return (
    <div>
      {/* Existing invoice details */}
      <DocumentHeader document={document} />
      <DocumentLineItems document={document} />

      {/* NEW: Deferred Revenue Card */}
      {hasDeferredRevenue && (
        <DeferredRevenueCard
          documentId={document.id}
          scheduleId={document.deferred_revenue_schedule_id}
        />
      )}

      {/* Accounting Impact Card - shows interception */}
      <AccountingImpactCard document={document} />

      {/* Activity Log - shows interception event */}
      <ActivityLogCard document={document} />
    </div>
  );
}
```

### Activity Log Display

When viewing the invoice, users will see an activity log entry:

```
┌─────────────────────────────────────────────────────────────┐
│ Activity Log                                                 │
├─────────────────────────────────────────────────────────────┤
│ 🤖 System • 2 minutes ago                                   │
│ Deferred Revenue Interception                               │
│                                                              │
│ Posting matrix intercepted: Revenue account 8400 → PRAP     │
│ account 2610. Revenue will be recognized monthly from       │
│ Jan 2024 to Dec 2024.                                       │
│                                                              │
│ Details:                                                     │
│ • Original account: 8400 (SaaS Revenue)                     │
│ • PRAP account: 2610 (Passive RAP)                          │
│ • Amount: €1,200                                            │
│ • Schedule: View Recognition Schedule →                     │
└─────────────────────────────────────────────────────────────┘
```

### 2. Deferred Revenue Card Component

```tsx theme={null}
// apps/webapp/app/ui/documents/deferred-revenue-card.tsx

export function DeferredRevenueCard({ scheduleId }: { scheduleId: string }) {
  const { data: schedule } = useDeferredRevenueSchedule(scheduleId);

  if (!schedule) return null;

  const progressPercent = (schedule.recognized_amount / schedule.total_amount) * 100;
  const nextRecognition = getNextRecognitionDate(schedule);

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <CalendarClock className="w-5 h-5" />
          Deferred Revenue Recognition
        </CardTitle>
        <CardDescription>Revenue is being recognized over the service period</CardDescription>
      </CardHeader>

      <CardContent className="space-y-4">
        {/* Progress Bar */}
        <div className="space-y-2">
          <div className="flex justify-between text-sm">
            <span className="text-muted-foreground">Recognition Progress</span>
            <span className="font-medium">{progressPercent.toFixed(0)}%</span>
          </div>
          <Progress value={progressPercent} className="h-2" />
          <div className="flex justify-between text-sm text-muted-foreground">
            <span>{formatCurrency(schedule.recognized_amount)} recognized</span>
            <span>{formatCurrency(schedule.remaining_amount)} remaining</span>
          </div>
        </div>

        {/* Status Badge */}
        <div className="flex items-center gap-4">
          <div>
            <div className="text-sm text-muted-foreground">Status</div>
            <Badge variant={getStatusVariant(schedule.status)}>{schedule.status}</Badge>
          </div>

          <div>
            <div className="text-sm text-muted-foreground">Service Period</div>
            <div className="text-sm font-medium">
              {formatDate(schedule.start_date)} - {formatDate(schedule.end_date)}
            </div>
          </div>

          {schedule.status === "active" && nextRecognition && (
            <div>
              <div className="text-sm text-muted-foreground">Next Recognition</div>
              <div className="text-sm font-medium">
                {formatDate(nextRecognition)} ({formatCurrency(schedule.monthly_amount)})
              </div>
            </div>
          )}
        </div>

        {/* Quick Stats */}
        <div className="grid grid-cols-3 gap-4 pt-4 border-t">
          <div>
            <div className="text-2xl font-bold">{formatCurrency(schedule.total_amount)}</div>
            <div className="text-sm text-muted-foreground">Total Amount</div>
          </div>
          <div>
            <div className="text-2xl font-bold text-green-600">
              {formatCurrency(schedule.recognized_amount)}
            </div>
            <div className="text-sm text-muted-foreground">Recognized</div>
          </div>
          <div>
            <div className="text-2xl font-bold text-orange-600">
              {formatCurrency(schedule.remaining_amount)}
            </div>
            <div className="text-sm text-muted-foreground">Deferred (Liability)</div>
          </div>
        </div>

        {/* Actions */}
        <div className="flex gap-2 pt-4">
          <Button variant="outline" asChild>
            <Link href={`/accounting/deferred-revenue/${schedule.id}`}>
              <Eye className="w-4 h-4 mr-2" />
              View Recognition Schedule
            </Link>
          </Button>

          {schedule.status === "active" && (
            <Button variant="outline" onClick={() => handleManualRecognition(schedule.id)}>
              <Zap className="w-4 h-4 mr-2" />
              Recognize Revenue Now
            </Button>
          )}
        </div>
      </CardContent>
    </Card>
  );
}
```

### 3. Deferred Revenue Schedule Detail Page

```tsx theme={null}
// apps/webapp/app/(pages)/(accounting)/accounting/deferred-revenue/[id]/page.tsx

export default async function DeferredRevenueSchedulePage({ params }) {
  const schedule = await getDeferredRevenueSchedule(params.id);
  const entries = await getRevenueRecognitionEntries(params.id);
  const sourceInvoice = await getDocument(schedule.source_document_id);

  return (
    <div className="space-y-6">
      {/* Header */}
      <div>
        <h1 className="text-3xl font-bold">Revenue Recognition Schedule</h1>
        <p className="text-muted-foreground">
          Tracking revenue recognition for Invoice{" "}
          <Link href={`/documents/${sourceInvoice.id}`}>{sourceInvoice.document_number}</Link>
        </p>
      </div>

      {/* Summary Card */}
      <Card>
        <CardContent className="pt-6">
          <div className="grid grid-cols-4 gap-6">
            <StatCard
              label="Total Amount"
              value={formatCurrency(schedule.total_amount)}
              icon={<DollarSign />}
            />
            <StatCard
              label="Recognized"
              value={formatCurrency(schedule.recognized_amount)}
              trend={`${progressPercent}%`}
              icon={<TrendingUp />}
            />
            <StatCard
              label="Remaining"
              value={formatCurrency(schedule.remaining_amount)}
              icon={<Clock />}
            />
            <StatCard
              label="Monthly Amount"
              value={formatCurrency(schedule.monthly_amount)}
              icon={<Calendar />}
            />
          </div>
        </CardContent>
      </Card>

      {/* Recognition Timeline */}
      <Card>
        <CardHeader>
          <CardTitle>Recognition Timeline</CardTitle>
          <CardDescription>Monthly revenue recognition entries for this schedule</CardDescription>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Period</TableHead>
                <TableHead>Recognition Date</TableHead>
                <TableHead>Amount</TableHead>
                <TableHead>Status</TableHead>
                <TableHead>GL Entry</TableHead>
                <TableHead>Actions</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {entries.map((entry) => (
                <TableRow key={entry.id}>
                  <TableCell className="font-medium">{entry.period}</TableCell>
                  <TableCell>{formatDate(entry.recognition_date)}</TableCell>
                  <TableCell>{formatCurrency(entry.amount)}</TableCell>
                  <TableCell>
                    <Badge variant={getStatusVariant(entry.status)}>{entry.status}</Badge>
                  </TableCell>
                  <TableCell>
                    {entry.general_ledger_id ? (
                      <Link href={`/accounting/entries/${entry.general_ledger_id}`}>
                        <Button variant="link" size="sm">
                          View Entry →
                        </Button>
                      </Link>
                    ) : (
                      <span className="text-muted-foreground">—</span>
                    )}
                  </TableCell>
                  <TableCell>
                    {entry.status === "pending" && (
                      <Button variant="ghost" size="sm" onClick={() => recognizeNow(entry.id)}>
                        Recognize Now
                      </Button>
                    )}
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </CardContent>
      </Card>

      {/* Cancellation Section (if cancelled) */}
      {schedule.status === "cancelled" && (
        <Alert variant="destructive">
          <AlertTriangle className="h-4 w-4" />
          <AlertTitle>Schedule Cancelled</AlertTitle>
          <AlertDescription>
            This schedule was cancelled on {formatDate(schedule.cancelled_at)}. Reason:{" "}
            {schedule.cancellation_reason}
            {schedule.cancellation_adjustment_id && (
              <Link href={`/documents/${schedule.cancellation_adjustment_id}`}>
                View Credit Note →
              </Link>
            )}
          </AlertDescription>
        </Alert>
      )}
    </div>
  );
}
```

***

## German Accounting Compliance

### Chart of Accounts (SKR03)

```
1800 - Bank
2610 - Passive Rechnungsabgrenzungsposten (Deferred Revenue Liability)
8401 - Erlöse aus Software-Abonnements (SaaS Subscription Revenue)
6900 - Sonstige Aufwendungen (Other Expenses - for cancellations)
```

### Example Bookings

**Initial Payment (Jan 1, 2024):**

```
Beleg: Sales Invoice INV-2024-001
Soll 1800 (Bank)                    €1,200.00
Haben 2610 (Passive RAP)            €1,200.00
Buchungstext: "Annual SaaS subscription received"
```

**Monthly Recognition (Jan 31, 2024):**

```
Beleg: GL Entry (attached to INV-2024-001)
Soll 2610 (Passive RAP)             €100.00
Haben 8401 (SaaS Revenue)           €100.00
Buchungstext: "Revenue recognition January 2024"
```

**Cancellation with Refund (Apr 15, 2024):**

```
Beleg: Credit Note CN-2024-001
Soll 2610 (Passive RAP)             €900.00  (remaining 9 months)
Haben 1800 (Bank)                   €900.00  (refund)
Buchungstext: "Subscription cancelled - refund unearned revenue"
```

***

## Temporal Workflow Schedule

### Monthly Revenue Recognition

```typescript theme={null}
// Schedule: Last day of every month at 23:00 UTC
const scheduleId = "monthly-revenue-recognition";

await client.schedule.create({
  scheduleId,
  spec: {
    calendars: [
      {
        dayOfMonth: -1, // Last day of month
        hour: 23,
        minute: 0,
      },
    ],
  },
  action: {
    type: "startWorkflow",
    workflowType: monthlyRevenueRecognitionWorkflow,
    taskQueue: "accounting-operations",
  },
});
```

***

## Migration Plan

### Phase 1: Foundation (Week 1)

* [ ] Create database tables
* [ ] Add core functions for schedule management
* [ ] Implement `shouldDeferRevenue()` logic

### Phase 2: Integration (Week 2)

* [ ] Update Paddle transaction processing
* [ ] Test with sandbox transactions
* [ ] Add custom properties to documents

### Phase 3: Recognition (Week 3)

* [ ] Implement monthly workflow
* [ ] Test recognition entries
* [ ] Schedule Temporal workflow

### Phase 4: UI (Week 4)

* [ ] Build DeferredRevenueCard component
* [ ] Create schedule detail page
* [ ] Add to document view

### Phase 5: Cancellations (Week 5)

* [ ] Implement cancellation handler
* [ ] Test refund scenarios
* [ ] Webhook integration

***

## Testing Strategy

### Unit Tests

* Schedule creation logic
* Amount calculation (monthly, daily)
* Rounding edge cases
* Cancellation scenarios

### Integration Tests

* Full Paddle transaction → schedule → recognition flow
* Cancellation with refund
* Multiple subscriptions per customer

### E2E Tests

* Import annual subscription
* Verify deferred revenue booking
* Trigger monthly workflow
* Check GL entries created
* Cancel and verify credit note

***

## Proration Implementation ✅

### Decision: Automatic Proration for Partial Periods

**Status: IMPLEMENTED in CONA-740**

All partial periods (first/last months, weeks, etc.) are **automatically prorated by days** to ensure accurate revenue recognition.

### MONTHLY Proration Example

**Scenario:** €120 for 12-month subscription starting Jan 15, 2024

```typescript theme={null}
Service period: Jan 15, 2024 → Jan 14, 2025
Total: €120
Base monthly amount: €120 ÷ 12 = €10/month

Recognition schedule with proration:
1. Jan 15-31 (2024):  16 days / 31 days × €10 = €5.16  ✅ PRORATED
2. Feb 1-29 (2024):   29 days / 29 days × €10 = €10.00 (full month, leap year)
3. Mar 1-31 (2024):   31 days / 31 days × €10 = €10.00 (full month)
4. Apr 1-30 (2024):   30 days / 30 days × €10 = €10.00 (full month)
...
11. Dec 1-31 (2024):  31 days / 31 days × €10 = €10.00 (full month)
12. Jan 1-14 (2025):  14 days / 31 days × €10 = €4.52  ✅ PRORATED

Total: €120.00 (exact, last period gets remainder to prevent rounding drift)
```

### WEEKLY Proration Example (ISO Weeks)

**Scenario:** €52 for 52-week subscription starting Wednesday (mid-week)

```typescript theme={null}
Service period: Wed, Jan 3, 2024 → Tue, Jan 2, 2025
Total: €52
Base weekly amount: €52 ÷ 52 = €1/week
ISO Week: Monday-Sunday

Recognition schedule with proration:
1. Wed-Sun (Week 1):  5 days / 7 days × €1 = €0.71  ✅ PRORATED (partial week)
2. Mon-Sun (Week 2):  7 days / 7 days × €1 = €1.00  (full week)
3. Mon-Sun (Week 3):  7 days / 7 days × €1 = €1.00  (full week)
...
51. Mon-Sun (Week 51): 7 days / 7 days × €1 = €1.00 (full week)
52. Mon-Tue (Week 52): 2 days / 7 days × €1 = €0.29  ✅ PRORATED (partial week)

Total: €52.00 (exact)
```

### Proration Formula

```typescript theme={null}
function calculateProratedAmount(
  baseAmount: number,
  periodStart: Date,
  periodEnd: Date,
  frequency: DEFERRED_REVENUE_PERIOD
): number {
  switch (frequency) {
    case DEFERRED_REVENUE_PERIOD.MONTHLY: {
      const daysInMonth = getDaysInMonth(periodStart);
      const daysInPeriod = differenceInDays(periodEnd, periodStart) + 1; // Inclusive
      return (baseAmount * daysInPeriod) / daysInMonth;
    }

    case DEFERRED_REVENUE_PERIOD.WEEKLY: {
      const daysInPeriod = differenceInDays(periodEnd, periodStart) + 1; // Inclusive
      return (baseAmount * daysInPeriod) / 7;
    }

    case DEFERRED_REVENUE_PERIOD.DAILY:
      return baseAmount; // No proration needed - already granular

    case DEFERRED_REVENUE_PERIOD.QUARTERLY: {
      const daysInQuarter =
        differenceInDays(getEndOfQuarter(periodStart), getStartOfQuarter(periodStart)) + 1;
      const daysInPeriod = differenceInDays(periodEnd, periodStart) + 1;
      return (baseAmount * daysInPeriod) / daysInQuarter;
    }

    case DEFERRED_REVENUE_PERIOD.YEARLY: {
      const daysInYear = isLeapYear(periodStart) ? 366 : 365;
      const daysInPeriod = differenceInDays(periodEnd, periodStart) + 1;
      return (baseAmount * daysInPeriod) / daysInYear;
    }
  }
}
```

### Rounding Strategy

**Last period gets the remainder** to prevent floating-point drift:

```typescript theme={null}
// Calculate all periods except last
const recognitions = periods.slice(0, -1).map((period) => ({
  period,
  amount: calculateProratedAmount(baseAmount, period.start, period.end, frequency),
}));

const totalRecognized = recognitions.reduce((sum, r) => sum + r.amount, 0);

// Last period = remaining amount (ensures exact total)
const lastRecognition = {
  period: periods[periods.length - 1],
  amount: totalAmount - totalRecognized,
};

recognitions.push(lastRecognition);

// Verify: recognitions.reduce((sum, r) => sum + r.amount, 0) === totalAmount ✅
```

### Benefits

✅ **IFRS 15 Compliant** - Revenue proportional to performance obligation
✅ **Accurate** - Matches service delivery exactly
✅ **Handles Edge Cases** - Leap years, varying month lengths, partial weeks
✅ **No Manual Adjustment** - Fully automated
✅ **Exact Totals** - No rounding errors (last period correction)

### Implementation Status

* ✅ **CONA-740**: Database schema with `DEFERRED_REVENUE_PERIOD` enum
* 🔲 **Future**: Schedule creation with proration calculation
* 🔲 **Future**: Monthly recognition workflow

See: `/apps/internal-docs/implementation-notes/deferred-revenue-proration.md`

***

## Open Questions

1. ~~**Partial Periods**: How to handle subscriptions starting mid-month?~~ ✅ **RESOLVED**
   * **Decision**: Auto-prorate first and last periods by days (implemented in CONA-740)

2. **Upgrades/Downgrades**: How to handle plan changes mid-period?
   * **Proposal**: Phase 2 feature - create new schedule, prorate old

3. **Multiple Currencies**: How to handle FX changes over recognition period?
   * **Proposal**: Lock FX rate at transaction date (IFRS approach)

4. ~~**Daily vs Monthly**: Should we support daily recognition?~~ ✅ **RESOLVED**
   * **Decision**: Support DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY via enum (implemented in CONA-740)

5. **Manual Adjustments**: Can accountants manually adjust schedules?
   * **Proposal**: Yes, with audit trail and approval workflow

***

## Risks & Mitigation

| Risk                         | Impact | Mitigation                                                  |
| ---------------------------- | ------ | ----------------------------------------------------------- |
| Complex cancellation logic   | High   | Start with simple refund scenarios, iterate                 |
| Performance (many schedules) | Medium | Index optimization, batch processing, pagination            |
| Wrong amounts recognized     | High   | Extensive testing, audit logs, manual reconciliation tools  |
| Missed monthly runs          | High   | Temporal retry policies, monitoring alerts, manual catch-up |
| Integration bugs             | Medium | Test with sandbox first, gradual rollout per integration    |

***

## Success Metrics

* **Correctness**: 100% of deferred revenue schedules match expected recognition
* **Compliance**: Pass external audit review for HGB/IFRS compliance
* **Performance**: Process 10,000+ schedules in \<5 minutes
* **Reliability**: 99.9% success rate for monthly recognition runs
* **User Experience**: \<2 seconds to load schedule detail page

***

## Appendix: Example Scenarios

### Scenario 1: Annual Subscription

```
Customer: Acme Corp
Plan: Pro Annual
Amount: €1,200
Period: Jan 1 - Dec 31, 2024

Initial Booking (Jan 1):
  DR: Bank €1,200
  CR: Deferred Revenue €1,200

Monthly Recognition (Jan 31, Feb 29, ... Dec 31):
  DR: Deferred Revenue €100
  CR: Revenue €100

Result: €100/month revenue recognized over 12 months
```

### Scenario 2: Quarterly Subscription

```
Customer: StartupXYZ
Plan: Starter Quarterly
Amount: €300
Period: Jan 1 - Mar 31, 2024

Initial Booking (Jan 1):
  DR: Bank €300
  CR: Deferred Revenue €300

Monthly Recognition (Jan 31, Feb 29, Mar 31):
  DR: Deferred Revenue €100
  CR: Revenue €100

Result: €100/month revenue recognized over 3 months
```

### Scenario 3: Cancellation After 3 Months

```
Customer: CancelCorp
Plan: Pro Annual
Amount: €1,200
Period: Jan 1 - Dec 31, 2024
Cancelled: Apr 15, 2024

Recognized so far:
  Jan: €100 ✓
  Feb: €100 ✓
  Mar: €100 ✓
  Total: €300

Cancellation (Apr 15):
  Credit Note for €900 (remaining 9 months)
  DR: Deferred Revenue €900
  CR: Bank €900 (refund)

Result: €300 revenue recognized, €900 refunded
```

***

## References

* [IFRS 15 - Revenue from Contracts with Customers](https://www.ifrs.org/issued-standards/list-of-standards/ifrs-15-revenue-from-contracts-with-customers/)
* [HGB §252 - Bewertungsgrundsätze](https://www.gesetze-im-internet.de/hgb/__252.html)
* [SKR03 Kontenrahmen](https://www.datev.de/web/de/m/ueber-datev/datev-im-web/steuerberatung/skr-03/)
* [Stripe Revenue Recognition](https://stripe.com/docs/revenue-recognition)
* [Chargebee Revenue Recognition](https://www.chargebee.com/docs/2.0/revenue-recognition.html)
