diff --git a/code-examples/individual-screening/README.md b/code-examples/individual-screening/README.md new file mode 100644 index 00000000..9e13e82a --- /dev/null +++ b/code-examples/individual-screening/README.md @@ -0,0 +1,86 @@ +# ComPilot Individual Screening Example + +This example demonstrates how to implement Individual screening using ComPilot's API. It includes: + +- Real-time Individual status updates via WebSocket +- Webhook handling with signature verification +- Frontend visualization of screening status +- Example implementation in TypeScript + +## Structure + +``` + ┌─────────────────┐ + │ │ + │ ComPilot API │ + │ │ + └────────┬────────┘ + │ + │ Webhooks + API Calls │ + ┌──────────────────┘ + │ + ▼ +┌────────────┐ ┌───────────────┐ ┌─────────────┐ +│ │ │ │ │ │ +│ Frontend │◄──►│ Backend │◄──►│ ngrok │ +│ │ │ (TypeScript) │ │ │ +└────────────┘ └───────────────┘ └─────────────┘ + ▲ ▲ + │ │ + └────────────────────┘ + WebSocket Updates +``` + +## Components + +### Frontend (Next.js) +- Provides the developer interface for testing individual screening +- Displays real-time status updates via WebSocket +- Shows detailed webhook logs +- Located in `/frontend` + +### Backend (TypeScript) +- Built with Express.js +- Uses WebSocket +- Located in `/backend-typescript` + +## Data Flow + +1. **Individual Submission** + - Frontend sends individual data to backend (`POST /workflows/{workflowID}/customers`) + - Backend forwards request to ComPilot API + - ComPilot returns 200 OK + - Frontend displays initial status + +2. **Status Updates** + - ComPilot sends webhook to backend via ngrok + - Backend broadcasts update via WebSocket + - Frontend updates UI in real-time + +## Setup Requirements + +1. **ComPilot Account** + - API Key + - Webhook Secret + - Individual Screening workflow + - CMS Project ID + +2. **Development Tools** + - Node.js + - ngrok (for webhook testing) + +## Getting Started + +1. Set up the backend following its README +2. Configure the frontend to point to your backend +3. Start both services + +For detailed setup instructions, see: +- [Frontend README](./frontend/README.md) +- [Backend TypeScript README](./backend-typescript/README.md) + +## Documentation + +- [ComPilot Documentation](https://docs.compilot.ai) +- [API Reference](https://docs.compilot.ai/developers/api) \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/.env.example b/code-examples/individual-screening/backend-typescript/.env.example new file mode 100644 index 00000000..a063c5a4 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/.env.example @@ -0,0 +1,16 @@ +# API Configuration +COMPILOT_API_URL=https://api.compilot.ai +COMPILOT_API_KEY= + +# Webhook Configuration +WEBHOOK_SECRET= + +# Workflow ID +COMPILOT_TMS_WORKFLOW_ID= + +# Server Configuration +PORT=3001 +NODE_ENV=development + +# Optional: If you need to specify a custom webhook URL for ComPilot to call back +WEBHOOK_URL= \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/.gitignore b/code-examples/individual-screening/backend-typescript/.gitignore new file mode 100644 index 00000000..8def6a46 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/.gitignore @@ -0,0 +1,20 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules/ +dist/ + +# environment variables +.env +.env.local + +# IDE +.idea/ +.vscode/ + +# package managers +yarn.lock +package-lock.json + +# misc +.DS_Store diff --git a/code-examples/individual-screening/backend-typescript/README.md b/code-examples/individual-screening/backend-typescript/README.md new file mode 100644 index 00000000..fb041d7f --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/README.md @@ -0,0 +1,113 @@ +ComPilot Customer Screening Example - Backend TypeScript +==================================================== + +This example demonstrates how to build a backend server that handles customer screening and status updates using ComPilot's API. It includes real-time updates via WebSocket and webhook processing. + +## Features + +- Express.js REST API for customer screening +- WebSocket server for real-time status updates +- Webhook handler with SVIX signature verification +- Support for Individual and Business customers +- Customer screening status tracking and broadcasting + +## Prerequisites + +- Access to ComPilot dashboard with an API key +- A customer screening workflow set up in your workspace +- Node.js and npm/yarn installed +- ngrok for webhook testing + +## Project Structure + +``` +src/ +├── controllers/ +│ ├── WebhookController.ts # Webhook handling and verification +│ └── customers.ts # Customer screening submission +├── services/ +│ └── compilot.ts # ComPilot API integration +├── routes/ +│ ├── customers.ts # Customer routes +│ └── webhookRoutes.ts # Webhook routes +├── websocket/ +│ └── index.ts # WebSocket server implementation +└── types/ + └── customer.ts # Type definitions +``` + +## Getting Started + +1. Install dependencies: +```bash +yarn install +``` + +2. Set up environment variables: +```bash +cp .env.example .env +``` + +3. Update `.env` with your configuration: +```env +# API Configuration +COMPILOT_API_URL=https://api.compilot.ai +COMPILOT_API_KEY=your_api_key + +# Webhook Configuration +WEBHOOK_SECRET=your_webhook_secret + +# Workflow ID +COMPILOT_SCREENING_WORKFLOW_ID=your_workflow_id + +# Server Configuration +PORT=8080 +NODE_ENV=development +``` + +4. Start ngrok tunnel: +```bash +ngrok http 8080 +``` + +5. Configure webhook URL in ComPilot dashboard with your ngrok URL + +6. Start the server: +```bash +yarn dev +``` + +## API Endpoints + +### Customer Screening +```http +POST /api/customers +Content-Type: application/json + +{ + "workspaceId": "your_workspace_id", + "organizationId": "your_organization_id", + "workflowId": "your_workflow_id" +} +``` + +### Webhook Endpoint +```http +POST / +``` +Handles ComPilot webhook events with SVIX signature verification. + +### WebSocket +``` +WS /ws +``` +Provides real-time customer screening status updates to connected clients. + +## Documentation + +- [ComPilot Documentation](https://docs.compilot.ai) +- [API Reference](https://docs.compilot.ai/developers/api) + +## Related + +- [Frontend Implementation](../frontend) \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/package.json b/code-examples/individual-screening/backend-typescript/package.json new file mode 100644 index 00000000..3901bfcd --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/package.json @@ -0,0 +1,38 @@ +{ + "name": "transaction-monitor-backend", + "version": "1.0.0", + "description": "Transaction monitoring example backend", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "nodemon src/index.ts", + "build": "tsc", + "lint": "eslint src/**/*.ts", + "format": "prettier --write \"src/**/*.ts\"" + }, + "dependencies": { + "@nexeraid/identity-schemas": "^2.72.0", + "axios": "^1.6.7", + "cors": "^2.8.5", + "dotenv": "^16.4.1", + "express": "^4.21.2", + "uuid": "^11.0.4", + "ws": "^8.18.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^20.11.16", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "nodemon": "^3.0.3", + "prettier": "^3.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/code-examples/individual-screening/backend-typescript/src/controllers/WebhookController.ts b/code-examples/individual-screening/backend-typescript/src/controllers/WebhookController.ts new file mode 100644 index 00000000..523e9d04 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/controllers/WebhookController.ts @@ -0,0 +1,102 @@ +import { Request, Response } from 'express'; +import { IWebSocketServer } from '../websocket'; +import crypto from 'crypto'; + +/** + * Controller handling ComPilot webhook events. + * This controller: + * 1. Receives webhook events from ComPilot + * 2. Verifies webhook signatures using SVIX + * 3. Broadcasts verified events to connected WebSocket clients + * + * Required environment variables: + * - WEBHOOK_SECRET: The webhook signing secret from ComPilot + */ +export class WebhookController { + private webhookSecret: string; + + constructor(private wsServer: IWebSocketServer) { + this.webhookSecret = process.env.WEBHOOK_SECRET || ''; + } + + /** + * Verifies the authenticity of incoming webhooks using SVIX signatures. + * + * @param payload - The webhook payload to verify + * @param headers - The request headers containing SVIX signature details + * @returns boolean - Whether the signature is valid + */ + private verifySignature(payload: any, headers: any): boolean { + try { + if (!this.webhookSecret) { + return true; // Skip verification if no secret is configured + } + + const svixId = headers['svix-id']; + const svixTimestamp = headers['svix-timestamp']; + const svixSignature = headers['svix-signature']; + + if (!svixId || !svixTimestamp || !svixSignature) { + return false; + } + + const message = Buffer.from(`${svixId}.${svixTimestamp}.${JSON.stringify(payload)}`); + const secretKey = this.webhookSecret.replace('whsec_', ''); + const secretBytes = Buffer.from(secretKey, 'base64'); + + const computedSignature = crypto + .createHmac('sha256', secretBytes) + .update(message) + .digest('base64'); + + const expectedSignature = svixSignature.split(',')[1]; + return crypto.timingSafeEqual( + Buffer.from(computedSignature), + Buffer.from(expectedSignature) + ); + } catch (error) { + console.error('❌ Signature verification failed:', error); + return false; + } + } + + /** + * Handles incoming webhook requests. + * 1. Logs the received webhook + * 2. Verifies the webhook signature + * 3. Broadcasts the webhook data to all connected clients + * + * Expected webhook format: + * { + * type: string, // e.g., 'transaction.updated' + * payload: { + * transactionId: string, + * status: string, + * ...other fields + * } + * } + */ + handleWebhook = (req: Request, res: Response): void => { + try { + console.log('📦 WEBHOOK RECEIVED:', { + eventType: req.body.eventType, + customerId: req.body.payload.customerId, + externalCustomerId: req.body.payload.externalCustomerId, + status: req.body.payload.status + }); + + if (!this.verifySignature(req.body, req.headers)) { + res.status(401).json({ error: 'Invalid signature' }); + return; + } + + // Broadcast to all connected clients + console.log('📤 Broadcasting webhook:', req.body); + this.wsServer.broadcast(req.body); + res.json({ received: true }); + } catch (error) { + console.error('❌ Webhook error:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/controllers/customers.ts b/code-examples/individual-screening/backend-typescript/src/controllers/customers.ts new file mode 100644 index 00000000..b4f3316e --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/controllers/customers.ts @@ -0,0 +1,76 @@ +import { Request, Response } from 'express'; +import { ComPilotService } from '../services/compilot'; +import { Customer } from '../types/customer'; + +/** + * Controller handling customer screening submissions to ComPilot. + * This controller: + * 1. Receives customer screening requests from clients + * 2. Validates and formats customer data + * 3. Submits screening requests to ComPilot API + * 4. Returns API responses to clients + * + * Endpoints: + * - POST /api/customers: Submit a new customer screening + */ +export class CustomerController { + /** + * Handles customer screening submission requests. + * 1. Validates the customer data + * 2. Submits to ComPilot API + * 3. Returns the API response + */ + static async submitCustomer(req: Request, res: Response) { + try { + const customer: Customer = req.body; + console.log('📨 Received customer request:', { + workspaceId: customer.workspaceId, + organizationId: customer.organizationId, + workflowId: customer.workflowId, + nationality: customer.customerPersonalInformation.nationality + }); + + const response = await ComPilotService.submitCustomerScreening(customer); + console.log('✅ Customer screening submitted successfully:', response); + res.json(response); + } catch (error) { + console.error('❌ Customer screening error:', error); + console.error('❌ Error stack:', error instanceof Error ? error.stack : 'No stack trace'); + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal server error' }); + } + } + } + + static async getWalletDetails(req: Request, res: Response) { + try { + const { customerId } = req.params; + const response = await ComPilotService.getCustomerWallets(customerId); + res.json(response); + } catch (error) { + console.error('❌ Get wallet details error:', error); + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal server error' }); + } + } + } + + static async getCustomerDetails(req: Request, res: Response) { + try { + const { customerId } = req.params; + const response = await ComPilotService.getCustomerDetails(customerId); + res.json(response); + } catch (error) { + console.error('❌ Get customer details error:', error); + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal server error' }); + } + } + } +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/index.ts b/code-examples/individual-screening/backend-typescript/src/index.ts new file mode 100644 index 00000000..370d9c47 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/index.ts @@ -0,0 +1,49 @@ +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import { createServer } from 'http'; +import { WebSocketServer } from './websocket'; + +import { WebhookController } from './controllers/WebhookController'; +import { createRouter } from './routes/index'; + +/** + * ComPilot Customer Screening System - TypescriptBackend Server + * + * This server provides: + * 1. REST API endpoints for Customer Screening + * 2. Webhook endpoints for receiving ComPilot updates + * 3. WebSocket server for real-time updates to clients + * + * Required environment variables: + * - PORT: Server port (default: 8080) + * - WEBHOOK_SECRET: ComPilot webhook signing secret + * - COMPILOT_API_URL: ComPilot API base URL + * - COMPILOT_API_KEY: ComPilot API key + */ + +const app = express(); +app.use(cors()); +app.use(express.json()); + +// Create HTTP server (required for WebSocket support) +const server = createServer(app); + +// Initialize WebSocket server for real-time updates +const wsServer = new WebSocketServer(server); + +// Root webhook endpoint +app.post('/', (req, res) => { + const webhookController = new WebhookController(wsServer); + return webhookController.handleWebhook(req, res); +}); + +// API routes +app.use('/api', createRouter(wsServer)); + +const PORT = process.env.PORT || 8080; +server.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); +}); + +export default app; \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/routes/customers.ts b/code-examples/individual-screening/backend-typescript/src/routes/customers.ts new file mode 100644 index 00000000..f2277a2e --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/routes/customers.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import { CustomerController } from '../controllers/customers'; + +/** + * Customer routes for the ComPilot Customer Screening System. + * Handles all customer screening-related endpoints. + * + * Available Routes: + * - POST /: Submit a new customer for screening + * - Request body should contain customer details + * - Returns screening response from ComPilot API + * - GET /:customerId/wallets: Get wallet details for a customer + * - GET /:customerId/details: Get customer details + * + * Note: These routes are mounted at /api/customers in index.ts + * So the full path would be /api/customers/ + */ +const router = Router(); + +router.post('/', CustomerController.submitCustomer); +router.get('/:customerId/wallets', CustomerController.getWalletDetails); +router.get('/:customerId/details', CustomerController.getCustomerDetails); + +export const customerRoutes = router; diff --git a/code-examples/individual-screening/backend-typescript/src/routes/index.ts b/code-examples/individual-screening/backend-typescript/src/routes/index.ts new file mode 100644 index 00000000..e767ae2a --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/routes/index.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import { customerRoutes } from './customers'; +import { createWebhookRoutes } from './webhookRoutes'; +import { IWebSocketServer } from '../websocket'; + +export function createRouter(wsServer: IWebSocketServer) { + const router = Router(); + + router.use('/customers', customerRoutes); + router.use('/webhooks', createWebhookRoutes(wsServer)); + + return router; +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/routes/webhookRoutes.ts b/code-examples/individual-screening/backend-typescript/src/routes/webhookRoutes.ts new file mode 100644 index 00000000..9217d498 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/routes/webhookRoutes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { WebhookController } from '../controllers/WebhookController'; +import { IWebSocketServer } from '../websocket'; + +/** + * Webhook routes for the ComPilot Customer Screening System. + * Handles incoming webhooks from ComPilot and broadcasts them via WebSocket. + + * @param wsServer - WebSocket server instance for broadcasting updates + * @returns Express Router configured with webhook endpoints + */ +export function createWebhookRoutes(wsServer: IWebSocketServer) { + const router = Router(); + const webhookController = new WebhookController(wsServer); + router.post('/', webhookController.handleWebhook); + return router; +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/services/compilot.ts b/code-examples/individual-screening/backend-typescript/src/services/compilot.ts new file mode 100644 index 00000000..539b7400 --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/services/compilot.ts @@ -0,0 +1,110 @@ +import { Customer, CustomerResponse } from '../types/customer'; + +/** + * Service for interacting with the ComPilot API. + * Handles transaction submissions and API communication. + * + * Required environment variables: + * - COMPILOT_API_URL: Base URL for the ComPilot API + * - COMPILOT_API_KEY: API key for authentication + * - COMPILOT_TMS_WORKFLOW_ID: Workflow ID for transaction monitoring + */ +export class ComPilotService { + /** + * Submits a individual screening request to the ComPilot API. + * + * @param customer - The individual details to submit for screening + * @throws Error if API credentials are missing or if the API request fails + * @returns The API response data + */ + static async submitCustomerScreening(customer: Customer): Promise { + console.log('📝 Environment variables:', { + apiUrl: process.env.COMPILOT_API_URL, + hasApiKey: !!process.env.COMPILOT_API_KEY, + workflowId: process.env.COMPILOT_SCREENING_WORKFLOW_ID, + apiKeyLength: process.env.COMPILOT_API_KEY?.length + }); + + console.log('🔑 API Key first 10 chars:', process.env.COMPILOT_API_KEY?.substring(0, 10)); + + if (!process.env.COMPILOT_API_URL) { + throw new Error('COMPILOT_API_URL is not defined'); + } + + if (!process.env.COMPILOT_SCREENING_WORKFLOW_ID) { + throw new Error('COMPILOT_SCREENING_WORKFLOW_ID is not defined'); + } + + const url = `${process.env.COMPILOT_API_URL}/workflows/${process.env.COMPILOT_SCREENING_WORKFLOW_ID}/customers`; + console.log('�� Request URL:', url); + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.COMPILOT_API_KEY}` + }; + console.log('📋 Request headers:', { + ...headers, + 'Authorization': headers.Authorization.substring(0, 20) + '...' + }); + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(customer) + }); + + const data = await response.json(); + console.log('📥 Response status:', response.status); + console.log('📦 Response data:', data); + + if (!response.ok) { + throw new Error(`ComPilot API Error: ${JSON.stringify(data, null, 2)}`); + } + + return data as CustomerResponse; + } + + static async getCustomerWallets(customerId: string) { + if (!process.env.COMPILOT_API_URL) { + throw new Error('COMPILOT_API_URL is not defined'); + } + + const url = `${process.env.COMPILOT_API_URL}/customers/${customerId}/wallets`; + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${process.env.COMPILOT_API_KEY}` + } + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(`ComPilot API Error: ${JSON.stringify(data, null, 2)}`); + } + + return data; + } + + static async getCustomerDetails(customerId: string) { + if (!process.env.COMPILOT_API_URL) { + throw new Error('COMPILOT_API_URL is not defined'); + } + + const url = `${process.env.COMPILOT_API_URL}/customers/${customerId}/details`; + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${process.env.COMPILOT_API_KEY}` + } + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(`ComPilot API Error: ${JSON.stringify(data, null, 2)}`); + } + + return data; + } +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/types/customer.ts b/code-examples/individual-screening/backend-typescript/src/types/customer.ts new file mode 100644 index 00000000..6e1c42ff --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/types/customer.ts @@ -0,0 +1,87 @@ +export interface CustomerPersonalInformation { + age: number; + nationality: string; + residence: string; +} + +export interface CustomerWallet { + wallet: string; + blockchainNamespace: string; + verified: boolean; + externalId?: string; +} + +export interface ContactInformation { + email: string; + phone: string; +} + +export interface Credential { + id: string; + "@context": string[]; + type: string[]; + expirationDate: string; + issuanceDate: string; + credentialSubject: { + id: string; + journeyId: string; + reviewAnswer: string; + reviewRejectType: string; + reviewRejectLabels: string[]; + documentType: string; + entryDate: number; + entryTime: string; + personalData: { + firstName: string; + lastName: string; + middleName?: string; + gender?: string; + age: number; + citizenship: string; + country: string; + fullName: string; + birthDate: number; + countryOfBirth: string; + stateOfBirth?: string; + }; + isSandbox: boolean; + }; + credentialStatus: { + id: string; + type: string; + revocationNonce: number; + }; + issuer: string; + credentialSchema: { + id: string; + type: string; + }; +} + +export interface CustomerPersonalData { + credentials: Credential[]; + requestIp: string; + address: string; + cmsProjectId: string; +} + +export interface Customer { + workspaceId: string; + organizationId: string; + workflowId: string; + externalId: string; + status: 'Active' | 'Inactive' | 'Failed' | 'Rejected' | 'Pending'; + onboardingLevel: 'Onboarded' | 'KYC'; + customerPersonalInformation: CustomerPersonalInformation; + customerData: Array<{ + customerWallet: CustomerWallet; + }>; + contactInformation: ContactInformation; + customerPersonalData: CustomerPersonalData; +} + +export interface CustomerResponse { + id: string; + status: 'active' | 'failed' | 'rejected' | 'under_review'; + message?: string; +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/src/websocket.ts b/code-examples/individual-screening/backend-typescript/src/websocket.ts new file mode 100644 index 00000000..c961278e --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/src/websocket.ts @@ -0,0 +1,56 @@ +import { WebSocket, WebSocketServer as WS } from 'ws'; +import { Server } from 'http'; + +/** + * Interface defining the WebSocket server capabilities. + * Any class implementing this interface must provide a broadcast method + * to send messages to all connected clients. + */ +export interface IWebSocketServer { + broadcast(data: any): void; +} + +/** + * WebSocket server implementation for customer screening updates. + * This server: + * 1. Maintains WebSocket connections with clients + * 2. Broadcasts webhook updates to all connected clients + * 3. Handles client connection/disconnection events + */ +export class WebSocketServer implements IWebSocketServer { + private wss: WS; + + constructor(server: Server) { + // Initialize WebSocket server using the existing HTTP server + this.wss = new WS({ server }); + this.setupWebSocket(); + } + + private setupWebSocket() { + this.wss.on('connection', (ws) => { + console.log('🔌 New WebSocket client connected'); + console.log('🔌 Number of clients:', this.wss.clients.size); + ws.on('close', () => { + console.log('🔌 Client disconnected'); + console.log('🔌 Number of clients:', this.wss.clients.size); + }); + }); + } + + /** + * Broadcasts a message to all connected WebSocket clients. + * Used to send webhook updates in real-time. + * + * @param data - The data to broadcast (will be JSON stringified) + */ + broadcast(data: any) { + console.log('🔊 About to broadcast:', data); + console.log('👥 Number of connected clients:', this.wss.clients.size); + console.log('📢 Broadcasting to', this.wss.clients.size, 'clients'); + this.wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(data)); + } + }); + } +} \ No newline at end of file diff --git a/code-examples/individual-screening/backend-typescript/tsconfig.json b/code-examples/individual-screening/backend-typescript/tsconfig.json new file mode 100644 index 00000000..5a904e1b --- /dev/null +++ b/code-examples/individual-screening/backend-typescript/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "lib": [ + "es2020" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/.env.example b/code-examples/individual-screening/frontend/.env.example new file mode 100644 index 00000000..d7414411 --- /dev/null +++ b/code-examples/individual-screening/frontend/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 +NEXT_PUBLIC_WS_URL=ws://localhost:8080 +NEXT_PUBLIC_CUSTOMER_ID=6ec8a3b2-8079-45f4-93da-27bb84bd90c4 \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/.gitignore b/code-examples/individual-screening/frontend/.gitignore new file mode 100644 index 00000000..c79c3ed4 --- /dev/null +++ b/code-examples/individual-screening/frontend/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# database +/prisma/db.sqlite +/prisma/db.sqlite-journal + +# next.js +/.next/ +/out/ +next-env.d.ts + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# package managers +yarn.lock +package-lock.json diff --git a/code-examples/individual-screening/frontend/README.md b/code-examples/individual-screening/frontend/README.md new file mode 100644 index 00000000..0d98c373 --- /dev/null +++ b/code-examples/individual-screening/frontend/README.md @@ -0,0 +1,94 @@ +ComPilot Customer Screening Example - Frontend +=============================================== + +This example demonstrates how to build a customer screening interface using Next.js and ComPilot's API. It provides a developer-friendly environment to test and monitor customer screening in real-time. + +## Features + +- Real-time customer screening status monitoring via WebSocket +- Developer mode for testing screening flows +- Detailed webhook logs and status visualization + +## Prerequisites + +- Access to ComPilot dashboard +- A customer screening workflow set up in your workspace +- Node.js and npm/yarn installed +- Backend server running + +## Getting Started + +1. Install dependencies: +```bash +yarn install +``` + +2. Set up environment variables: +```bash +cp .env.example .env.local +``` + +3. Update `.env.local` with your configuration: +```env +# Backend URLs +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 +NEXT_PUBLIC_WS_URL=ws://localhost:8080 + +# ComPilot Configuration +NEXT_PUBLIC_WORKFLOW_ID= +NEXT_PUBLIC_CMS_PROJECT_ID= +``` + +4. Start the development server: +```bash +yarn dev +``` + +5. Open [http://localhost:3000](http://localhost:3000) with your browser + +## Project Structure + +``` +src/ +├── app/ +│ └── page.tsx # Main application page +├── components/ +│ ├── CustomerScreeningInspector.tsx # Developer mode interface +│ └── lifecycleinspectorsection/ # Inspector components +│ ├── JsonEditorSection.tsx # JSON payload editor +│ ├── LogSection.tsx # Webhook logs display +│ ├── StatusDisplaySection.tsx # Status visualization +│ └── WebhookSection.tsx # Webhook monitoring +├── hooks/ +│ ├── useCustomerApi.ts # API integration +│ └── useCustomerWebSocket.ts # Real-time updates +├── lib/ +│ ├── customer-examples.ts # Customer templates +│ └── generators.ts # Test data generators +└── types/ + └── devmode.ts # Developer mode types +``` + +## Developer Mode Features + +### Customer Screening Inspector +- JSON editor for customer payload +- Real-time API response viewing +- Detailed webhook logs +- Screening status monitoring +- Test data generation + +### Webhook Monitoring +- Real-time webhook visualization +- Status change tracking +- Full webhook payload inspection +- Connection status monitoring + +## Documentation + +- [ComPilot Documentation](https://docs.compilot.ai) +- [API Reference](https://docs.compilot.ai/developers/api) + +## Related + +- [Backend TypeScript Implementation](../backend-typescript) \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/eslint.config.mjs b/code-examples/individual-screening/frontend/eslint.config.mjs new file mode 100644 index 00000000..c85fb67c --- /dev/null +++ b/code-examples/individual-screening/frontend/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/code-examples/individual-screening/frontend/next.config.ts b/code-examples/individual-screening/frontend/next.config.ts new file mode 100644 index 00000000..e9ffa308 --- /dev/null +++ b/code-examples/individual-screening/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/code-examples/individual-screening/frontend/package.json b/code-examples/individual-screening/frontend/package.json new file mode 100644 index 00000000..28ab7e8e --- /dev/null +++ b/code-examples/individual-screening/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@heroicons/react": "^2.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "next": "15.1.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.1.3", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/code-examples/individual-screening/frontend/postcss.config.mjs b/code-examples/individual-screening/frontend/postcss.config.mjs new file mode 100644 index 00000000..1a69fd2a --- /dev/null +++ b/code-examples/individual-screening/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/code-examples/individual-screening/frontend/public/file.svg b/code-examples/individual-screening/frontend/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/code-examples/individual-screening/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/public/globe.svg b/code-examples/individual-screening/frontend/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/code-examples/individual-screening/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/public/next.svg b/code-examples/individual-screening/frontend/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/code-examples/individual-screening/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/public/vercel.svg b/code-examples/individual-screening/frontend/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/code-examples/individual-screening/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/public/window.svg b/code-examples/individual-screening/frontend/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/code-examples/individual-screening/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/src/app/favicon.ico b/code-examples/individual-screening/frontend/src/app/favicon.ico new file mode 100644 index 00000000..5ce0abeb Binary files /dev/null and b/code-examples/individual-screening/frontend/src/app/favicon.ico differ diff --git a/code-examples/individual-screening/frontend/src/app/globals.css b/code-examples/individual-screening/frontend/src/app/globals.css new file mode 100644 index 00000000..6b717ad3 --- /dev/null +++ b/code-examples/individual-screening/frontend/src/app/globals.css @@ -0,0 +1,21 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/code-examples/individual-screening/frontend/src/app/layout.tsx b/code-examples/individual-screening/frontend/src/app/layout.tsx new file mode 100644 index 00000000..c58f83ce --- /dev/null +++ b/code-examples/individual-screening/frontend/src/app/layout.tsx @@ -0,0 +1,17 @@ +import './globals.css'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+ {children} +
+ + + ); +} diff --git a/code-examples/individual-screening/frontend/src/app/page.tsx b/code-examples/individual-screening/frontend/src/app/page.tsx new file mode 100644 index 00000000..e955e207 --- /dev/null +++ b/code-examples/individual-screening/frontend/src/app/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import CustomerScreeningInspector from '@/components/CustomerScreeningInspector'; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/code-examples/individual-screening/frontend/src/components/CustomerScreeningInspector.tsx b/code-examples/individual-screening/frontend/src/components/CustomerScreeningInspector.tsx new file mode 100644 index 00000000..e26d14ad --- /dev/null +++ b/code-examples/individual-screening/frontend/src/components/CustomerScreeningInspector.tsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { CustomerTypeSelectorSection } from './lifecycleinspectorsection/CustomerTypeSelectionSection'; +import { JsonEditorSection } from './lifecycleinspectorsection/JsonEditorSection'; +import { customerExamples } from '../lib/customer-examples'; +import { useCustomerApi, CustomerResponse } from '../hooks/useCustomerApi'; +import { useCustomerWebSocket } from '../hooks/useCustomerWebSocket'; +import { LogSection } from './lifecycleinspectorsection/LogSection'; +import { StatusDisplaySection } from './lifecycleinspectorsection/StatusDisplaySection'; + +const CustomerScreeningInspector = () => { + const [selectedCustomer, setSelectedCustomer] = useState( + JSON.stringify(customerExamples.nationality.eu.data, null, 2) + ); + const [jsonError, setJsonError] = useState(null); + const [apiResponse, setApiResponse] = useState(null); + const [externalId, setExternalId] = useState(undefined); + const { makeApiCall } = useCustomerApi(); + const { logs } = useCustomerWebSocket(externalId); + const [customerDetails, setCustomerDetails] = useState(null); + + const handleExampleSelect = (category: string, type: string) => { + if (category === 'nationality') { + if (type === 'eu') setSelectedCustomer(JSON.stringify(customerExamples.nationality.eu.data, null, 2)); + if (type === 'us') setSelectedCustomer(JSON.stringify(customerExamples.nationality.us.data, null, 2)); + } + if (category === 'age') { + if (type === 'over18') setSelectedCustomer(JSON.stringify(customerExamples.age.over18.data, null, 2)); + if (type === 'under18') setSelectedCustomer(JSON.stringify(customerExamples.age.under18.data, null, 2)); + } + if (category === 'walletRisk') { + if (type === 'low') setSelectedCustomer(JSON.stringify(customerExamples.walletRisk.low.data, null, 2)); + if (type === 'high') setSelectedCustomer(JSON.stringify(customerExamples.walletRisk.high.data, null, 2)); + } + if (category === 'amlHits') { + if (type === 'has') setSelectedCustomer(JSON.stringify(customerExamples.amlHits.has.data, null, 2)); + if (type === 'none') setSelectedCustomer(JSON.stringify(customerExamples.amlHits.none.data, null, 2)); + } + }; + + const handleJsonChange = (value: string) => { + setSelectedCustomer(value); + }; + + const handleSubmit = async () => { + try { + JSON.parse(selectedCustomer); + + const response = await makeApiCall(selectedCustomer); + setApiResponse(response); + const customerData = JSON.parse(selectedCustomer); + setExternalId(customerData.customerData[0].externalId); + setJsonError(null); + } catch (error) { + console.error('Error:', error); + setJsonError('Invalid JSON format'); + } + }; + + const handleGetCustomerDetails = async () => { + try { + const lastLog = logs[logs.length - 1]; + if (!lastLog?.details?.payload?.customerId) { + console.error('No customer ID available in webhook'); + return; + } + const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/customers/${lastLog.details.payload.customerId}/details`); + const data = await response.json(); + setCustomerDetails(data); + } catch (error) { + console.error('Error getting customer details:', error); + } + }; + + return ( +
+

Customer Screening Workflow

+
+
+
+ +
+ +
+ +
+
+ {apiResponse && } + {externalId && } +
+
+ ); +}; + +export default CustomerScreeningInspector; \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/CustomerTypeSelectionSection.tsx b/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/CustomerTypeSelectionSection.tsx new file mode 100644 index 00000000..401d2ce6 --- /dev/null +++ b/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/CustomerTypeSelectionSection.tsx @@ -0,0 +1,85 @@ +import { customerExamples } from '@/lib/customer-examples'; + +interface CustomerTypeSelectorSectionProps { + onSelect: (category: 'nationality' | 'age' | 'walletRisk' | 'amlHits', type: string) => void; +} + +export const CustomerTypeSelectorSection = ({ onSelect }: CustomerTypeSelectorSectionProps) => ( +
+
+
+

Nationality

+
+ + +
+
+ +
+

Age

+
+ + +
+
+ +
+

Wallet Risk

+
+ + +
+
+ +
+

AML Hits

+
+ + +
+
+
+
+); + +export default CustomerTypeSelectorSection; \ No newline at end of file diff --git a/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/JsonEditorSection.tsx b/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/JsonEditorSection.tsx new file mode 100644 index 00000000..e15740cf --- /dev/null +++ b/code-examples/individual-screening/frontend/src/components/lifecycleinspectorsection/JsonEditorSection.tsx @@ -0,0 +1,25 @@ +interface JsonEditorSectionProps { + value: string; + onChange: (value: string) => void; + error?: string | null; +} + +export const JsonEditorSection = ({ value, onChange, error }: JsonEditorSectionProps) => ( +
+
+
+ POST + /workflows/workflowID/customers +
+ {error && ( +
{error}
+ )} +