Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,6 @@ export type { RotationEmailData } from './notification/rotationEmailTemplate';

// ── Monitoring — Lock Metrics (Issue #610) ────────────────────────────────────
export { collectLockMetrics, lockMetricsExporter } from '../monitoring/lockMetrics';

// ── Proration Calculator API (Issue #784) ──────────────────────────────────────
export { ProrationApiService } from './prorationApiService';
105 changes: 105 additions & 0 deletions backend/services/prorationApiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Backend Proration API & Controller Service
*
* Exposes proration calculation, policy configuration, and analytics
* endpoints for backend/API consumption.
*
* @see https://github.com/Smartdevs17/SubTrackr/issues/784
*/

import type {
ProrationConfig,
ProrationCalculationRequest,
ProrationCalculationResult,
ProrationAnalyticsSummary,
ProrationRecord,
} from '../../src/types/prorationCalculator';
import { DEFAULT_PRORATION_CONFIG } from '../../src/types/prorationCalculator';
import {
calculateProration,
buildProrationAnalytics,
} from '../../src/services/prorationCalculatorService';

export class ProrationApiService {
private config: ProrationConfig = { ...DEFAULT_PRORATION_CONFIG };
private history: ProrationRecord[] = [];

constructor(initialConfig?: Partial<ProrationConfig>) {
if (initialConfig) {
this.config = { ...this.config, ...initialConfig };
}
}

/**
* POST /api/v1/proration/calculate
* Calculate exact prorated amount with full breakdown.
*/
calculateProration(request: ProrationCalculationRequest): ProrationCalculationResult {
const mergedRequest: ProrationCalculationRequest = {
...request,
config: { ...this.config, ...request.config },
};

const result = calculateProration(mergedRequest);

// Save record if subscription ID present
if (request.subscriptionId) {
this.history.push({
id: `proration-rec-${Date.now().toString(36)}`,
subscriptionId: request.subscriptionId,
result,
status: 'preview',
createdAt: Date.now(),
});
}

return result;
}

/**
* POST /api/v1/proration/apply
* Mark a proration calculation as applied to a subscription.
*/
applyProration(calculationId: string, subscriptionId: string): ProrationRecord | null {
const record = this.history.find((r) => r.result.id === calculationId || r.id === calculationId);
if (!record) return null;

record.status = 'applied';
record.appliedAt = Date.now();
record.subscriptionId = subscriptionId;
return record;
}

/**
* GET /api/v1/proration/config
* Retrieve current proration configuration.
*/
getConfig(): ProrationConfig {
return { ...this.config };
}

/**
* PUT /api/v1/proration/config
* Update proration configuration.
*/
updateConfig(newConfig: Partial<ProrationConfig>): ProrationConfig {
this.config = { ...this.config, ...newConfig };
return { ...this.config };
}

/**
* GET /api/v1/proration/analytics
* Retrieve proration analytics summary.
*/
getAnalytics(): ProrationAnalyticsSummary {
return buildProrationAnalytics(this.history);
}

/**
* GET /api/v1/proration/history/:subscriptionId
* Retrieve proration history for a specific subscription.
*/
getHistoryForSubscription(subscriptionId: string): ProrationRecord[] {
return this.history.filter((r) => r.subscriptionId === subscriptionId);
}
}
139 changes: 139 additions & 0 deletions docs/subscription-proration-calculator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Subscription Proration Calculator with Transparency

> Issue: [#784](https://github.com/Smartdevs17/SubTrackr/issues/784)

## Overview

SubTrackr's Subscription Proration Calculator provides transparent, exact-day calculations for plan changes (upgrades, downgrades, cancellations, and billing cycle switches). It removes opacity around mid-cycle adjustments by generating itemized line-item breakdowns, human-readable explanations, tax adjustments, credit memo applications, and historical analytics.

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ Transparent Proration Calculator │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Calculation Req │───▶│ Calculator │───▶│ Transparent │ │
│ │ (Plan A ➔ B) │ │ Engine │ │ Line Items │ │
│ └─────────────────┘ └────────┬────────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌──────────────┐ │
│ │ Human-Readable │ │ Net Balance │ │
│ │ Explanation Text│ │ & Credit Memo│ │
│ └─────────────────┘ └──────────────┘ │
│ │ │ │
│ └─────────┬──────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Proration Store │ │
│ │ & Analytics │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

## Calculation Formula

Proration uses exact-day calculations based on cycle boundaries:

$$\text{Daily Rate (Old)} = \frac{\text{Old Plan Price}}{\text{Cycle Total Days}}$$

$$\text{Daily Rate (New)} = \frac{\text{New Plan Price}}{\text{New Cycle Total Days}}$$

$$\text{Unused Credit} = \text{Daily Rate (Old)} \times \text{Days Remaining}$$

$$\text{Prorated Charge} = \text{Daily Rate (New)} \times \text{Days Remaining}$$

$$\text{Net Adjustment} = \text{Prorated Charge} - \text{Unused Credit}$$

If $\text{Net Adjustment} > 0$, the customer pays the difference immediately.
If $\text{Net Adjustment} < 0$, the customer receives an account credit.

## Key Features

1. **Proration Calculator**: Real-time exact-day calculation engine supporting plan upgrades, downgrades, cancellations, and cycle changes.
2. **Transparent Proration Display**: Line-item breakdown displaying unused days credited vs. remaining days charged, with clear human-readable explanations.
3. **Proration Analytics**: Track total calculations, upgrades, downgrades, revenue collected, credits issued, and top upgrade paths over time.
4. **Proration Configuration**: Customizable policies (`exact_day`, `calendar_month`), tax rules, and minimum charge thresholds.
5. **Proration API**: Server-side service (`ProrationApiService`) exposing REST endpoints for backend integration.
6. **State Management & UI**: Persistent Zustand store (`useProrationStore`), React hook (`useProrationCalculator`), and React Native screen component (`ProrationCalculatorScreen`).

## Usage

### React Hook Example

```tsx
import { useProrationCalculator } from '../hooks/useProrationCalculator';

function PlanChangeModal() {
const { calculate, activePreview, applyProration } = useProrationCalculator();

const handlePreview = () => {
const result = calculate({
currentPlanId: 'basic-monthly',
currentPlanName: 'Basic Plan',
currentPrice: 29.99,
currentCycle: BillingCycle.MONTHLY,
newPlanId: 'pro-monthly',
newPlanName: 'Pro Plan',
newPrice: 59.99,
newCycle: BillingCycle.MONTHLY,
cycleStartDate: '2026-07-01',
cycleEndDate: '2026-07-31',
effectiveDate: '2026-07-16',
});

console.log(result.explanationText);
console.log(`Net due: $${result.netProratedAmount}`);
};
}
```

### Backend API Example

```typescript
import { ProrationApiService } from './services/prorationApiService';

const prorationApi = new ProrationApiService({
includeTax: true,
defaultTaxRate: 10,
});

const result = prorationApi.calculateProration({
subscriptionId: 'sub_9912',
currentPlanId: 'p1',
currentPlanName: 'Starter',
currentPrice: 15,
currentCycle: BillingCycle.MONTHLY,
newPlanId: 'p2',
newPlanName: 'Growth',
newPrice: 45,
newCycle: BillingCycle.MONTHLY,
cycleStartDate: Date.now() - 10 * 86400000,
cycleEndDate: Date.now() + 20 * 86400000,
});
```

## File Structure

```
src/
├── types/
│ └── prorationCalculator.ts # Type definitions & default config
├── services/
│ ├── prorationCalculatorService.ts # Core calculator & analytics engine
│ └── __tests__/
│ └── prorationCalculatorService.test.ts # Unit tests
├── store/
│ └── prorationStore.ts # Zustand store
├── hooks/
│ └── useProrationCalculator.ts # React hook
└── components/subscription/
└── ProrationCalculatorScreen.tsx # Transparent UI component
backend/
└── services/
└── prorationApiService.ts # Backend service API
docs/
└── subscription-proration-calculator.md # Documentation
```
Loading
Loading