Phase 4: Customer Portal & Notifications
Person 3 Responsibility
Build the customer-facing portal with order management, multi-channel notifications, analytics, and delivery preferences.
Learning Objectives
- Implement Observer pattern for notifications
- Apply Template Method for notification templates
- Use Mediator pattern for routing
- Build Facade pattern for unified interface
- Create analytics with React Query and Recharts
Requirements
1. Observer Pattern - Multi-Channel Notifications
Create src/features/customer-portal/services/notification-observer.ts:
typescript1// Observer interface2export interface NotificationObserver {3 notify(notification: Notification): Promise<void>;4 getChannel(): NotificationChannel;5}67// Subject8export class NotificationSubject {9 private observers: Map<NotificationChannel, Set<NotificationObserver>> = new Map();1011 subscribe(channel: NotificationChannel, observer: NotificationObserver): void {12 if (!this.observers.has(channel)) {13 this.observers.set(channel, new Set());14 }1516 this.observers.get(channel)!.add(observer);17 }1819 unsubscribe(channel: NotificationChannel, observer: NotificationObserver): void {20 this.observers.get(channel)?.delete(observer);21 }2223 async notifyChannel(channel: NotificationChannel, notification: Notification): Promise<void> {24 const observers = this.observers.get(channel);2526 if (!observers || observers.size === 0) {27 console.warn(`No observers for channel: ${channel}`);28 return;29 }3031 const promises = Array.from(observers).map(observer =>32 observer.notify(notification)33 );3435 await Promise.allSettled(promises);36 }3738 async notifyAll(notification: Notification, channels: NotificationChannel[]): Promise<void> {39 const promises = channels.map(channel => this.notifyChannel(channel, notification));40 await Promise.allSettled(promises);41 }42}4344// Concrete observers45export class EmailNotificationObserver implements NotificationObserver {46 getChannel(): NotificationChannel {47 return NotificationChannel.EMAIL;48 }4950 async notify(notification: Notification): Promise<void> {51 console.log(`Sending email: ${notification.subject}`);52 // Mock email sending53 await new Promise(resolve => setTimeout(resolve, 100));54 }55}5657export class SMSNotificationObserver implements NotificationObserver {58 getChannel(): NotificationChannel {59 return NotificationChannel.SMS;60 }6162 async notify(notification: Notification): Promise<void> {63 console.log(`Sending SMS: ${notification.message}`);64 // Mock SMS sending65 await new Promise(resolve => setTimeout(resolve, 50));66 }67}6869export class PushNotificationObserver implements NotificationObserver {70 getChannel(): NotificationChannel {71 return NotificationChannel.PUSH;72 }7374 async notify(notification: Notification): Promise<void> {75 console.log(`Sending push notification: ${notification.subject}`);76 // Mock push notification77 await new Promise(resolve => setTimeout(resolve, 30));78 }79}8081export class InAppNotificationObserver implements NotificationObserver {82 getChannel(): NotificationChannel {83 return NotificationChannel.IN_APP;84 }8586 async notify(notification: Notification): Promise<void> {87 // Save to in-app notification store88 await saveInAppNotification(notification);89 }90}
2. Template Method - Notification Templates
Create src/features/customer-portal/services/notification-templates.ts:
typescript1// Abstract template2export abstract class NotificationTemplate {3 // Template method4 async createNotification(5 userId: string,6 shipmentId: string,7 data: Record<string, any>8 ): Promise<Notification> {9 const subject = this.createSubject(data);10 const message = this.createMessage(data);11 const type = this.getNotificationType();1213 return {14 id: crypto.randomUUID(),15 userId,16 shipmentId,17 type,18 channel: NotificationChannel.EMAIL, // Default, can be overridden19 subject,20 message,21 sentAt: new Date(),22 };23 }2425 // Primitive operations (to be implemented by subclasses)26 protected abstract createSubject(data: Record<string, any>): string;27 protected abstract createMessage(data: Record<string, any>): string;28 protected abstract getNotificationType(): NotificationType;2930 // Hook (optional override)31 protected shouldIncludeTrackingLink(): boolean {32 return true;33 }3435 protected formatTrackingLink(trackingNumber: string): string {36 return `https://example.com/track/${trackingNumber}`;37 }38}3940// Concrete templates41export class ShipmentCreatedTemplate extends NotificationTemplate {42 protected getNotificationType(): NotificationType {43 return NotificationType.SHIPMENT_CREATED;44 }4546 protected createSubject(data: Record<string, any>): string {47 return `Shipment Created - Order #${data.orderId}`;48 }4950 protected createMessage(data: Record<string, any>): string {51 const { orderId, trackingNumber, estimatedDelivery } = data;5253 return `54Your shipment has been created!5556Order ID: ${orderId}57Tracking Number: ${trackingNumber}58Estimated Delivery: ${estimatedDelivery}5960${this.shouldIncludeTrackingLink() ? `Track your shipment: ${this.formatTrackingLink(trackingNumber)}` : ''}61 `.trim();62 }63}6465export class StatusUpdateTemplate extends NotificationTemplate {66 protected getNotificationType(): NotificationType {67 return NotificationType.STATUS_UPDATE;68 }6970 protected createSubject(data: Record<string, any>): string {71 return `Shipment Update - ${data.status}`;72 }7374 protected createMessage(data: Record<string, any>): string {75 const { trackingNumber, status, location, timestamp } = data;7677 return `78Your shipment status has been updated!7980Tracking Number: ${trackingNumber}81Status: ${status}82Location: ${location}83Updated: ${new Date(timestamp).toLocaleString()}8485${this.shouldIncludeTrackingLink() ? `Track your shipment: ${this.formatTrackingLink(trackingNumber)}` : ''}86 `.trim();87 }88}8990export class DeliveryConfirmationTemplate extends NotificationTemplate {91 protected getNotificationType(): NotificationType {92 return NotificationType.DELIVERY_CONFIRMATION;93 }9495 protected createSubject(data: Record<string, any>): string {96 return `Package Delivered - Order #${data.orderId}`;97 }9899 protected createMessage(data: Record<string, any>): string {100 const { orderId, trackingNumber, deliveryTime, signedBy } = data;101102 return `103Your package has been delivered!104105Order ID: ${orderId}106Tracking Number: ${trackingNumber}107Delivered: ${new Date(deliveryTime).toLocaleString()}108${signedBy ? `Signed by: ${signedBy}` : 'Left at location'}109110Thank you for your business!111 `.trim();112 }113114 protected shouldIncludeTrackingLink(): boolean {115 return false; // No need for tracking link after delivery116 }117}118119export class ExceptionAlertTemplate extends NotificationTemplate {120 protected getNotificationType(): NotificationType {121 return NotificationType.EXCEPTION_ALERT;122 }123124 protected createSubject(data: Record<string, any>): string {125 return `Delivery Exception - Action Required`;126 }127128 protected createMessage(data: Record<string, any>): string {129 const { trackingNumber, exceptionType, description } = data;130131 return `132There's an issue with your shipment that requires attention.133134Tracking Number: ${trackingNumber}135Issue: ${exceptionType}136Details: ${description}137138Please contact customer service or update your delivery preferences.139140${this.shouldIncludeTrackingLink() ? `Manage shipment: ${this.formatTrackingLink(trackingNumber)}` : ''}141 `.trim();142 }143}
3. Mediator Pattern - Notification Routing
Create src/features/customer-portal/services/notification-mediator.ts:
typescript1// Mediator interface2export interface NotificationMediator {3 sendNotification(4 userId: string,5 shipmentId: string,6 type: NotificationType,7 data: Record<string, any>8 ): Promise<void>;9}1011// Concrete mediator12export class NotificationRoutingMediator implements NotificationMediator {13 private subject = new NotificationSubject();14 private templates: Map<NotificationType, NotificationTemplate>;1516 constructor() {17 // Register observers for each channel18 this.subject.subscribe(NotificationChannel.EMAIL, new EmailNotificationObserver());19 this.subject.subscribe(NotificationChannel.SMS, new SMSNotificationObserver());20 this.subject.subscribe(NotificationChannel.PUSH, new PushNotificationObserver());21 this.subject.subscribe(NotificationChannel.IN_APP, new InAppNotificationObserver());2223 // Register templates24 this.templates = new Map([25 [NotificationType.SHIPMENT_CREATED, new ShipmentCreatedTemplate()],26 [NotificationType.STATUS_UPDATE, new StatusUpdateTemplate()],27 [NotificationType.DELIVERY_CONFIRMATION, new DeliveryConfirmationTemplate()],28 [NotificationType.EXCEPTION_ALERT, new ExceptionAlertTemplate()],29 ]);30 }3132 async sendNotification(33 userId: string,34 shipmentId: string,35 type: NotificationType,36 data: Record<string, any>37 ): Promise<void> {38 // Get user preferences39 const preferences = await getUserPreferences(userId);4041 // Get template42 const template = this.templates.get(type);43 if (!template) {44 throw new Error(`No template found for type: ${type}`);45 }4647 // Create notification from template48 const notification = await template.createNotification(userId, shipmentId, data);4950 // Determine channels based on preferences and notification type51 const channels = this.determineChannels(type, preferences);5253 // Send via all channels54 await this.subject.notifyAll(notification, channels);5556 // Log notification57 await this.logNotification(notification, channels);58 }5960 private determineChannels(61 type: NotificationType,62 preferences: CustomerPreferences63 ): NotificationChannel[] {64 // Exception alerts go to all channels65 if (type === NotificationType.EXCEPTION_ALERT) {66 return [67 NotificationChannel.EMAIL,68 NotificationChannel.SMS,69 NotificationChannel.PUSH,70 NotificationChannel.IN_APP,71 ];72 }7374 // Otherwise, use user preferences75 return preferences.notificationChannels;76 }7778 private async logNotification(79 notification: Notification,80 channels: NotificationChannel[]81 ): Promise<void> {82 console.log(`Notification sent via ${channels.join(', ')}: ${notification.subject}`);83 // Save to database/logs84 }85}
4. Facade Pattern - Order Management
Create src/features/customer-portal/services/order-facade.ts:
typescript1// Facade providing unified interface2export class OrderManagementFacade {3 private shipmentService: ShipmentService;4 private trackingService: TrackingService;5 private notificationService: NotificationService;67 constructor() {8 this.shipmentService = new ShipmentService();9 this.trackingService = new TrackingService();10 this.notificationService = new NotificationService();11 }1213 // Unified method for complete order workflow14 async createOrder(15 userId: string,16 orderData: CreateOrderRequest17 ): Promise<OrderSummary> {18 try {19 // 1. Create shipment (calls Person 1's API)20 const shipment = await this.shipmentService.createShipment(orderData);2122 // 2. Initialize tracking (internal)23 await this.trackingService.initializeTracking(shipment.id);2425 // 3. Send confirmation notification26 await this.notificationService.sendShipmentCreated(userId, shipment);2728 // 4. Return unified summary29 return {30 orderId: shipment.orderId,31 trackingNumber: shipment.trackingNumber,32 estimatedDelivery: shipment.estimatedDelivery,33 cost: shipment.cost,34 status: shipment.status,35 };36 } catch (error) {37 // Handle errors from any subsystem38 await this.handleOrderCreationError(userId, error);39 throw error;40 }41 }4243 async getOrderHistory(44 userId: string,45 options?: { limit?: number; status?: ShipmentStatus }46 ): Promise<OrderSummary[]> {47 // 1. Get user's shipments48 const shipments = await this.shipmentService.getUserShipments(userId, options);4950 // 2. Get tracking info for each51 const trackingData = await Promise.all(52 shipments.map(s => this.trackingService.getLatestUpdate(s.id))53 );5455 // 3. Combine and format56 return shipments.map((shipment, index) => ({57 orderId: shipment.orderId,58 trackingNumber: shipment.trackingNumber,59 estimatedDelivery: shipment.estimatedDelivery,60 cost: shipment.cost,61 status: shipment.status,62 latestUpdate: trackingData[index],63 }));64 }6566 async reorder(userId: string, previousOrderId: string): Promise<OrderSummary> {67 // 1. Get previous order details68 const previousOrder = await this.shipmentService.getShipment(previousOrderId);6970 // 2. Create new order with same details71 const newOrderData = {72 package: previousOrder.package,73 origin: previousOrder.origin,74 destination: previousOrder.destination,75 // User can modify these in UI before confirming76 };7778 // 3. Create order through unified workflow79 return this.createOrder(userId, newOrderData);80 }8182 private async handleOrderCreationError(userId: string, error: any): Promise<void> {83 // Send error notification84 await this.notificationService.sendErrorAlert(userId, error.message);85 }86}
5. Analytics Dashboard
Create src/features/customer-portal/components/AnalyticsDashboard.tsx:
typescript1'use client'23import { useQuery } from '@tanstack/react-query';4import { BarChart, LineChart, PieChart } from 'recharts';56export function AnalyticsDashboard({ userId }: { userId: string }) {7 const { data: stats, isLoading } = useQuery({8 queryKey: ['analytics', userId],9 queryFn: () => fetchUserAnalytics(userId),10 });1112 if (isLoading) return <AnalyticsSkeletonLoader />;1314 return (15 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">16 {/* Shipment Volume Over Time */}17 <Card title="Shipment Volume">18 <LineChart data={stats.volumeOverTime} />19 </Card>2021 {/* Cost Analysis */}22 <Card title="Shipping Costs">23 <BarChart data={stats.costByMonth} />24 </Card>2526 {/* Carrier Distribution */}27 <Card title="Carrier Usage">28 <PieChart data={stats.carrierDistribution} />29 </Card>3031 {/* Delivery Performance */}32 <Card title="On-Time Delivery Rate">33 <MetricDisplay value={stats.onTimeRate} unit="%" />34 </Card>35 </div>36 );37}
6. EventBus Integration
Listen for STATUS_CHANGED events:
typescript1// Subscribe to status changes from Person 22eventBus.subscribe<ShipmentStatusChangedPayload>(3 EventType.SHIPMENT_STATUS_CHANGED,4 async (event) => {5 const { shipmentId, newStatus } = event.payload;67 // Get shipment details8 const shipment = await getShipment(shipmentId);910 // Determine notification type11 let notificationType: NotificationType;12 if (newStatus === ShipmentStatus.DELIVERED) {13 notificationType = NotificationType.DELIVERY_CONFIRMATION;14 } else if (newStatus === ShipmentStatus.EXCEPTION) {15 notificationType = NotificationType.EXCEPTION_ALERT;16 } else {17 notificationType = NotificationType.STATUS_UPDATE;18 }1920 // Send notification via mediator21 const mediator = new NotificationRoutingMediator();22 await mediator.sendNotification(23 shipment.createdBy,24 shipmentId,25 notificationType,26 {27 orderId: shipment.orderId,28 trackingNumber: shipment.trackingNumber,29 status: newStatus,30 timestamp: new Date(),31 }32 );33 }34);
Deliverables
- Customer portal UI with order history
- Observer pattern for notifications (4+ channels)
- Template Method (4+ templates)
- Mediator pattern for routing
- Facade pattern for order management
- Analytics dashboard with charts
- Delivery preferences management
- EventBus integration (listen to STATUS_CHANGED)
- Unit tests (70%+ coverage)
Testing Requirements
typescript1describe('NotificationObserver', () => {2 it('should send notifications to all channels', async () => {3 const subject = new NotificationSubject();4 const emailObserver = new EmailNotificationObserver();56 subject.subscribe(NotificationChannel.EMAIL, emailObserver);78 await subject.notifyChannel(NotificationChannel.EMAIL, mockNotification);9 // Verify notification sent10 });11});1213describe('NotificationTemplates', () => {14 it('should create formatted notification', async () => {15 const template = new ShipmentCreatedTemplate();16 const notification = await template.createNotification('user-1', 'ship-1', mockData);1718 expect(notification.subject).toContain('Order #');19 expect(notification.message).toContain('Tracking Number');20 });21});2223describe('OrderFacade', () => {24 it('should create order through unified workflow', async () => {25 const facade = new OrderManagementFacade();26 const order = await facade.createOrder('user-1', mockOrderData);2728 expect(order.orderId).toBeDefined();29 expect(order.trackingNumber).toBeDefined();30 });31});
Integration Points
Consumes from Person 2:
- STATUS_CHANGED events via EventBus
Consumes from Person 1:
- Shipment creation API (for reorder feature)
Estimated Time
20-25 hours for Person 3