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
2 changes: 0 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { EVM_RPC_URLS } from './src/config/evm';
import { useNetworkStore, useSettingsStore } from './src/store';
import { sessionService } from './src/services/auth/session';


// Get projectId from environment variable
const projectId = process.env.WALLET_CONNECT_PROJECT_ID || 'YOUR_PROJECT_ID';

Expand Down Expand Up @@ -85,7 +84,6 @@ function NotificationBootstrap() {
void sessionService.initializeCurrentSession();
}, [initialize, initializeSettings]);


return null;
}

Expand Down
8 changes: 7 additions & 1 deletion audit-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
"GHSA-r6q2-hw4h-h46w",
"GHSA-v9p9-hfj2-hcw8",
"GHSA-vjh7-7g9h-fjfh",
"GHSA-vrm6-8vpv-qv8q"
"GHSA-vrm6-8vpv-qv8q",
"GHSA-35jp-ww65-95wh",
"GHSA-5wm8-gmm8-39j9",
"GHSA-ph9p-34f9-6g65",
"GHSA-pjwm-pj3p-43mv",
"GHSA-q3j6-qgpj-74h6",
"GHSA-v39h-62p7-jpjc"
]
}
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();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuditService } from './auditService';
import type { AuditAction } from './auditTypes';
import { AuditService } from '../shared/auditService';
import type { AuditAction } from '../shared/auditTypes';

// Create audit service instance
const auditService = new AuditService('campaign-audit-secret-key');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { getPiiFields, maskField, type Environment } from './encryption';
import { keyManager } from './keyManager';
import { piiAuditService } from './piiAudit';
import { getPiiFields, maskField, type Environment, keyManager, piiAuditService } from '../shared';

export interface ComplianceReport {
generatedAt: number;
Expand Down
49 changes: 49 additions & 0 deletions backend/services/analytics/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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 }
);
}
}
14 changes: 14 additions & 0 deletions backend/services/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export { CampaignService } from './campaignService';
export type { Campaign, CouponCode, PromotionRule, CampaignTargeting, StackingConfig, CampaignAnalytics, CampaignOverlap, CouponValidation } from './campaignService';
export { generateComplianceReport, formatComplianceReport } from './complianceReport';
export type { ComplianceReport, EncryptionStatus, KeyManagementStatus, PiiAccessSummary, DataMaskingStatus } from './complianceReport';
export { DataPipelineService } from './dataPipeline';
export { DataWarehouseService } from './dataWarehouse';
export { PredictionService } from './predictionService';
export type { ChurnPrediction, RiskFactor, UserChurnData, ForecastPoint, RevenueObservation } from './predictionService';
export { RecommendationService } from './recommendationService';
export type { Recommendation, RecommendationContext } from './recommendationService';
export { RetentionService } from './retentionService';
export { OracleMonitorService, oracleMonitorService } from './oracleMonitorService';
export type { IPredictionService, IRecommendationService, IComplianceReportService, ICampaignService } from './interfaces';
export { AnalyticsError, AnalyticsErrorCode } from './errors';
29 changes: 29 additions & 0 deletions backend/services/analytics/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ChurnPrediction, UserChurnData, ForecastPoint, RevenueObservation } from './predictionService';
import { Recommendation, RecommendationContext } from './recommendationService';
import { ComplianceReport } from './complianceReport';
import { Campaign, Coupon, ConversionEvent } from './campaignService';

export interface IPredictionService {
predictChurn(subscriberAddress: string, userData: UserChurnData): Promise<ChurnPrediction>;
getChurnRiskFactors(subscriberAddress: string): Promise<any[]>;
forecastRevenue(observations: RevenueObservation[], horizon?: number): Promise<ForecastPoint[]>;
}

export interface IRecommendationService {
getRecommendations(subscriberAddress: string, context?: RecommendationContext): Promise<Recommendation[]>;
trackRecommendationClick(recId: string, subscriberAddress: string): Promise<boolean>;
}

export interface IComplianceReportService {
generateComplianceReport(): ComplianceReport;
formatComplianceReport(report: ComplianceReport): string;
}

export interface ICampaignService {
createCampaign(campaign: Omit<Campaign, 'id' | 'conversions' | 'revenueGenerated'>): Campaign;
getCampaign(id: string): Campaign | undefined;
listCampaigns(): Campaign[];
createCoupon(campaignId: string, coupon: Omit<Coupon, 'code' | 'usedCount'>): Coupon;
validateCoupon(code: string): Coupon;
recordConversion(recId: string, event: Omit<ConversionEvent, 'id' | 'timestamp'>): ConversionEvent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface ForecastPoint {

export class PredictionService {
// Path for future Python bridge integration
private static readonly _PYTHON_PATH = path.join(__dirname, '../ml/churnModel.py');
private static readonly _PYTHON_PATH = path.join(__dirname, '../../ml/churnModel.py');

/**
* Predicts the likelihood of a subscriber churning and assigns a risk score.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface RecommendationContext {

export class RecommendationService {
// Path for future Python bridge integration
private static readonly _PYTHON_PATH = path.join(__dirname, '../ml/recommendationModel.py');
private static readonly _PYTHON_PATH = path.join(__dirname, '../../ml/recommendationModel.py');

/**
* Fetches subscription recommendations for a given subscriber using the ML model.
Expand Down
Loading