Phase 3: Package Tracking & Status Management
Person 2 Responsibility
Build the complete tracking system including status state machine, tracking dashboard, exception handling, and integration with notifications.
Learning Objectives
- Implement State pattern for lifecycle management
- Apply Observer pattern for event-driven notifications
- Use Memento pattern for history tracking
- Build Command pattern for actions
- Create real-time tracking UI
Requirements
1. State Pattern - Shipment Lifecycle
Create src/features/package-tracking/services/shipment-state.ts:
typescript1// State interface2export interface ShipmentState {3 getStatus(): ShipmentStatus;4 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void;5 canTransitionTo(status: ShipmentStatus): boolean;6 getValidTransitions(): ShipmentStatus[];7}89// Context10export class ShipmentContext {11 private state: ShipmentState;12 private shipmentId: string;1314 constructor(shipmentId: string, initialStatus: ShipmentStatus) {15 this.shipmentId = shipmentId;16 this.state = this.createState(initialStatus);17 }1819 setState(state: ShipmentState): void {20 this.state = state;21 }2223 getState(): ShipmentState {24 return this.state;25 }2627 transitionTo(newStatus: ShipmentStatus): void {28 this.state.transitionTo(this, newStatus);29 }3031 private createState(status: ShipmentStatus): ShipmentState {32 switch (status) {33 case ShipmentStatus.CREATED:34 return new CreatedState();35 case ShipmentStatus.PICKED_UP:36 return new PickedUpState();37 case ShipmentStatus.IN_TRANSIT:38 return new InTransitState();39 case ShipmentStatus.OUT_FOR_DELIVERY:40 return new OutForDeliveryState();41 case ShipmentStatus.DELIVERED:42 return new DeliveredState();43 case ShipmentStatus.EXCEPTION:44 return new ExceptionState();45 default:46 throw new Error(`Unknown status: ${status}`);47 }48 }49}5051// Concrete states52class CreatedState implements ShipmentState {53 getStatus(): ShipmentStatus {54 return ShipmentStatus.CREATED;55 }5657 canTransitionTo(status: ShipmentStatus): boolean {58 return [ShipmentStatus.PICKED_UP, ShipmentStatus.CANCELLED].includes(status);59 }6061 getValidTransitions(): ShipmentStatus[] {62 return [ShipmentStatus.PICKED_UP, ShipmentStatus.CANCELLED];63 }6465 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void {66 if (!this.canTransitionTo(newStatus)) {67 throw new Error(`Cannot transition from CREATED to ${newStatus}`);68 }6970 context.setState(this.createNextState(newStatus));71 }7273 private createNextState(status: ShipmentStatus): ShipmentState {74 switch (status) {75 case ShipmentStatus.PICKED_UP:76 return new PickedUpState();77 case ShipmentStatus.CANCELLED:78 return new CancelledState();79 default:80 throw new Error(`Invalid transition to ${status}`);81 }82 }83}8485class PickedUpState implements ShipmentState {86 getStatus(): ShipmentStatus {87 return ShipmentStatus.PICKED_UP;88 }8990 canTransitionTo(status: ShipmentStatus): boolean {91 return [ShipmentStatus.IN_TRANSIT, ShipmentStatus.EXCEPTION].includes(status);92 }9394 getValidTransitions(): ShipmentStatus[] {95 return [ShipmentStatus.IN_TRANSIT, ShipmentStatus.EXCEPTION];96 }9798 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void {99 if (!this.canTransitionTo(newStatus)) {100 throw new Error(`Cannot transition from PICKED_UP to ${newStatus}`);101 }102103 context.setState(this.createNextState(newStatus));104 }105106 private createNextState(status: ShipmentStatus): ShipmentState {107 return status === ShipmentStatus.IN_TRANSIT108 ? new InTransitState()109 : new ExceptionState();110 }111}112113// Repeat for InTransitState, OutForDeliveryState, DeliveredState, ExceptionState
2. Observer Pattern - Status Change Notifications
Create src/features/package-tracking/services/status-observer.ts:
typescript1// Observer interface2export interface StatusObserver {3 update(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): void;4}56// Subject (Observable)7export class ShipmentStatusSubject {8 private observers: Set<StatusObserver> = new Set();910 attach(observer: StatusObserver): void {11 this.observers.add(observer);12 }1314 detach(observer: StatusObserver): void {15 this.observers.delete(observer);16 }1718 async notify(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): Promise<void> {19 const promises = Array.from(this.observers).map(observer =>20 Promise.resolve(observer.update(shipmentId, oldStatus, newStatus))21 );2223 await Promise.allSettled(promises);24 }25}2627// Concrete observers28export class EventBusObserver implements StatusObserver {29 update(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): void {30 // Publish to event bus for Person 3 (notifications)31 eventBus.publish({32 type: EventType.SHIPMENT_STATUS_CHANGED,33 payload: {34 shipmentId,35 oldStatus,36 newStatus,37 timestamp: new Date(),38 },39 timestamp: new Date(),40 source: 'package-tracking',41 });42 }43}4445export class TrackingEventObserver implements StatusObserver {46 update(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): void {47 // Create tracking event in history48 const event: TrackingEvent = {49 id: crypto.randomUUID(),50 shipmentId,51 status: newStatus,52 location: this.getLocationForStatus(newStatus),53 timestamp: new Date(),54 description: this.getDescriptionForStatus(newStatus),55 };5657 saveTrackingEvent(event);58 }5960 private getLocationForStatus(status: ShipmentStatus): string {61 // Mock location data62 const locations = {63 [ShipmentStatus.PICKED_UP]: 'Origin Facility',64 [ShipmentStatus.IN_TRANSIT]: 'Distribution Center',65 [ShipmentStatus.OUT_FOR_DELIVERY]: 'Local Delivery Hub',66 [ShipmentStatus.DELIVERED]: 'Destination Address',67 };6869 return locations[status] || 'Unknown';70 }7172 private getDescriptionForStatus(status: ShipmentStatus): string {73 // Human-readable descriptions74 const descriptions = {75 [ShipmentStatus.CREATED]: 'Shipment information received',76 [ShipmentStatus.PICKED_UP]: 'Package picked up by carrier',77 [ShipmentStatus.IN_TRANSIT]: 'Package in transit',78 [ShipmentStatus.OUT_FOR_DELIVERY]: 'Out for delivery',79 [ShipmentStatus.DELIVERED]: 'Package delivered',80 [ShipmentStatus.EXCEPTION]: 'Delivery exception',81 };8283 return descriptions[status] || status;84 }85}8687// Usage in status update service88export class StatusUpdateService {89 private subject = new ShipmentStatusSubject();90 private stateContexts = new Map<string, ShipmentContext>();9192 constructor() {93 // Attach observers94 this.subject.attach(new EventBusObserver());95 this.subject.attach(new TrackingEventObserver());96 }9798 async updateStatus(shipmentId: string, newStatus: ShipmentStatus): Promise<void> {99 const context = this.getOrCreateContext(shipmentId);100 const oldStatus = context.getState().getStatus();101102 // Attempt state transition103 context.transitionTo(newStatus);104105 // Notify observers106 await this.subject.notify(shipmentId, oldStatus, newStatus);107 }108109 private getOrCreateContext(shipmentId: string): ShipmentContext {110 if (!this.stateContexts.has(shipmentId)) {111 const shipment = getShipmentById(shipmentId);112 this.stateContexts.set(113 shipmentId,114 new ShipmentContext(shipmentId, shipment.status)115 );116 }117118 return this.stateContexts.get(shipmentId)!;119 }120}
3. Memento Pattern - Status History
Create src/features/package-tracking/services/status-memento.ts:
typescript1// Memento2export class StatusMemento {3 constructor(4 private readonly status: ShipmentStatus,5 private readonly timestamp: Date,6 private readonly location: string,7 private readonly description: string8 ) {}910 getStatus(): ShipmentStatus {11 return this.status;12 }1314 getTimestamp(): Date {15 return this.timestamp;16 }1718 getLocation(): string {19 return this.location;20 }2122 getDescription(): string {23 return this.description;24 }25}2627// Originator28export class ShipmentOriginator {29 private status: ShipmentStatus;30 private location: string;31 private description: string;3233 constructor(34 status: ShipmentStatus,35 location: string = '',36 description: string = ''37 ) {38 this.status = status;39 this.location = location;40 this.description = description;41 }4243 updateStatus(status: ShipmentStatus, location: string, description: string): void {44 this.status = status;45 this.location = location;46 this.description = description;47 }4849 save(): StatusMemento {50 return new StatusMemento(51 this.status,52 new Date(),53 this.location,54 this.description55 );56 }5758 restore(memento: StatusMemento): void {59 this.status = memento.getStatus();60 this.location = memento.getLocation();61 this.description = memento.getDescription();62 }63}6465// Caretaker66export class StatusHistory {67 private history: Map<string, StatusMemento[]> = new Map();6869 saveState(shipmentId: string, memento: StatusMemento): void {70 if (!this.history.has(shipmentId)) {71 this.history.set(shipmentId, []);72 }7374 this.history.get(shipmentId)!.push(memento);75 }7677 getHistory(shipmentId: string): StatusMemento[] {78 return this.history.get(shipmentId) || [];79 }8081 getLatest(shipmentId: string): StatusMemento | null {82 const history = this.getHistory(shipmentId);83 return history.length > 0 ? history[history.length - 1] : null;84 }8586 rollback(shipmentId: string, steps: number = 1): StatusMemento | null {87 const history = this.getHistory(shipmentId);8889 if (history.length <= steps) {90 return null;91 }9293 return history[history.length - steps - 1];94 }95}
4. Command Pattern - Exception Resolution
Create src/features/package-tracking/services/exception-commands.ts:
typescript1// Command interface2export interface ExceptionCommand {3 execute(): Promise<void>;4 undo(): Promise<void>;5 getDescription(): string;6}78// Receiver9export class ExceptionHandler {10 async retryDelivery(shipmentId: string): Promise<void> {11 console.log(`Scheduling retry delivery for ${shipmentId}`);12 // Implementation13 }1415 async returnToSender(shipmentId: string): Promise<void> {16 console.log(`Initiating return to sender for ${shipmentId}`);17 // Implementation18 }1920 async holdAtFacility(shipmentId: string, facilityId: string): Promise<void> {21 console.log(`Holding shipment ${shipmentId} at ${facilityId}`);22 // Implementation23 }2425 async requestAddressCorrection(shipmentId: string, newAddress: Address): Promise<void> {26 console.log(`Updating address for ${shipmentId}`);27 // Implementation28 }29}3031// Concrete commands32export class RetryDeliveryCommand implements ExceptionCommand {33 constructor(34 private handler: ExceptionHandler,35 private shipmentId: string36 ) {}3738 async execute(): Promise<void> {39 await this.handler.retryDelivery(this.shipmentId);40 }4142 async undo(): Promise<void> {43 // Cancel retry44 }4546 getDescription(): string {47 return `Retry delivery for shipment ${this.shipmentId}`;48 }49}5051export class ReturnToSenderCommand implements ExceptionCommand {52 constructor(53 private handler: ExceptionHandler,54 private shipmentId: string55 ) {}5657 async execute(): Promise<void> {58 await this.handler.returnToSender(this.shipmentId);59 }6061 async undo(): Promise<void> {62 // Cancel return63 }6465 getDescription(): string {66 return `Return shipment ${this.shipmentId} to sender`;67 }68}6970// Invoker71export class ExceptionCommandInvoker {72 private commandHistory: ExceptionCommand[] = [];7374 async executeCommand(command: ExceptionCommand): Promise<void> {75 await command.execute();76 this.commandHistory.push(command);77 }7879 async undoLastCommand(): Promise<void> {80 const command = this.commandHistory.pop();81 if (command) {82 await command.undo();83 }84 }8586 getHistory(): ExceptionCommand[] {87 return [...this.commandHistory];88 }89}
5. Tracking Dashboard UI
Create src/features/package-tracking/components/TrackingDashboard.tsx:
- Search by tracking number or order ID
- Real-time status timeline
- Map visualization (optional)
- Exception alerts
- Delivery estimate
6. EventBus Integration
Listen for SHIPMENT_CREATED:
typescript1// Subscribe to shipment creation events2eventBus.subscribe<ShipmentCreatedPayload>(3 EventType.SHIPMENT_CREATED,4 async (event) => {5 const { shipment } = event.payload;67 // Initialize tracking8 const initialEvent: TrackingEvent = {9 id: crypto.randomUUID(),10 shipmentId: shipment.id,11 status: ShipmentStatus.CREATED,12 location: 'System',13 timestamp: new Date(),14 description: 'Shipment information received',15 };1617 await saveTrackingEvent(initialEvent);18 }19);
Deliverables
- State pattern for lifecycle (6+ states)
- Observer pattern with 2+ observers
- Memento pattern for history
- Command pattern for exceptions (4+ commands)
- Tracking dashboard UI
- EventBus integration (listen to SHIPMENT_CREATED, emit STATUS_CHANGED)
- Exception handling UI
- Unit tests (70%+ coverage)
Testing Requirements
typescript1describe('ShipmentState', () => {2 it('should allow valid transitions', () => {3 const context = new ShipmentContext('ship-1', ShipmentStatus.CREATED);4 context.transitionTo(ShipmentStatus.PICKED_UP);5 expect(context.getState().getStatus()).toBe(ShipmentStatus.PICKED_UP);6 });78 it('should reject invalid transitions', () => {9 const context = new ShipmentContext('ship-1', ShipmentStatus.CREATED);10 expect(() => context.transitionTo(ShipmentStatus.DELIVERED))11 .toThrow();12 });13});1415describe('StatusObservers', () => {16 it('should notify all observers on status change', async () => {17 const subject = new ShipmentStatusSubject();18 const mockObserver = { update: jest.fn() };1920 subject.attach(mockObserver);21 await subject.notify('ship-1', ShipmentStatus.CREATED, ShipmentStatus.PICKED_UP);2223 expect(mockObserver.update).toHaveBeenCalled();24 });25});
Integration Points
Consumes from Person 1:
- SHIPMENT_CREATED events via EventBus
Provides to Person 3:
- STATUS_CHANGED events via EventBus
Estimated Time
20-25 hours for Person 2