60 minlesson

Phase 4: Customer Portal & Notifications

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:

typescript
1// Observer interface
2export interface NotificationObserver {
3 notify(notification: Notification): Promise<void>;
4 getChannel(): NotificationChannel;
5}
6
7// Subject
8export class NotificationSubject {
9 private observers: Map<NotificationChannel, Set<NotificationObserver>> = new Map();
10
11 subscribe(channel: NotificationChannel, observer: NotificationObserver): void {
12 if (!this.observers.has(channel)) {
13 this.observers.set(channel, new Set());
14 }
15
16 this.observers.get(channel)!.add(observer);
17 }
18
19 unsubscribe(channel: NotificationChannel, observer: NotificationObserver): void {
20 this.observers.get(channel)?.delete(observer);
21 }
22
23 async notifyChannel(channel: NotificationChannel, notification: Notification): Promise<void> {
24 const observers = this.observers.get(channel);
25
26 if (!observers || observers.size === 0) {
27 console.warn(`No observers for channel: ${channel}`);
28 return;
29 }
30
31 const promises = Array.from(observers).map(observer =>
32 observer.notify(notification)
33 );
34
35 await Promise.allSettled(promises);
36 }
37
38 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}
43
44// Concrete observers
45export class EmailNotificationObserver implements NotificationObserver {
46 getChannel(): NotificationChannel {
47 return NotificationChannel.EMAIL;
48 }
49
50 async notify(notification: Notification): Promise<void> {
51 console.log(`Sending email: ${notification.subject}`);
52 // Mock email sending
53 await new Promise(resolve => setTimeout(resolve, 100));
54 }
55}
56
57export class SMSNotificationObserver implements NotificationObserver {
58 getChannel(): NotificationChannel {
59 return NotificationChannel.SMS;
60 }
61
62 async notify(notification: Notification): Promise<void> {
63 console.log(`Sending SMS: ${notification.message}`);
64 // Mock SMS sending
65 await new Promise(resolve => setTimeout(resolve, 50));
66 }
67}
68
69export class PushNotificationObserver implements NotificationObserver {
70 getChannel(): NotificationChannel {
71 return NotificationChannel.PUSH;
72 }
73
74 async notify(notification: Notification): Promise<void> {
75 console.log(`Sending push notification: ${notification.subject}`);
76 // Mock push notification
77 await new Promise(resolve => setTimeout(resolve, 30));
78 }
79}
80
81export class InAppNotificationObserver implements NotificationObserver {
82 getChannel(): NotificationChannel {
83 return NotificationChannel.IN_APP;
84 }
85
86 async notify(notification: Notification): Promise<void> {
87 // Save to in-app notification store
88 await saveInAppNotification(notification);
89 }
90}

2. Template Method - Notification Templates

Create src/features/customer-portal/services/notification-templates.ts:

typescript
1// Abstract template
2export abstract class NotificationTemplate {
3 // Template method
4 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();
12
13 return {
14 id: crypto.randomUUID(),
15 userId,
16 shipmentId,
17 type,
18 channel: NotificationChannel.EMAIL, // Default, can be overridden
19 subject,
20 message,
21 sentAt: new Date(),
22 };
23 }
24
25 // 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;
29
30 // Hook (optional override)
31 protected shouldIncludeTrackingLink(): boolean {
32 return true;
33 }
34
35 protected formatTrackingLink(trackingNumber: string): string {
36 return `https://example.com/track/${trackingNumber}`;
37 }
38}
39
40// Concrete templates
41export class ShipmentCreatedTemplate extends NotificationTemplate {
42 protected getNotificationType(): NotificationType {
43 return NotificationType.SHIPMENT_CREATED;
44 }
45
46 protected createSubject(data: Record<string, any>): string {
47 return `Shipment Created - Order #${data.orderId}`;
48 }
49
50 protected createMessage(data: Record<string, any>): string {
51 const { orderId, trackingNumber, estimatedDelivery } = data;
52
53 return `
54Your shipment has been created!
55
56Order ID: ${orderId}
57Tracking Number: ${trackingNumber}
58Estimated Delivery: ${estimatedDelivery}
59
60${this.shouldIncludeTrackingLink() ? `Track your shipment: ${this.formatTrackingLink(trackingNumber)}` : ''}
61 `.trim();
62 }
63}
64
65export class StatusUpdateTemplate extends NotificationTemplate {
66 protected getNotificationType(): NotificationType {
67 return NotificationType.STATUS_UPDATE;
68 }
69
70 protected createSubject(data: Record<string, any>): string {
71 return `Shipment Update - ${data.status}`;
72 }
73
74 protected createMessage(data: Record<string, any>): string {
75 const { trackingNumber, status, location, timestamp } = data;
76
77 return `
78Your shipment status has been updated!
79
80Tracking Number: ${trackingNumber}
81Status: ${status}
82Location: ${location}
83Updated: ${new Date(timestamp).toLocaleString()}
84
85${this.shouldIncludeTrackingLink() ? `Track your shipment: ${this.formatTrackingLink(trackingNumber)}` : ''}
86 `.trim();
87 }
88}
89
90export class DeliveryConfirmationTemplate extends NotificationTemplate {
91 protected getNotificationType(): NotificationType {
92 return NotificationType.DELIVERY_CONFIRMATION;
93 }
94
95 protected createSubject(data: Record<string, any>): string {
96 return `Package Delivered - Order #${data.orderId}`;
97 }
98
99 protected createMessage(data: Record<string, any>): string {
100 const { orderId, trackingNumber, deliveryTime, signedBy } = data;
101
102 return `
103Your package has been delivered!
104
105Order ID: ${orderId}
106Tracking Number: ${trackingNumber}
107Delivered: ${new Date(deliveryTime).toLocaleString()}
108${signedBy ? `Signed by: ${signedBy}` : 'Left at location'}
109
110Thank you for your business!
111 `.trim();
112 }
113
114 protected shouldIncludeTrackingLink(): boolean {
115 return false; // No need for tracking link after delivery
116 }
117}
118
119export class ExceptionAlertTemplate extends NotificationTemplate {
120 protected getNotificationType(): NotificationType {
121 return NotificationType.EXCEPTION_ALERT;
122 }
123
124 protected createSubject(data: Record<string, any>): string {
125 return `Delivery Exception - Action Required`;
126 }
127
128 protected createMessage(data: Record<string, any>): string {
129 const { trackingNumber, exceptionType, description } = data;
130
131 return `
132There's an issue with your shipment that requires attention.
133
134Tracking Number: ${trackingNumber}
135Issue: ${exceptionType}
136Details: ${description}
137
138Please contact customer service or update your delivery preferences.
139
140${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:

typescript
1// Mediator interface
2export interface NotificationMediator {
3 sendNotification(
4 userId: string,
5 shipmentId: string,
6 type: NotificationType,
7 data: Record<string, any>
8 ): Promise<void>;
9}
10
11// Concrete mediator
12export class NotificationRoutingMediator implements NotificationMediator {
13 private subject = new NotificationSubject();
14 private templates: Map<NotificationType, NotificationTemplate>;
15
16 constructor() {
17 // Register observers for each channel
18 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());
22
23 // Register templates
24 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 }
31
32 async sendNotification(
33 userId: string,
34 shipmentId: string,
35 type: NotificationType,
36 data: Record<string, any>
37 ): Promise<void> {
38 // Get user preferences
39 const preferences = await getUserPreferences(userId);
40
41 // Get template
42 const template = this.templates.get(type);
43 if (!template) {
44 throw new Error(`No template found for type: ${type}`);
45 }
46
47 // Create notification from template
48 const notification = await template.createNotification(userId, shipmentId, data);
49
50 // Determine channels based on preferences and notification type
51 const channels = this.determineChannels(type, preferences);
52
53 // Send via all channels
54 await this.subject.notifyAll(notification, channels);
55
56 // Log notification
57 await this.logNotification(notification, channels);
58 }
59
60 private determineChannels(
61 type: NotificationType,
62 preferences: CustomerPreferences
63 ): NotificationChannel[] {
64 // Exception alerts go to all channels
65 if (type === NotificationType.EXCEPTION_ALERT) {
66 return [
67 NotificationChannel.EMAIL,
68 NotificationChannel.SMS,
69 NotificationChannel.PUSH,
70 NotificationChannel.IN_APP,
71 ];
72 }
73
74 // Otherwise, use user preferences
75 return preferences.notificationChannels;
76 }
77
78 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/logs
84 }
85}

4. Facade Pattern - Order Management

Create src/features/customer-portal/services/order-facade.ts:

typescript
1// Facade providing unified interface
2export class OrderManagementFacade {
3 private shipmentService: ShipmentService;
4 private trackingService: TrackingService;
5 private notificationService: NotificationService;
6
7 constructor() {
8 this.shipmentService = new ShipmentService();
9 this.trackingService = new TrackingService();
10 this.notificationService = new NotificationService();
11 }
12
13 // Unified method for complete order workflow
14 async createOrder(
15 userId: string,
16 orderData: CreateOrderRequest
17 ): Promise<OrderSummary> {
18 try {
19 // 1. Create shipment (calls Person 1's API)
20 const shipment = await this.shipmentService.createShipment(orderData);
21
22 // 2. Initialize tracking (internal)
23 await this.trackingService.initializeTracking(shipment.id);
24
25 // 3. Send confirmation notification
26 await this.notificationService.sendShipmentCreated(userId, shipment);
27
28 // 4. Return unified summary
29 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 subsystem
38 await this.handleOrderCreationError(userId, error);
39 throw error;
40 }
41 }
42
43 async getOrderHistory(
44 userId: string,
45 options?: { limit?: number; status?: ShipmentStatus }
46 ): Promise<OrderSummary[]> {
47 // 1. Get user's shipments
48 const shipments = await this.shipmentService.getUserShipments(userId, options);
49
50 // 2. Get tracking info for each
51 const trackingData = await Promise.all(
52 shipments.map(s => this.trackingService.getLatestUpdate(s.id))
53 );
54
55 // 3. Combine and format
56 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 }
65
66 async reorder(userId: string, previousOrderId: string): Promise<OrderSummary> {
67 // 1. Get previous order details
68 const previousOrder = await this.shipmentService.getShipment(previousOrderId);
69
70 // 2. Create new order with same details
71 const newOrderData = {
72 package: previousOrder.package,
73 origin: previousOrder.origin,
74 destination: previousOrder.destination,
75 // User can modify these in UI before confirming
76 };
77
78 // 3. Create order through unified workflow
79 return this.createOrder(userId, newOrderData);
80 }
81
82 private async handleOrderCreationError(userId: string, error: any): Promise<void> {
83 // Send error notification
84 await this.notificationService.sendErrorAlert(userId, error.message);
85 }
86}

5. Analytics Dashboard

Create src/features/customer-portal/components/AnalyticsDashboard.tsx:

typescript
1'use client'
2
3import { useQuery } from '@tanstack/react-query';
4import { BarChart, LineChart, PieChart } from 'recharts';
5
6export function AnalyticsDashboard({ userId }: { userId: string }) {
7 const { data: stats, isLoading } = useQuery({
8 queryKey: ['analytics', userId],
9 queryFn: () => fetchUserAnalytics(userId),
10 });
11
12 if (isLoading) return <AnalyticsSkeletonLoader />;
13
14 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>
20
21 {/* Cost Analysis */}
22 <Card title="Shipping Costs">
23 <BarChart data={stats.costByMonth} />
24 </Card>
25
26 {/* Carrier Distribution */}
27 <Card title="Carrier Usage">
28 <PieChart data={stats.carrierDistribution} />
29 </Card>
30
31 {/* 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:

typescript
1// Subscribe to status changes from Person 2
2eventBus.subscribe<ShipmentStatusChangedPayload>(
3 EventType.SHIPMENT_STATUS_CHANGED,
4 async (event) => {
5 const { shipmentId, newStatus } = event.payload;
6
7 // Get shipment details
8 const shipment = await getShipment(shipmentId);
9
10 // Determine notification type
11 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 }
19
20 // Send notification via mediator
21 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

typescript
1describe('NotificationObserver', () => {
2 it('should send notifications to all channels', async () => {
3 const subject = new NotificationSubject();
4 const emailObserver = new EmailNotificationObserver();
5
6 subject.subscribe(NotificationChannel.EMAIL, emailObserver);
7
8 await subject.notifyChannel(NotificationChannel.EMAIL, mockNotification);
9 // Verify notification sent
10 });
11});
12
13describe('NotificationTemplates', () => {
14 it('should create formatted notification', async () => {
15 const template = new ShipmentCreatedTemplate();
16 const notification = await template.createNotification('user-1', 'ship-1', mockData);
17
18 expect(notification.subject).toContain('Order #');
19 expect(notification.message).toContain('Tracking Number');
20 });
21});
22
23describe('OrderFacade', () => {
24 it('should create order through unified workflow', async () => {
25 const facade = new OrderManagementFacade();
26 const order = await facade.createOrder('user-1', mockOrderData);
27
28 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