Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fe301ed
feat(perf): implement lazy loading and code splitting for faster star…
sweetesty May 29, 2026
1c96410
feat(#384): Implement sandbox environment for developer testing
sweetesty May 31, 2026
c5d9e2a
refactor: Reorganize backend services into domain subdirectories
sweetesty May 31, 2026
2b81516
refactor(#399): Backend services from monolithic to domain-based modules
sweetesty May 31, 2026
b56320b
Merge remote-tracking branch 'origin/main' into feature/399-backend-d…
sweetesty May 31, 2026
ad92b72
Merge remote-tracking branch 'origin/main' into feature/384-sandbox-e…
sweetesty May 31, 2026
fb5378b
Merge origin/main into feat/410-lazy-loading-code-splitting
sweetesty May 31, 2026
d329adf
fix: Fix subscriptionStore syntax errors, broken imports, formatting,…
sweetesty May 31, 2026
906dcb3
fix: Fix AppNavigator lazyScreen type constraint and SettingsScreen i…
sweetesty May 31, 2026
2171870
fix: Fix subscriptionStore syntax errors, broken imports, formatting,…
sweetesty May 31, 2026
2d5bfea
fix: Fix subscriptionStore syntax errors, broken imports, formatting,…
sweetesty May 31, 2026
e25c3e8
fix: Fix subscriptionStore syntax errors, broken imports, formatting,…
sweetesty May 31, 2026
6197d3e
feat: implement affiliate marketing, multi-touch attribution, clawbac…
sweetesty May 31, 2026
acb8580
Merge branch 'main' of https://github.com/sweetesty/SubTrackr into fe…
sweetesty May 31, 2026
4dcc604
feat: merge feat/410-lazy-loading-code-splitting with conflict resolu…
sweetesty Jun 7, 2026
5d14613
feat: implement billing infrastructure including error handling, merc…
sweetesty Jun 7, 2026
3424d78
chore: merge main into feat/affiliate-referral-system
sweetesty Jun 7, 2026
0ced2b9
style: apply prettier and eslint --fix formatting
sweetesty Jun 7, 2026
1f73cd0
feat: merge feat/410-lazy-loading-code-splitting into feat/affiliate-…
sweetesty Jun 7, 2026
a9dddfc
Resolve git merge conflicts, fix styling bugs, and typecheck issues
sweetesty Jun 7, 2026
734f4c3
feat: implement gamification components, affiliate dashboard, and sup…
sweetesty Jun 7, 2026
2b3d468
chore: resolve merge conflicts
sweetesty Jun 7, 2026
0e24204
Merge branch 'main' of https://github.com/sweetesty/SubTrackr into fe…
sweetesty Jun 7, 2026
6d9d3de
feat: implement sandbox management store with API key hashing and add…
sweetesty Jun 7, 2026
81e3041
chore: allowlist 4 new axios advisories in audit-ci
sweetesty Jun 7, 2026
b0322c5
fix: resolve CI failures across Rust, k6, i18n, and npm audit
sweetesty Jun 7, 2026
b3b484a
fix: replace grafana/k6-action@v0 with direct k6 apt install
sweetesty Jun 7, 2026
9783999
fix: bump RUST_VERSION to 1.88 in CI
sweetesty Jun 7, 2026
b6b32a6
fix: resolve remaining CI failures in build and e2e workflows
sweetesty Jun 7, 2026
2ab1741
fix: resolve cargo-fuzz rustix breakage and babel cache conflict
sweetesty Jun 7, 2026
716e71d
fix: rust fmt, batch syntax error, k6 json import, ios workspace, kot…
sweetesty Jun 7, 2026
f2e321b
Merge branch 'feat/410-lazy-loading-code-splitting' of https://github…
sweetesty Jun 8, 2026
02b0f2a
Merge branch 'Smartdevs17:main' into main
sweetesty Jun 8, 2026
03cb197
Merge branch 'main' of https://github.com/sweetesty/SubTrackr into fe…
sweetesty Jun 8, 2026
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
140 changes: 140 additions & 0 deletions backend/services/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Backend Services Architecture

## Module Boundaries

```
backend/services/
├── container.ts # IoC Container — sole coupling point
├── index.ts # Public API barrel
├── shared/ # Cross-cutting infrastructure
│ ├── errors.ts # DomainError base class
│ ├── logging.ts # Structured logger
│ ├── encryption.ts # PII encryption & blind indexes
│ ├── apiResponse.ts # Standard API response envelope
│ ├── apiClient.ts # HTTP client
│ ├── auditService.ts # Audit trail
│ ├── monitoring.ts # Health checks & metrics
│ ├── rateLimitingService.ts # Rate limiting
│ ├── gdpr.ts # Data subject requests
│ ├── keyManager.ts # Key rotation
│ └── piiAudit.ts # PII access audit
├── subscription/ # Subscription domain
│ ├── interfaces.ts # ISubscriptionEventStore, IElasticsearchService
│ ├── errors.ts # SubscriptionError + SubscriptionErrorCode
│ ├── subscriptionEventStore.ts
│ ├── ElasticsearchService.ts
│ └── __tests__/
├── billing/ # Billing domain
│ ├── interfaces.ts # IMeteringService, IPricingService, ITaxService, etc.
│ ├── errors.ts # BillingError + BillingErrorCode
│ ├── meteringService.ts
│ ├── pricingService.ts
│ ├── taxService.ts
│ ├── dunningService.ts
│ ├── accountingExportService.ts
│ └── __tests__/
├── notification/ # Notification domain
│ ├── interfaces.ts # INotificationPreferenceService, IAlertingService, etc.
│ ├── errors.ts # NotificationError + NotificationErrorCode
│ ├── preferenceService.ts
│ ├── alerting.ts
│ ├── webhook.ts
│ ├── websocket.ts
│ └── __tests__/
└── analytics/ # Analytics domain
├── interfaces.ts # IPredictionService, IRecommendationService, etc.
├── errors.ts # AnalyticsError + AnalyticsErrorCode
├── campaignService.ts
├── complianceReport.ts
├── dataPipeline.ts
├── dataWarehouse.ts
├── predictionService.ts
├── recommendationService.ts
├── retentionService.ts
├── oracleMonitorService.ts
└── __tests__/
```

## Domain Modules

### subscription
**Responsibility:** Subscription lifecycle, event sourcing, full-text search.
**Interfaces:** `ISubscriptionEventStore`, `IElasticsearchService`
**Depends on:** `shared` (errors, types, logging)
**DOES NOT depend on:** `billing`, `notification`, `analytics`

### billing
**Responsibility:** Usage metering, pricing, tax calculation, dunning, accounting exports.
**Interfaces:** `IMeteringService`, `IPricingService`, `ITaxService`, `IDunningService`, `IAccountingExportService`
**Depends on:** `shared` (errors, types, logging)
**DOES NOT depend on:** `subscription`, `notification`, `analytics`

### notification
**Responsibility:** Push notifications, webhooks, alerts, WebSocket real-time, user preferences.
**Interfaces:** `INotificationPreferenceService`, `IAlertingService`, `IWebhookDeliveryService`, `IWebsocketService`
**Depends on:** `shared` (errors, types, logging)
**DOES NOT depend on:** `subscription`, `billing`, `analytics`

### analytics
**Responsibility:** Campaigns, churn prediction, recommendations, compliance reports, oracle data.
**Interfaces:** `IPredictionService`, `IRecommendationService`, `IComplianceReportService`, `ICampaignService`
**Depends on:** `shared` (errors, types, logging)
**DOES NOT depend on:** `subscription`, `billing`, `notification`

## Dependency Injection

All cross-module communication flows through the `Container` in `container.ts`. Modules NEVER import concrete classes from sibling domains — they only depend on interfaces registered with I-prefix tokens.

```typescript
// ✅ CORRECT — resolve via container
const billing = container.resolve<IBillingService>('IBillingService');

// ❌ WRONG — direct cross-module import
import { SubscriptionEventStore } from '../subscription/subscriptionEventStore';
```

### Container API

| Method | Description |
|--------|-------------|
| `register(token, instance)` | Register an eager singleton |
| `bind(token, factory, lifetime?)` | Lazy binding (singleton by default) |
| `bindTransient(token, factory)` | New instance on every resolve |
| `resolve(token)` | Resolve a dependency (throws if missing) |
| `tryResolve(token)` | Resolve or return null |
| `has(token)` | Check if token is registered |
| `registerModule(reg)` | Bulk-register module bindings |
| `disposeAll()` | Call dispose() on all Disposable singletons |
| `clear()` | Reset all bindings (test isolation) |
| `listTokens()` | List all registered tokens |

## Error Handling

Each module has its own error class extending `DomainError` and a set of typed error codes:

- `SubscriptionError` / `SubscriptionErrorCode` — `SUB_NOT_FOUND`, `SUB_EVENT_STORE_FULL`, etc.
- `BillingError` / `BillingErrorCode` — `BILL_PAYMENT_FAILED`, `BILL_TAX_CALCULATION_FAILED`, etc.
- `NotificationError` / `NotificationErrorCode` — `NOTIF_DELIVERY_FAILED`, `NOTIF_WEBHOOK_HEALTH_FAILED`, etc.
- `AnalyticsError` / `AnalyticsErrorCode` — `ANALYTICS_PREDICTION_FAILED`, `ANALYTICS_INSUFFICIENT_DATA`, etc.

Every error includes a factory method for common cases (e.g. `SubscriptionError.notFound(id)`).

## Anti-Patterns (Avoid)

1. **Cross-module imports of concrete classes** — Always use interfaces + container
2. **Circular dependencies between modules** — Container detects and throws
3. **Shared mutable state** — Each module owns its own state
4. **Direct filesystem access between modules** — Use the shared infrastructure layer
5. **Module A importing from module B's internal utils** — Abstract via shared/ or interfaces

## Testing

Each module has `__tests__/module.test.ts` validating:
- Error codes are unique and correctly typed
- DI container bindings resolve correctly
- Container edge cases (circular deps, missing tokens, transient vs singleton)

Run module-level tests:
```bash
npm test -- --testPathPattern="backend/services/.*/module.test.ts"
```
65 changes: 65 additions & 0 deletions backend/services/analytics/__tests__/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Module-level tests for analytics domain.
* Validates error codes and DI container integration.
*/
import { Container } from '../../container';
import { AnalyticsError, AnalyticsErrorCode } from '../errors';

describe('Analytics Module', () => {
// ── Error handling ──────────────────────────────────────────────────────────

describe('AnalyticsError', () => {
it('creates predictionFailed error', () => {
const err = AnalyticsError.predictionFailed('0xabc', 'insufficient_history');
expect(err.code).toBe(AnalyticsErrorCode.PREDICTION_FAILED);
expect(err.details).toEqual({ subscriberAddress: '0xabc', reason: 'insufficient_history' });
});

it('creates insufficientData error', () => {
const err = AnalyticsError.insufficientData('churn_rate');
expect(err.code).toBe(AnalyticsErrorCode.INSUFFICIENT_DATA);
expect(err.details).toEqual({ metric: 'churn_rate' });
});

it('creates oracleFetchFailed error', () => {
const err = AnalyticsError.oracleFetchFailed('ETH', 'timeout');
expect(err.code).toBe(AnalyticsErrorCode.ORACLE_FETCH_FAILED);
expect(err.details).toEqual({ token: 'ETH', reason: 'timeout' });
});

it('all error codes are unique within the module', () => {
const codes = Object.values(AnalyticsErrorCode);
expect(new Set(codes).size).toBe(codes.length);
});
});

// ── DI Container bindings ───────────────────────────────────────────────────

describe('DI Container', () => {
let container: Container;

beforeEach(() => {
container = new Container();
});

it('resolves IPredictionService', () => {
container.bind('IPredictionService', () => ({ predictChurn: jest.fn() }));
expect(container.resolve('IPredictionService')).toBeDefined();
});

it('resolves IRecommendationService', () => {
container.bind('IRecommendationService', () => ({ getRecommendations: jest.fn() }));
expect(container.resolve('IRecommendationService')).toBeDefined();
});

it('resolves IComplianceReportService', () => {
container.bind('IComplianceReportService', () => ({ generateComplianceReport: jest.fn() }));
expect(container.resolve('IComplianceReportService')).toBeDefined();
});

it('resolves ICampaignService', () => {
container.bind('ICampaignService', () => ({ createCampaign: jest.fn() }));
expect(container.resolve('ICampaignService')).toBeDefined();
});
});
});
41 changes: 41 additions & 0 deletions backend/services/analytics/errors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import { DomainError } from '../shared/errors';
import { ErrorCode } from '../shared/apiResponse';

/**
* Analytics module error codes.
* All codes follow pattern: ANALYTICS_[CATEGORY]_[SPECIFIC]
*/
export const AnalyticsErrorCode = {
PREDICTION_FAILED: 'ANALYTICS_PREDICTION_FAILED' as ErrorCode,
RECOMMENDATION_FAILED: 'ANALYTICS_RECOMMENDATION_FAILED' as ErrorCode,
REPORT_GENERATION_FAILED: 'ANALYTICS_REPORT_GENERATION_FAILED' as ErrorCode,
DATA_PIPELINE_FAILED: 'ANALYTICS_DATA_PIPELINE_FAILED' as ErrorCode,
DATA_WAREHOUSE_FAILED: 'ANALYTICS_DATA_WAREHOUSE_FAILED' as ErrorCode,
CAMPAIGN_CREATION_FAILED: 'ANALYTICS_CAMPAIGN_CREATION_FAILED' as ErrorCode,
COUPON_VALIDATION_FAILED: 'ANALYTICS_COUPON_VALIDATION_FAILED' as ErrorCode,
ORACLE_FETCH_FAILED: 'ANALYTICS_ORACLE_FETCH_FAILED' as ErrorCode,
INSUFFICIENT_DATA: 'ANALYTICS_INSUFFICIENT_DATA' as ErrorCode,
RETENTION_ANALYSIS_FAILED: 'ANALYTICS_RETENTION_ANALYSIS_FAILED' as ErrorCode,
} as const;

export class AnalyticsError extends DomainError {
constructor(code: ErrorCode, message: string, details?: Record<string, string>) {
super(code, message, details);
}

static predictionFailed(subscriberAddress: string, reason: string): AnalyticsError {
return new AnalyticsError(
AnalyticsErrorCode.PREDICTION_FAILED,
`Churn prediction failed for ${subscriberAddress}: ${reason}`,
{ subscriberAddress, reason }
);
}

static insufficientData(metric: string): AnalyticsError {
return new AnalyticsError(
AnalyticsErrorCode.INSUFFICIENT_DATA,
`Insufficient data to compute ${metric}`,
{ metric }
);
}

static oracleFetchFailed(token: string, reason: string): AnalyticsError {
return new AnalyticsError(
AnalyticsErrorCode.ORACLE_FETCH_FAILED,
`Oracle price fetch failed for ${token}: ${reason}`,
{ token, reason }
);
}
}
2 changes: 1 addition & 1 deletion backend/services/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export type { Recommendation, RecommendationContext } from './recommendationServ
export { RetentionService } from './retentionService';
export { OracleMonitorService, oracleMonitorService } from './oracleMonitorService';
export type { IPredictionService, IRecommendationService, IComplianceReportService, ICampaignService } from './interfaces';
export { AnalyticsError } from './errors';
export { AnalyticsError, AnalyticsErrorCode } from './errors';
122 changes: 122 additions & 0 deletions backend/services/billing/__tests__/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Module-level tests for billing domain.
* Validates error codes, DI container bindings, and module boundaries.
*/
import { container, Container } from '../../container';
import { BillingError, BillingErrorCode } from '../errors';

describe('Billing Module', () => {
// ── Error handling ──────────────────────────────────────────────────────────

describe('BillingError', () => {
it('creates paymentFailed error with correct code', () => {
const err = BillingError.paymentFailed('sub_123', 'insufficient_funds');
expect(err.code).toBe(BillingErrorCode.PAYMENT_FAILED);
expect(err.message).toContain('sub_123');
expect(err.message).toContain('insufficient_funds');
expect(err.details).toEqual({ subscriptionId: 'sub_123', reason: 'insufficient_funds' });
});

it('creates taxCalculationFailed error', () => {
const err = BillingError.taxCalculationFailed('merchant_1', 'invalid_nexus');
expect(err.code).toBe(BillingErrorCode.TAX_CALCULATION_FAILED);
});

it('creates dunningFailed error', () => {
const err = BillingError.dunningFailed('sub_456', 'final_notice');
expect(err.code).toBe(BillingErrorCode.DUNNING_FAILED);
expect(err.details).toEqual({ subscriptionId: 'sub_456', stage: 'final_notice' });
});

it('all error codes are unique within the module', () => {
const codes = Object.values(BillingErrorCode);
expect(new Set(codes).size).toBe(codes.length);
});
});

// ── DI Container bindings ───────────────────────────────────────────────────

describe('DI Container', () => {
let testContainer: Container;

beforeEach(() => {
testContainer = new Container();
});

it('resolves IMeteringService', () => {
testContainer.bind('IMeteringService', () => ({ recordUsage: jest.fn() }));
const svc = testContainer.resolve('IMeteringService');
expect(svc).toBeDefined();
expect(typeof svc.recordUsage).toBe('function');
});

it('resolves IPricingService', () => {
testContainer.bind('IPricingService', () => ({ calculateOptimalPrice: jest.fn() }));
const svc = testContainer.resolve('IPricingService');
expect(svc).toBeDefined();
});

it('resolves ITaxService', () => {
testContainer.bind('ITaxService', () => ({ calculateTax: jest.fn() }));
const svc = testContainer.resolve('ITaxService');
expect(svc).toBeDefined();
});

it('resolves IDunningService', () => {
testContainer.bind('IDunningService', () => ({ startDunning: jest.fn() }));
const svc = testContainer.resolve('IDunningService');
expect(svc).toBeDefined();
});

it('throws for unregistered token', () => {
expect(() => testContainer.resolve('IUnregisteredService')).toThrow(
'Service not registered'
);
});

it('binds singletons by default (same instance)', () => {
testContainer.bind('ITestService', () => ({}));
const a = testContainer.resolve('ITestService');
const b = testContainer.resolve('ITestService');
expect(a).toBe(b);
});

it('binds transients (new instance each resolve)', () => {
testContainer.bindTransient('ITestService', () => ({}));
const a = testContainer.resolve('ITestService');
const b = testContainer.resolve('ITestService');
expect(a).not.toBe(b);
});

it('detects circular dependencies', () => {
testContainer.bind('IA', (c) => c.resolve('IB'));
testContainer.bind('IB', (c) => c.resolve('IA'));
expect(() => testContainer.resolve('IA')).toThrow('Circular dependency');
});

it('has() returns true for registered tokens', () => {
testContainer.bind('ITestService', () => ({}));
expect(testContainer.has('ITestService')).toBe(true);
expect(testContainer.has('IUnknown')).toBe(false);
});

it('clear() removes all bindings', () => {
testContainer.bind('ITestService', () => ({}));
expect(testContainer.has('ITestService')).toBe(true);
testContainer.clear();
expect(testContainer.has('ITestService')).toBe(false);
});
});

// ── Module boundary ─────────────────────────────────────────────────────────

describe('Module boundary', () => {
it('billing module does not import from subscription directly', () => {
// Interfaces enforce the boundary — concrete classes are never imported across modules.
// The container is the sole coupling point.
expect(container.has('IMeteringService')).toBe(true);
expect(container.has('ISubscriptionEventStore')).toBe(true);
// They live in different domain modules but are wired via the container.
});
});
});
Loading
Loading