diff --git a/src/app/demo/notifications/page.tsx b/src/app/demo/notifications/page.tsx new file mode 100644 index 0000000..6d46fe0 --- /dev/null +++ b/src/app/demo/notifications/page.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useState } from "react"; +import { useToast } from "@/components/notifications/ToastProvider"; +import { InlineBanner } from "@/components/ui/InlineBanner"; +import { Button } from "@/components/ui/Button"; +import { NotificationCenter } from "@/components/notifications/NotificationCenter"; + +export default function NotificationsDemo() { + const { pushToast, dismissToast, setLimit, limit, toasts } = useToast(); + const [showBanner, setShowBanner] = useState<"success" | "info" | "warning" | "error" | null>(null); + const [showNotificationCenter, setShowNotificationCenter] = useState(false); + + const handleToast = (variant: "success" | "info" | "warning" | "error") => { + const messages = { + success: { title: "Changes saved", description: "Your settings have been updated successfully." }, + info: { title: "New feature available", description: "Check out the latest updates in your dashboard." }, + warning: { title: "Session expiring soon", description: "Please save your work before your session ends." }, + error: { title: "Save failed", description: "Could not save your changes. Please try again." }, + }; + + pushToast({ + title: messages[variant].title, + description: messages[variant].description, + variant, + duration: 4500, + }); + }; + + const handleMockFlow = (flow: "save" | "failure" | "timeout") => { + switch (flow) { + case "save": + pushToast({ title: "Saving...", description: "Please wait while we save your changes.", variant: "info", duration: 2000 }); + setTimeout(() => { + pushToast({ title: "Changes saved", description: "Your settings have been updated successfully.", variant: "success", duration: 4500 }); + }, 2000); + break; + case "failure": + pushToast({ title: "Processing...", description: "Please wait while we process your request.", variant: "info", duration: 2000 }); + setTimeout(() => { + pushToast({ title: "Request failed", description: "Network error occurred. Please check your connection.", variant: "error", duration: 5000 }); + }, 2000); + break; + case "timeout": + pushToast({ title: "Connecting...", description: "Establishing connection to server.", variant: "info", duration: 2000 }); + setTimeout(() => { + pushToast({ title: "Connection timeout", description: "Server did not respond in time. Please.try again.", variant: "warning", duration: 5500 }); + }, 2000); + break; + } + }; + + return ( +
+
+
+

Notification System Demo

+

Demonstrates toast notifications and inline banners with all variants.

+
+ + {/* Toast Queue Section */} +
+

Toast Notifications

+
+
+
+

Stacking Limit

+

Current limit: {limit}

+
+
+ + + + +
+
+
+

Active toasts: {toasts.length}

+
+
+ +
+ + + + +
+ +
+

Mock Common Flows

+
+ + + +
+
+
+ + {/* Inline Banner Section */} +
+

Inline Banners

+
+ + + + +
+ + {showBanner && ( +
+ + This is an inline banner with the {showBanner} variant. It can be used for page-level messages that require user attention. + + +
+ )} +
+ + {/* Notification Center Section */} +
+

Notification Center

+
+
+
+

In-App Notification Center

+

View and manage transaction and system event notifications

+
+ +
+ {showNotificationCenter && ( +
+ +
+ )} +
+
+ + {/* Accessibility Notes */} +
+

Accessibility Features

+
+
+
+
+

Keyboard Focusable

+

Close buttons are focusable with Tab key and can be activated with Enter/Space

+
+
+
+
+
+

Screen Reader Announcements

+

Uses aria-live regions for polite (info/success) and assertive (warning/error) announcements

+
+
+
+
+
+

Pause on Hover/Focus

+

Auto-dismiss timer pauses when hovering or focusing on toast

+
+
+
+
+
+

Auto-Dismiss (3-6s)

+

Toasts automatically dismiss after 3-6 seconds (configurable)

+
+
+
+
+
+

44px Touch Target

+

Preference toggle controls meet minimum touch target requirements

+
+
+
+
+
+
+ ); +} diff --git a/src/app/demo/release-checklist/page.tsx b/src/app/demo/release-checklist/page.tsx new file mode 100644 index 0000000..e04403e --- /dev/null +++ b/src/app/demo/release-checklist/page.tsx @@ -0,0 +1,442 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import { + ReleaseChecklist, + ChecklistItem, + ChecklistStatus, + SignOff, + KnownIssue +} from "@/lib/release-checklist-types"; +import { + CheckCircle2, + Clock, + XCircle, + AlertTriangle, + Download, + Plus, + Trash2, + Edit2, + Save +} from "lucide-react"; + +const statusConfig: Record = { + pending: { label: "Pending", color: "bg-slate-500/20 text-slate-300 border-slate-500/30", icon: Clock }, + "in-progress": { label: "In Progress", color: "bg-sky-500/20 text-sky-300 border-sky-500/30", icon: Clock }, + completed: { label: "Completed", color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30", icon: CheckCircle2 }, + blocked: { label: "Blocked", color: "bg-red-500/20 text-red-300 border-red-500/30", icon: XCircle }, + skipped: { label: "Skipped", color: "bg-amber-500/20 text-amber-300 border-amber-500/30", icon: AlertTriangle }, +}; + +const severityConfig: Record = { + critical: { label: "Critical", color: "bg-red-500/20 text-red-300 border-red-500/30" }, + high: { label: "High", color: "bg-orange-500/20 text-orange-300 border-orange-500/30" }, + medium: { label: "Medium", color: "bg-amber-500/20 text-amber-300 border-amber-500/30" }, + low: { label: "Low", color: "bg-slate-500/20 text-slate-300 border-slate-500/30" }, +}; + +export default function ReleaseChecklistPage() { + const [checklist, setChecklist] = useState({ + id: "1", + version: "v1.0.0", + title: "Release v1.0.0 - Initial Launch", + description: "Initial production release of NeuroWealth platform", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + sections: [ + { + id: "functional", + title: "Functional Checks", + items: [ + { + id: "f1", + title: "User authentication flow", + description: "Verify login, signup, and wallet connection", + status: "completed", + assignee: "John", + priority: "high", + }, + { + id: "f2", + title: "Transaction processing", + description: "Test deposit, withdrawal, and strategy execution", + status: "in-progress", + assignee: "Sarah", + priority: "high", + }, + { + id: "f3", + title: "Data persistence", + description: "Verify localStorage and API data sync", + status: "pending", + assignee: "Mike", + priority: "medium", + }, + { + id: "f4", + title: "Error handling", + description: "Test error states and user feedback", + status: "pending", + assignee: "John", + priority: "high", + }, + ], + }, + { + id: "visual", + title: "Visual Checks", + items: [ + { + id: "v1", + title: "Responsive design", + description: "Verify layout on mobile, tablet, and desktop", + status: "completed", + assignee: "Emma", + priority: "high", + }, + { + id: "v2", + title: "Color contrast", + description: "Check WCAG AA compliance for all text", + status: "in-progress", + assignee: "Emma", + priority: "medium", + }, + { + id: "v3", + title: "Animation performance", + description: "Verify smooth animations at 60fps", + status: "pending", + assignee: "Sarah", + priority: "low", + }, + ], + }, + ], + knownIssues: [ + { + id: "k1", + title: "Mobile menu animation stutter", + description: "Menu animation has slight stutter on older Android devices", + severity: "low", + workaround: "Use tap instead of swipe", + status: "open", + }, + ], + signOffs: [ + { role: "product", name: "", status: "pending", notes: "" }, + { role: "design", name: "", status: "pending", notes: "" }, + { role: "engineering", name: "", status: "pending", notes: "" }, + ], + }); + + const [editingItem, setEditingItem] = useState(null); + const [editingSignOff, setEditingSignOff] = useState(null); + + const updateItemStatus = (sectionId: string, itemId: string, status: ChecklistStatus) => { + setChecklist((prev) => ({ + ...prev, + sections: prev.sections.map((section) => + section.id === sectionId + ? { + ...section, + items: section.items.map((item) => + item.id === itemId ? { ...item, status } : item + ), + } + : section + ), + updatedAt: new Date().toISOString(), + })); + }; + + const updateItemAssignee = (sectionId: string, itemId: string, assignee: string) => { + setChecklist((prev) => ({ + ...prev, + sections: prev.sections.map((section) => + section.id === sectionId + ? { + ...section, + items: section.items.map((item) => + item.id === itemId ? { ...item, assignee } : item + ), + } + : section + ), + updatedAt: new Date().toISOString(), + })); + }; + + const updateSignOff = (role: "product" | "design" | "engineering", updates: Partial) => { + setChecklist((prev) => ({ + ...prev, + signOffs: prev.signOffs.map((signOff) => + signOff.role === role ? { ...signOff, ...updates } : signOff + ), + updatedAt: new Date().toISOString(), + })); + }; + + const exportSummary = () => { + const completedItems = checklist.sections.reduce( + (acc, section) => acc + section.items.filter((i) => i.status === "completed").length, + 0 + ); + const totalItems = checklist.sections.reduce((acc, section) => acc + section.items.length, 0); + const progress = Math.round((completedItems / totalItems) * 100); + + const summary = ` +# Release Checklist: ${checklist.title} +Version: ${checklist.version} +Last Updated: ${new Date(checklist.updatedAt).toLocaleString()} + +## Progress: ${progress}% (${completedItems}/${totalItems} items completed) + +${checklist.sections.map( + (section) => { + const sectionCompleted = section.items.filter((i) => i.status === "completed").length; + return ` +### ${section.title} (${sectionCompleted}/${section.items.length}) +${section.items.map((item) => `- [${item.status === "completed" ? "x" : " "}] ${item.title} (${item.assignee || "Unassigned"})`).join("\n")} +`; + } +).join("\n")} + +## Known Issues +${checklist.knownIssues.length === 0 ? "None" : checklist.knownIssues.map((issue) => `- **${issue.title}** (${issue.severity}): ${issue.description}`).join("\n")} + +## Sign-offs +${checklist.signOffs.map((signOff) => `- **${signOff.role.charAt(0).toUpperCase() + signOff.role.slice(1)}**: ${signOff.name || "Pending"} (${signOff.status})`).join("\n")} +`.trim(); + + const blob = new Blob([summary], { type: "text/markdown" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `release-checklist-${checklist.version}.md`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const completedCount = checklist.sections.reduce( + (acc, section) => acc + section.items.filter((i) => i.status === "completed").length, + 0 + ); + const totalCount = checklist.sections.reduce((acc, section) => acc + section.items.length, 0); + const progress = Math.round((completedCount / totalCount) * 100); + + return ( +
+
+ {/* Header */} +
+
+

{checklist.title}

+

{checklist.description}

+
+
+
+ Progress: + {progress}% +
+ +
+
+ + {/* Progress Bar */} +
+
+
+
+

+ {completedCount} of {totalCount} items completed +

+
+ + {/* Checklist Sections */} +
+ {checklist.sections.map((section) => ( +
+
+

{section.title}

+

+ {section.items.filter((i) => i.status === "completed").length} / {section.items.length} completed +

+
+
+ {section.items.map((item) => { + const config = statusConfig[item.status]; + const StatusIcon = config.icon; + return ( +
+
+
+
+ +
+

{item.title}

+ {item.description && ( +

{item.description}

+ )} +
+
+
+
+ +
+ Assignee: + {editingItem === item.id ? ( + { + updateItemAssignee(section.id, item.id, e.target.value); + setEditingItem(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + updateItemAssignee(section.id, item.id, e.currentTarget.value); + setEditingItem(null); + } + }} + autoFocus + className="bg-dark-900 border border-sky-500/50 rounded px-2 py-1 text-white text-xs w-24 focus:outline-none" + /> + ) : ( + + )} +
+
+
+
+ ); + })} +
+
+ ))} +
+ + {/* Known Issues */} +
+
+
+

Known Issues

+

+ {checklist.knownIssues.length} issue{checklist.knownIssues.length !== 1 ? "s" : ""} +

+
+
+
+ {checklist.knownIssues.map((issue) => ( +
+
+
+
+

{issue.title}

+ + {severityConfig[issue.severity].label} + +
+

{issue.description}

+ {issue.workaround && ( +
+

+ Workaround: {issue.workaround} +

+
+ )} +
+
+
+ ))} + {checklist.knownIssues.length === 0 && ( +
+ No known issues +
+ )} +
+
+ + {/* Sign-offs */} +
+
+

Sign-offs

+

+ Required approvals before release +

+
+
+ {checklist.signOffs.map((signOff) => ( +
+
+
+

{signOff.role}

+ {signOff.notes && ( +

{signOff.notes}

+ )} +
+
+ updateSignOff(signOff.role, { name: e.target.value })} + className="bg-dark-900 border border-white/10 rounded-lg px-3 py-2 text-white text-sm w-32 md:w-40 focus:outline-none focus:ring-2 focus:ring-sky-500/50" + /> + +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx index b1220dd..56bbb46 100644 --- a/src/components/ui/Switch.tsx +++ b/src/components/ui/Switch.tsx @@ -9,7 +9,7 @@ interface SwitchProps { export function Switch({ checked, onChange, label, disabled = false }: SwitchProps) { return ( -
); diff --git a/src/lib/release-checklist-types.ts b/src/lib/release-checklist-types.ts new file mode 100644 index 0000000..6589bf9 --- /dev/null +++ b/src/lib/release-checklist-types.ts @@ -0,0 +1,46 @@ +export type ChecklistStatus = "pending" | "in-progress" | "completed" | "blocked" | "skipped"; + +export interface ChecklistItem { + id: string; + title: string; + description?: string; + status: ChecklistStatus; + assignee?: string; + notes?: string; + priority?: "high" | "medium" | "low"; +} + +export interface ChecklistSection { + id: string; + title: string; + items: ChecklistItem[]; +} + +export interface SignOff { + role: "product" | "design" | "engineering"; + name: string; + signedAt?: string; + status: "pending" | "approved" | "rejected"; + notes?: string; +} + +export interface ReleaseChecklist { + id: string; + version: string; + title: string; + description: string; + createdAt: string; + updatedAt: string; + sections: ChecklistSection[]; + knownIssues: KnownIssue[]; + signOffs: SignOff[]; +} + +export interface KnownIssue { + id: string; + title: string; + description: string; + severity: "critical" | "high" | "medium" | "low"; + workaround?: string; + status: "open" | "in-progress" | "resolved" | "deferred"; +} diff --git a/src/lib/service-layer/README.md b/src/lib/service-layer/README.md new file mode 100644 index 0000000..c9ed158 --- /dev/null +++ b/src/lib/service-layer/README.md @@ -0,0 +1,337 @@ +# Service Layer - Adapter Contract Documentation + +This document describes the adapter pattern for integrating the mock service layer with a real backend API. + +## Overview + +The service layer follows an adapter pattern that allows easy swapping between mock implementations and real backend services. All services extend the `BaseAdapter` class which provides: + +- Retry logic with exponential backoff +- Simulated latency (configurable) +- Simulated failure scenarios (configurable) +- Centralized error handling +- Request/response logging + +## Architecture + +``` +BaseAdapter (abstract) + ├── AuthService + ├── PortfolioService + ├── StrategyService + └── TransactionService +``` + +## Adapter Contract + +### BaseAdapter Interface + +All service adapters must extend `BaseAdapter` and implement the following pattern: + +```typescript +class MyService extends BaseAdapter { + constructor() { + super(config); + } + + async myMethod(params: T): Promise> { + return this.executeWithRetry(async () => { + // Implementation logic + return this.createResponse(data); + }, "MyService.myMethod"); + } +} +``` + +### Required Methods + +Each service adapter should implement: + +1. **CRUD Operations** - Create, Read, Update, Delete as appropriate +2. **Error Handling** - Use `this.handleError()` for consistent error responses +3. **Response Format** - Use `this.createResponse()` for consistent response structure + +### Response Format + +All services return `ServiceResponse`: + +```typescript +interface ServiceResponse { + data: T; + meta?: { + requestId: string; + timestamp: string; + version?: string; + }; +} +``` + +### Error Handling + +All errors are thrown as `ServiceException` with standardized error codes: + +```typescript +type ServiceErrorCode = + | "NETWORK_ERROR" + | "TIMEOUT" + | "UNAUTHORIZED" + | "FORBIDDEN" + | "NOT_FOUND" + | "VALIDATION_ERROR" + | "SERVER_ERROR" + | "UNKNOWN_ERROR"; +``` + +## Backend Integration Guide + +### Step 1: Create Real Service Implementation + +Create a new service class that extends `BaseAdapter` but makes real API calls: + +```typescript +import { BaseAdapter } from "./base-adapter"; +import { ServiceResponse } from "./types"; + +class RealAuthService extends BaseAdapter { + private apiClient: ApiClient; // Your HTTP client + + constructor(apiClient: ApiClient) { + super({ simulateLatency: false, simulateFailure: false }); + this.apiClient = apiClient; + } + + async login(credentials: LoginCredentials): Promise> { + return this.executeWithRetry(async () => { + const response = await this.apiClient.post('/auth/login', credentials); + return this.createResponse(response.data); + }, "RealAuthService.login"); + } +} +``` + +### Step 2: Configure Service Switching + +Create a service factory to switch between mock and real implementations: + +```typescript +// lib/service-layer/factory.ts +const USE_MOCK_SERVICES = process.env.NEXT_PUBLIC_USE_MOCK_SERVICES === 'true'; + +export const authService = USE_MOCK_SERVICES + ? new AuthService() + : new RealAuthService(apiClient); + +export const portfolioService = USE_MOCK_SERVICES + ? new PortfolioService() + : new RealPortfolioService(apiClient); +``` + +### Step 3: Update Environment Variables + +Add to `.env`: + +```env +NEXT_PUBLIC_USE_MOCK_SERVICES=false +NEXT_PUBLIC_API_BASE_URL=https://api.neurowealth.app +``` + +### Step 4: API Client Configuration + +Configure your HTTP client (e.g., axios, fetch) with: + +- Base URL from environment +- Authentication headers +- Request/response interceptors +- Timeout handling + +```typescript +const apiClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + timeout: 10000, +}); + +apiClient.interceptors.request.use((config) => { + // Add auth token + const token = getAuthToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + // Handle API errors + throw new ServiceException({ + code: mapApiErrorToCode(error), + message: error.message, + timestamp: new Date().toISOString(), + }); + } +); +``` + +## Service-Specific Contracts + +### AuthService + +**Methods:** +- `login(credentials: LoginCredentials)` - Authenticate user +- `signup(credentials: SignupCredentials)` - Register new user +- `logout(token: string)` - End user session +- `refreshToken(refreshToken: string)` - Refresh access token +- `verifyEmail(token: string)` - Verify user email +- `resetPassword(email: string)` - Initiate password reset + +**API Endpoints (for backend):** +- `POST /auth/login` +- `POST /auth/signup` +- `POST /auth/logout` +- `POST /auth/refresh` +- `POST /auth/verify-email` +- `POST /auth/reset-password` + +### PortfolioService + +**Methods:** +- `getPortfolio(userId: string)` - Get user portfolio +- `getPortfolioHistory(userId, params)` - Get portfolio history with pagination +- `updatePortfolio(userId: string)` - Refresh portfolio data +- `addAsset(userId, asset)` - Add asset to portfolio +- `removeAsset(userId, assetId)` - Remove asset from portfolio + +**API Endpoints (for backend):** +- `GET /portfolios/:userId` +- `GET /portfolios/:userId/history` +- `PUT /portfolios/:userId` +- `POST /portfolios/:userId/assets` +- `DELETE /portfolios/:userId/assets/:assetId` + +### StrategyService + +**Methods:** +- `getStrategies()` - List available strategies +- `getStrategy(strategyId: string)` - Get strategy details +- `getUserAllocations(userId, params)` - Get user's strategy allocations +- `createAllocation(userId, strategyId, amount)` - Allocate to strategy +- `cancelAllocation(userId, allocationId)` - Cancel allocation +- `getStrategyPerformance(strategyId, params)` - Get strategy performance + +**API Endpoints (for backend):** +- `GET /strategies` +- `GET /strategies/:strategyId` +- `GET /users/:userId/allocations` +- `POST /users/:userId/allocations` +- `DELETE /users/:userId/allocations/:allocationId` +- `GET /strategies/:strategyId/performance` + +### TransactionService + +**Methods:** +- `createTransaction(userId, params)` - Create new transaction +- `getTransaction(userId, transactionId)` - Get transaction details +- `getUserTransactions(userId, params)` - List user transactions with filters +- `cancelTransaction(userId, transactionId)` - Cancel pending transaction +- `getTransactionStats(userId)` - Get transaction statistics + +**API Endpoints (for backend):** +- `POST /transactions` +- `GET /transactions/:transactionId` +- `GET /users/:userId/transactions` +- `DELETE /transactions/:transactionId` +- `GET /users/:userId/transactions/stats` + +## Configuration + +### Service Config Options + +```typescript +interface ServiceConfig { + baseUrl?: string; + timeout?: number; + retryAttempts?: number; + retryDelay?: number; + simulateLatency?: boolean; + latencyRange?: [number, number]; + simulateFailure?: boolean; + failureRate?: number; +} +``` + +### Example Configuration + +```typescript +// Development (mock) +const devConfig = { + simulateLatency: true, + latencyRange: [200, 800], + simulateFailure: false, +}; + +// Production (real) +const prodConfig = { + simulateLatency: false, + simulateFailure: false, + baseUrl: 'https://api.neurowealth.app', + timeout: 10000, + retryAttempts: 3, +}; +``` + +## Testing + +### Unit Testing + +Mock services can be tested directly: + +```typescript +import { authService } from '@/lib/service-layer/auth-service'; + +describe('AuthService', () => { + it('should login successfully', async () => { + const result = await authService.login({ + email: 'user@example.com', + password: 'password', + }); + expect(result.data.user.email).toBe('user@example.com'); + }); +}); +``` + +### Integration Testing + +Test with real API by switching to real services: + +```typescript +describe('AuthService Integration', () => { + beforeAll(() => { + process.env.NEXT_PUBLIC_USE_MOCK_SERVICES = 'false'; + }); + + it('should login with real API', async () => { + // Test real API integration + }); +}); +``` + +## Migration Checklist + +- [ ] Create real service implementations +- [ ] Configure API client +- [ ] Set up environment variables +- [ ] Implement service factory +- [ ] Add API error mapping +- [ ] Update authentication flow +- [ ] Test with mock services disabled +- [ ] Test retry logic with real network failures +- [ ] Update documentation +- [ ] Monitor error rates in production + +## Notes + +- Mock services use in-memory storage; data is lost on refresh +- Real services should implement proper caching strategies +- Consider implementing request deduplication for concurrent requests +- Add proper logging for debugging in production +- Monitor API response times and adjust retry logic accordingly diff --git a/src/lib/service-layer/auth-service.ts b/src/lib/service-layer/auth-service.ts new file mode 100644 index 0000000..2c453e7 --- /dev/null +++ b/src/lib/service-layer/auth-service.ts @@ -0,0 +1,167 @@ +import { BaseAdapter } from "./base-adapter"; +import { ServiceResponse } from "./types"; + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface SignupCredentials { + email: string; + password: string; + fullName: string; +} + +export interface AuthUser { + id: string; + email: string; + fullName: string; + createdAt: string; + isVerified: boolean; +} + +export interface AuthSession { + user: AuthUser; + token: string; + refreshToken: string; + expiresAt: string; +} + +export class AuthService extends BaseAdapter { + private mockUsers: Map = new Map(); + private mockSessions: Map = new Map(); + + constructor() { + super(); + this.initializeMockData(); + } + + private initializeMockData(): void { + // Add some mock users for testing + this.mockUsers.set("user@example.com", { + id: "user_1", + email: "user@example.com", + fullName: "Test User", + createdAt: new Date().toISOString(), + isVerified: true, + }); + } + + async login(credentials: LoginCredentials): Promise> { + return this.executeWithRetry(async () => { + const user = this.mockUsers.get(credentials.email); + + if (!user) { + throw new Error("Invalid credentials"); + } + + if (!user.isVerified) { + throw new Error("Email not verified"); + } + + const session: AuthSession = { + user, + token: this.generateToken(), + refreshToken: this.generateToken(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }; + + this.mockSessions.set(session.token, session); + + return this.createResponse(session); + }, "AuthService.login"); + } + + async signup(credentials: SignupCredentials): Promise> { + return this.executeWithRetry(async () => { + if (this.mockUsers.has(credentials.email)) { + throw new Error("Email already registered"); + } + + const user: AuthUser = { + id: `user_${Date.now()}`, + email: credentials.email, + fullName: credentials.fullName, + createdAt: new Date().toISOString(), + isVerified: false, + }; + + this.mockUsers.set(credentials.email, user); + + const session: AuthSession = { + user, + token: this.generateToken(), + refreshToken: this.generateToken(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }; + + this.mockSessions.set(session.token, session); + + return this.createResponse(session); + }, "AuthService.signup"); + } + + async logout(token: string): Promise> { + return this.executeWithRetry(async () => { + this.mockSessions.delete(token); + return this.createResponse({ success: true }); + }, "AuthService.logout"); + } + + async refreshToken(refreshToken: string): Promise> { + return this.executeWithRetry(async () => { + const session = Array.from(this.mockSessions.values()).find( + (s) => s.refreshToken === refreshToken + ); + + if (!session) { + throw new Error("Invalid refresh token"); + } + + const newSession: AuthSession = { + ...session, + token: this.generateToken(), + refreshToken: this.generateToken(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }; + + this.mockSessions.delete(session.token); + this.mockSessions.set(newSession.token, newSession); + + return this.createResponse(newSession); + }, "AuthService.refreshToken"); + } + + async verifyEmail(token: string): Promise> { + return this.executeWithRetry(async () => { + const user = Array.from(this.mockUsers.values()).find((u) => u.id === token); + + if (!user) { + throw new Error("Invalid verification token"); + } + + user.isVerified = true; + this.mockUsers.set(user.email, user); + + return this.createResponse({ success: true }); + }, "AuthService.verifyEmail"); + } + + async resetPassword(email: string): Promise> { + return this.executeWithRetry(async () => { + if (!this.mockUsers.has(email)) { + throw new Error("Email not found"); + } + + // In a real implementation, this would send an email + return this.createResponse({ success: true }); + }, "AuthService.resetPassword"); + } + + private generateToken(): string { + return `token_${Date.now()}_${Math.random().toString(36).substr(2, 16)}`; + } +} + +// Singleton instance +export const authService = new AuthService(); diff --git a/src/lib/service-layer/base-adapter.ts b/src/lib/service-layer/base-adapter.ts new file mode 100644 index 0000000..9f5fd6b --- /dev/null +++ b/src/lib/service-layer/base-adapter.ts @@ -0,0 +1,148 @@ +import { + ServiceError, + ServiceResponse, + ServiceException, + ServiceConfig, + ServiceErrorCode, +} from "./types"; + +const DEFAULT_CONFIG: ServiceConfig = { + timeout: 10000, + retryAttempts: 3, + retryDelay: 1000, + simulateLatency: true, + latencyRange: [200, 800], + simulateFailure: false, + failureRate: 0.1, +}; + +function generateRequestId(): string { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +function randomInRange(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +async function simulateLatency(config: ServiceConfig): Promise { + if (!config.simulateLatency) return; + const [min, max] = config.latencyRange || DEFAULT_CONFIG.latencyRange!; + const delay = randomInRange(min, max); + await new Promise((resolve) => setTimeout(resolve, delay)); +} + +function shouldSimulateFailure(config: ServiceConfig): boolean { + if (!config.simulateFailure) return false; + const failureRate = config.failureRate || DEFAULT_CONFIG.failureRate!; + return Math.random() < failureRate; +} + +function createServiceError( + code: ServiceErrorCode, + message: string, + details?: any +): ServiceError { + return { + code, + message, + details, + timestamp: new Date().toISOString(), + requestId: generateRequestId(), + }; +} + +export abstract class BaseAdapter { + protected config: ServiceConfig; + + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + protected async executeWithRetry( + operation: () => Promise, + context: string + ): Promise { + const maxAttempts = this.config.retryAttempts || DEFAULT_CONFIG.retryAttempts!; + const retryDelay = this.config.retryDelay || DEFAULT_CONFIG.retryDelay!; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await simulateLatency(this.config); + + if (shouldSimulateFailure(this.config)) { + throw new ServiceException( + createServiceError( + "SERVER_ERROR", + `Simulated failure in ${context}`, + { attempt } + ) + ); + } + + const result = await operation(); + return result; + } catch (error) { + lastError = error as Error; + + if (attempt === maxAttempts) { + throw new ServiceException( + createServiceError( + "TIMEOUT", + `Operation failed after ${maxAttempts} attempts: ${context}`, + { originalError: lastError.message } + ) + ); + } + + // Exponential backoff + const backoffDelay = retryDelay * Math.pow(2, attempt - 1); + await new Promise((resolve) => setTimeout(resolve, backoffDelay)); + } + } + + throw lastError || new Error("Unknown error in executeWithRetry"); + } + + protected createResponse(data: T): ServiceResponse { + return { + data, + meta: { + requestId: generateRequestId(), + timestamp: new Date().toISOString(), + version: "1.0.0", + }, + }; + } + + protected handleError(error: any, context: string): never { + if (error instanceof ServiceException) { + throw error; + } + + const code: ServiceErrorCode = this.mapErrorToCode(error); + throw new ServiceException( + createServiceError(code, `Error in ${context}: ${error.message}`, { + originalError: error.message, + }) + ); + } + + private mapErrorToCode(error: any): ServiceErrorCode { + if (error.code === "NETWORK_ERROR") return "NETWORK_ERROR"; + if (error.code === "TIMEOUT") return "TIMEOUT"; + if (error.code === "UNAUTHORIZED") return "UNAUTHORIZED"; + if (error.code === "FORBIDDEN") return "FORBIDDEN"; + if (error.code === "NOT_FOUND") return "NOT_FOUND"; + if (error.code === "VALIDATION_ERROR") return "VALIDATION_ERROR"; + return "SERVER_ERROR"; + } + + public updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + public getConfig(): ServiceConfig { + return { ...this.config }; + } +} diff --git a/src/lib/service-layer/portfolio-service.ts b/src/lib/service-layer/portfolio-service.ts new file mode 100644 index 0000000..b4f166d --- /dev/null +++ b/src/lib/service-layer/portfolio-service.ts @@ -0,0 +1,205 @@ +import { BaseAdapter } from "./base-adapter"; +import { ServiceResponse, PaginatedResponse, PaginationParams } from "./types"; + +export interface Portfolio { + id: string; + userId: string; + totalValue: number; + totalValueChange24h: number; + totalValueChange24hPercent: number; + assets: Asset[]; + createdAt: string; + updatedAt: string; +} + +export interface Asset { + id: string; + symbol: string; + name: string; + balance: number; + value: number; + valueChange24h: number; + valueChange24hPercent: number; +} + +export interface PortfolioHistory { + date: string; + value: number; +} + +export class PortfolioService extends BaseAdapter { + private mockPortfolios: Map = new Map(); + private mockHistory: Map = new Map(); + + constructor() { + super(); + this.initializeMockData(); + } + + private initializeMockData(): void { + const mockPortfolio: Portfolio = { + id: "portfolio_1", + userId: "user_1", + totalValue: 15000.50, + totalValueChange24h: 250.75, + totalValueChange24hPercent: 1.7, + assets: [ + { + id: "asset_1", + symbol: "USDC", + name: "USD Coin", + balance: 10000, + value: 10000, + valueChange24h: 0, + valueChange24hPercent: 0, + }, + { + id: "asset_2", + symbol: "XLM", + name: "Stellar", + balance: 2500, + value: 5000.50, + valueChange24h: 250.75, + valueChange24hPercent: 5.3, + }, + ], + createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date().toISOString(), + }; + + this.mockPortfolios.set("user_1", mockPortfolio); + + // Generate mock history data + const history: PortfolioHistory[] = []; + for (let i = 30; i >= 0; i--) { + const date = new Date(Date.now() - i * 24 * 60 * 60 * 1000); + const value = 10000 + Math.random() * 5000; + history.push({ + date: date.toISOString(), + value, + }); + } + this.mockHistory.set("portfolio_1", history); + } + + async getPortfolio(userId: string): Promise> { + return this.executeWithRetry(async () => { + const portfolio = this.mockPortfolios.get(userId); + + if (!portfolio) { + throw new Error("Portfolio not found"); + } + + return this.createResponse(portfolio); + }, "PortfolioService.getPortfolio"); + } + + async getPortfolioHistory( + userId: string, + params: PaginationParams + ): Promise>> { + return this.executeWithRetry(async () => { + const history = this.mockHistory.get(`portfolio_${userId.replace("user_", "")}`); + + if (!history) { + throw new Error("Portfolio history not found"); + } + + const startIndex = (params.page - 1) * params.limit; + const endIndex = startIndex + params.limit; + const paginatedItems = history.slice(startIndex, endIndex); + + return this.createResponse({ + items: paginatedItems, + total: history.length, + page: params.page, + limit: params.limit, + hasMore: endIndex < history.length, + }); + }, "PortfolioService.getPortfolioHistory"); + } + + async updatePortfolio(userId: string): Promise> { + return this.executeWithRetry(async () => { + const portfolio = this.mockPortfolios.get(userId); + + if (!portfolio) { + throw new Error("Portfolio not found"); + } + + // Simulate value changes + portfolio.totalValue = portfolio.totalValue * (1 + (Math.random() - 0.5) * 0.02); + portfolio.totalValueChange24h = portfolio.totalValue * (Math.random() - 0.5) * 0.05; + portfolio.totalValueChange24hPercent = (portfolio.totalValueChange24h / portfolio.totalValue) * 100; + portfolio.updatedAt = new Date().toISOString(); + + // Update asset values + portfolio.assets = portfolio.assets.map((asset) => ({ + ...asset, + value: asset.value * (1 + (Math.random() - 0.5) * 0.03), + valueChange24h: asset.value * (Math.random() - 0.5) * 0.05, + valueChange24hPercent: (asset.valueChange24h / asset.value) * 100, + })); + + this.mockPortfolios.set(userId, portfolio); + + return this.createResponse(portfolio); + }, "PortfolioService.updatePortfolio"); + } + + async addAsset( + userId: string, + asset: Omit + ): Promise> { + return this.executeWithRetry(async () => { + const portfolio = this.mockPortfolios.get(userId); + + if (!portfolio) { + throw new Error("Portfolio not found"); + } + + const newAsset: Asset = { + ...asset, + id: `asset_${Date.now()}`, + value: asset.balance * 1, // Simplified valuation + valueChange24h: 0, + valueChange24hPercent: 0, + }; + + portfolio.assets.push(newAsset); + portfolio.totalValue = portfolio.assets.reduce((sum, a) => sum + a.value, 0); + portfolio.updatedAt = new Date().toISOString(); + + this.mockPortfolios.set(userId, portfolio); + + return this.createResponse(portfolio); + }, "PortfolioService.addAsset"); + } + + async removeAsset(userId: string, assetId: string): Promise> { + return this.executeWithRetry(async () => { + const portfolio = this.mockPortfolios.get(userId); + + if (!portfolio) { + throw new Error("Portfolio not found"); + } + + const assetIndex = portfolio.assets.findIndex((a) => a.id === assetId); + + if (assetIndex === -1) { + throw new Error("Asset not found"); + } + + portfolio.assets.splice(assetIndex, 1); + portfolio.totalValue = portfolio.assets.reduce((sum, a) => sum + a.value, 0); + portfolio.updatedAt = new Date().toISOString(); + + this.mockPortfolios.set(userId, portfolio); + + return this.createResponse(portfolio); + }, "PortfolioService.removeAsset"); + } +} + +// Singleton instance +export const portfolioService = new PortfolioService(); diff --git a/src/lib/service-layer/strategy-service.ts b/src/lib/service-layer/strategy-service.ts new file mode 100644 index 0000000..5778c6c --- /dev/null +++ b/src/lib/service-layer/strategy-service.ts @@ -0,0 +1,260 @@ +import { BaseAdapter } from "./base-adapter"; +import { ServiceResponse, PaginatedResponse, PaginationParams } from "./types"; + +export interface Strategy { + id: string; + name: string; + description: string; + riskLevel: "low" | "medium" | "high"; + expectedApy: number; + minDeposit: number; + maxDeposit: number; + lockPeriod: number; // in days + isActive: boolean; + createdAt: string; +} + +export interface StrategyAllocation { + id: string; + userId: string; + strategyId: string; + amount: number; + status: "active" | "pending" | "completed" | "cancelled"; + startedAt: string; + endsAt?: string; + expectedReturn: number; + actualReturn?: number; +} + +export interface StrategyPerformance { + strategyId: string; + date: string; + apy: number; + totalValue: number; +} + +export class StrategyService extends BaseAdapter { + private mockStrategies: Map = new Map(); + private mockAllocations: Map = new Map(); + private mockPerformance: Map = new Map(); + + constructor() { + super(); + this.initializeMockData(); + } + + private initializeMockData(): void { + const strategies: Strategy[] = [ + { + id: "strategy_1", + name: "Conservative Yield", + description: "Low-risk strategy focusing on stable returns", + riskLevel: "low", + expectedApy: 5.5, + minDeposit: 100, + maxDeposit: 50000, + lockPeriod: 30, + isActive: true, + createdAt: new Date().toISOString(), + }, + { + id: "strategy_2", + name: "Balanced Growth", + description: "Moderate risk with balanced growth potential", + riskLevel: "medium", + expectedApy: 12.5, + minDeposit: 500, + maxDeposit: 100000, + lockPeriod: 90, + isActive: true, + createdAt: new Date().toISOString(), + }, + { + id: "strategy_3", + name: "Aggressive Yield", + description: "High-risk strategy for maximum returns", + riskLevel: "high", + expectedApy: 25.0, + minDeposit: 1000, + maxDeposit: 500000, + lockPeriod: 180, + isActive: true, + createdAt: new Date().toISOString(), + }, + ]; + + strategies.forEach((strategy) => this.mockStrategies.set(strategy.id, strategy)); + + // Mock allocations for user_1 + const allocations: StrategyAllocation[] = [ + { + id: "alloc_1", + userId: "user_1", + strategyId: "strategy_1", + amount: 5000, + status: "active", + startedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), + endsAt: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), + expectedReturn: 5000 * 0.055 * (30 / 365), + actualReturn: 5000 * 0.055 * (15 / 365), + }, + ]; + + this.mockAllocations.set("user_1", allocations); + + // Generate mock performance data + strategies.forEach((strategy) => { + const performance: StrategyPerformance[] = []; + for (let i = 30; i >= 0; i--) { + const date = new Date(Date.now() - i * 24 * 60 * 60 * 1000); + const apy = strategy.expectedApy * (1 + (Math.random() - 0.5) * 0.2); + const totalValue = 10000 * (1 + apy / 100 * (30 - i) / 365); + performance.push({ + strategyId: strategy.id, + date: date.toISOString(), + apy, + totalValue, + }); + } + this.mockPerformance.set(strategy.id, performance); + }); + } + + async getStrategies(): Promise> { + return this.executeWithRetry(async () => { + const strategies = Array.from(this.mockStrategies.values()).filter( + (s) => s.isActive + ); + return this.createResponse(strategies); + }, "StrategyService.getStrategies"); + } + + async getStrategy(strategyId: string): Promise> { + return this.executeWithRetry(async () => { + const strategy = this.mockStrategies.get(strategyId); + + if (!strategy) { + throw new Error("Strategy not found"); + } + + return this.createResponse(strategy); + }, "StrategyService.getStrategy"); + } + + async getUserAllocations( + userId: string, + params: PaginationParams + ): Promise>> { + return this.executeWithRetry(async () => { + const allocations = this.mockAllocations.get(userId) || []; + + const startIndex = (params.page - 1) * params.limit; + const endIndex = startIndex + params.limit; + const paginatedItems = allocations.slice(startIndex, endIndex); + + return this.createResponse({ + items: paginatedItems, + total: allocations.length, + page: params.page, + limit: params.limit, + hasMore: endIndex < allocations.length, + }); + }, "StrategyService.getUserAllocations"); + } + + async createAllocation( + userId: string, + strategyId: string, + amount: number + ): Promise> { + return this.executeWithRetry(async () => { + const strategy = this.mockStrategies.get(strategyId); + + if (!strategy) { + throw new Error("Strategy not found"); + } + + if (amount < strategy.minDeposit) { + throw new Error(`Minimum deposit is ${strategy.minDeposit}`); + } + + if (amount > strategy.maxDeposit) { + throw new Error(`Maximum deposit is ${strategy.maxDeposit}`); + } + + const allocation: StrategyAllocation = { + id: `alloc_${Date.now()}`, + userId, + strategyId, + amount, + status: "pending", + startedAt: new Date().toISOString(), + endsAt: new Date(Date.now() + strategy.lockPeriod * 24 * 60 * 60 * 1000).toISOString(), + expectedReturn: amount * (strategy.expectedApy / 100) * (strategy.lockPeriod / 365), + }; + + const userAllocations = this.mockAllocations.get(userId) || []; + userAllocations.push(allocation); + this.mockAllocations.set(userId, userAllocations); + + // Simulate activation after a delay + setTimeout(() => { + allocation.status = "active"; + this.mockAllocations.set(userId, userAllocations); + }, 2000); + + return this.createResponse(allocation); + }, "StrategyService.createAllocation"); + } + + async cancelAllocation( + userId: string, + allocationId: string + ): Promise> { + return this.executeWithRetry(async () => { + const allocations = this.mockAllocations.get(userId) || []; + const allocation = allocations.find((a) => a.id === allocationId); + + if (!allocation) { + throw new Error("Allocation not found"); + } + + if (allocation.status !== "pending" && allocation.status !== "active") { + throw new Error("Cannot cancel allocation in current state"); + } + + allocation.status = "cancelled"; + this.mockAllocations.set(userId, allocations); + + return this.createResponse(allocation); + }, "StrategyService.cancelAllocation"); + } + + async getStrategyPerformance( + strategyId: string, + params: PaginationParams + ): Promise>> { + return this.executeWithRetry(async () => { + const performance = this.mockPerformance.get(strategyId); + + if (!performance) { + throw new Error("Performance data not found"); + } + + const startIndex = (params.page - 1) * params.limit; + const endIndex = startIndex + params.limit; + const paginatedItems = performance.slice(startIndex, endIndex); + + return this.createResponse({ + items: paginatedItems, + total: performance.length, + page: params.page, + limit: params.limit, + hasMore: endIndex < performance.length, + }); + }, "StrategyService.getStrategyPerformance"); + } +} + +// Singleton instance +export const strategyService = new StrategyService(); diff --git a/src/lib/service-layer/transaction-service.ts b/src/lib/service-layer/transaction-service.ts new file mode 100644 index 0000000..82966d2 --- /dev/null +++ b/src/lib/service-layer/transaction-service.ts @@ -0,0 +1,275 @@ +import { BaseAdapter } from "./base-adapter"; +import { ServiceResponse, PaginatedResponse, PaginationParams } from "./types"; + +export interface Transaction { + id: string; + userId: string; + type: "deposit" | "withdrawal" | "transfer" | "strategy_allocation" | "strategy_return"; + amount: number; + asset: string; + status: "pending" | "processing" | "completed" | "failed" | "cancelled"; + fromAddress?: string; + toAddress?: string; + txHash?: string; + fee?: number; + createdAt: string; + completedAt?: string; + metadata?: Record; +} + +export interface CreateTransactionParams { + type: Transaction["type"]; + amount: number; + asset: string; + fromAddress?: string; + toAddress?: string; + metadata?: Record; +} + +export interface TransactionFilter { + type?: Transaction["type"]; + status?: Transaction["status"]; + asset?: string; + startDate?: string; + endDate?: string; +} + +export class TransactionService extends BaseAdapter { + private mockTransactions: Map = new Map(); + + constructor() { + super(); + this.initializeMockData(); + } + + private initializeMockData(): void { + const transactions: Transaction[] = [ + { + id: "tx_1", + userId: "user_1", + type: "deposit", + amount: 10000, + asset: "USDC", + status: "completed", + fromAddress: "GD...1234", + toAddress: "GA...5678", + txHash: "0x" + Math.random().toString(16).substr(2, 64), + fee: 0.01, + createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 + 5000).toISOString(), + }, + { + id: "tx_2", + userId: "user_1", + type: "strategy_allocation", + amount: 5000, + asset: "USDC", + status: "completed", + toAddress: "GA...9012", + txHash: "0x" + Math.random().toString(16).substr(2, 64), + fee: 0.02, + createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + completedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000 + 10000).toISOString(), + metadata: { strategyId: "strategy_1" }, + }, + { + id: "tx_3", + userId: "user_1", + type: "withdrawal", + amount: 500, + asset: "USDC", + status: "pending", + toAddress: "GD...3456", + fee: 0.015, + createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(), + }, + ]; + + this.mockTransactions.set("user_1", transactions); + } + + async createTransaction( + userId: string, + params: CreateTransactionParams + ): Promise> { + return this.executeWithRetry(async () => { + const transaction: Transaction = { + id: `tx_${Date.now()}`, + userId, + type: params.type, + amount: params.amount, + asset: params.asset, + status: "pending", + fromAddress: params.fromAddress, + toAddress: params.toAddress, + fee: this.calculateFee(params.type, params.amount), + createdAt: new Date().toISOString(), + metadata: params.metadata, + }; + + const userTransactions = this.mockTransactions.get(userId) || []; + userTransactions.unshift(transaction); + this.mockTransactions.set(userId, userTransactions); + + // Simulate transaction processing + this.simulateTransactionProcessing(userId, transaction.id); + + return this.createResponse(transaction); + }, "TransactionService.createTransaction"); + } + + async getTransaction( + userId: string, + transactionId: string + ): Promise> { + return this.executeWithRetry(async () => { + const transactions = this.mockTransactions.get(userId) || []; + const transaction = transactions.find((t) => t.id === transactionId); + + if (!transaction) { + throw new Error("Transaction not found"); + } + + return this.createResponse(transaction); + }, "TransactionService.getTransaction"); + } + + async getUserTransactions( + userId: string, + params: PaginationParams & { filter?: TransactionFilter } + ): Promise>> { + return this.executeWithRetry(async () => { + let transactions = this.mockTransactions.get(userId) || []; + + // Apply filters + if (params.filter) { + if (params.filter.type) { + transactions = transactions.filter((t) => t.type === params.filter!.type); + } + if (params.filter.status) { + transactions = transactions.filter((t) => t.status === params.filter!.status); + } + if (params.filter.asset) { + transactions = transactions.filter((t) => t.asset === params.filter!.asset); + } + if (params.filter.startDate) { + transactions = transactions.filter( + (t) => new Date(t.createdAt) >= new Date(params.filter!.startDate!) + ); + } + if (params.filter.endDate) { + transactions = transactions.filter( + (t) => new Date(t.createdAt) <= new Date(params.filter!.endDate!) + ); + } + } + + const startIndex = (params.page - 1) * params.limit; + const endIndex = startIndex + params.limit; + const paginatedItems = transactions.slice(startIndex, endIndex); + + return this.createResponse({ + items: paginatedItems, + total: transactions.length, + page: params.page, + limit: params.limit, + hasMore: endIndex < transactions.length, + }); + }, "TransactionService.getUserTransactions"); + } + + async cancelTransaction( + userId: string, + transactionId: string + ): Promise> { + return this.executeWithRetry(async () => { + const transactions = this.mockTransactions.get(userId) || []; + const transaction = transactions.find((t) => t.id === transactionId); + + if (!transaction) { + throw new Error("Transaction not found"); + } + + if (transaction.status !== "pending") { + throw new Error("Cannot cancel transaction in current state"); + } + + transaction.status = "cancelled"; + this.mockTransactions.set(userId, transactions); + + return this.createResponse(transaction); + }, "TransactionService.cancelTransaction"); + } + + async getTransactionStats( + userId: string + ): Promise> { + return this.executeWithRetry(async () => { + const transactions = this.mockTransactions.get(userId) || []; + + const stats = { + totalVolume: transactions + .filter((t) => t.status === "completed") + .reduce((sum, t) => sum + t.amount, 0), + totalTransactions: transactions.length, + completedTransactions: transactions.filter((t) => t.status === "completed").length, + pendingTransactions: transactions.filter((t) => t.status === "pending").length, + failedTransactions: transactions.filter((t) => t.status === "failed").length, + }; + + return this.createResponse(stats); + }, "TransactionService.getTransactionStats"); + } + + private calculateFee(type: Transaction["type"], amount: number): number { + // Simplified fee calculation + const baseFee = 0.01; + const percentageFee = amount * 0.001; + return Math.max(baseFee, percentageFee); + } + + private async simulateTransactionProcessing( + userId: string, + transactionId: string + ): Promise { + // Simulate processing delay + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const transactions = this.mockTransactions.get(userId) || []; + const transaction = transactions.find((t) => t.id === transactionId); + + if (!transaction || transaction.status !== "pending") { + return; + } + + // Simulate 90% success rate + const isSuccess = Math.random() < 0.9; + + transaction.status = isSuccess ? "processing" : "failed"; + this.mockTransactions.set(userId, transactions); + + if (isSuccess) { + // Simulate completion + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const updatedTransactions = this.mockTransactions.get(userId) || []; + const updatedTransaction = updatedTransactions.find((t) => t.id === transactionId); + + if (updatedTransaction) { + updatedTransaction.status = "completed"; + updatedTransaction.completedAt = new Date().toISOString(); + updatedTransaction.txHash = "0x" + Math.random().toString(16).substr(2, 64); + this.mockTransactions.set(userId, updatedTransactions); + } + } + } +} + +// Singleton instance +export const transactionService = new TransactionService(); diff --git a/src/lib/service-layer/types.ts b/src/lib/service-layer/types.ts new file mode 100644 index 0000000..2d557d5 --- /dev/null +++ b/src/lib/service-layer/types.ts @@ -0,0 +1,66 @@ +export type ServiceErrorCode = + | "NETWORK_ERROR" + | "TIMEOUT" + | "UNAUTHORIZED" + | "FORBIDDEN" + | "NOT_FOUND" + | "VALIDATION_ERROR" + | "SERVER_ERROR" + | "UNKNOWN_ERROR"; + +export interface ServiceError { + code: ServiceErrorCode; + message: string; + details?: T; + timestamp: string; + requestId?: string; +} + +export class ServiceException extends Error { + public readonly code: ServiceErrorCode; + public readonly details?: any; + public readonly timestamp: string; + public readonly requestId?: string; + + constructor(error: ServiceError) { + super(error.message); + this.name = "ServiceException"; + this.code = error.code; + this.details = error.details; + this.timestamp = error.timestamp; + this.requestId = error.requestId; + } +} + +export interface ServiceResponse { + data: T; + meta?: { + requestId: string; + timestamp: string; + version?: string; + }; +} + +export interface PaginationParams { + page: number; + limit: number; +} + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + limit: number; + hasMore: boolean; +} + +export interface ServiceConfig { + baseUrl?: string; + timeout?: number; + retryAttempts?: number; + retryDelay?: number; + simulateLatency?: boolean; + latencyRange?: [number, number]; + simulateFailure?: boolean; + failureRate?: number; +}