60 minlesson

Phase 3: Package Tracking & Status Management

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:

typescript
1// State interface
2export interface ShipmentState {
3 getStatus(): ShipmentStatus;
4 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void;
5 canTransitionTo(status: ShipmentStatus): boolean;
6 getValidTransitions(): ShipmentStatus[];
7}
8
9// Context
10export class ShipmentContext {
11 private state: ShipmentState;
12 private shipmentId: string;
13
14 constructor(shipmentId: string, initialStatus: ShipmentStatus) {
15 this.shipmentId = shipmentId;
16 this.state = this.createState(initialStatus);
17 }
18
19 setState(state: ShipmentState): void {
20 this.state = state;
21 }
22
23 getState(): ShipmentState {
24 return this.state;
25 }
26
27 transitionTo(newStatus: ShipmentStatus): void {
28 this.state.transitionTo(this, newStatus);
29 }
30
31 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}
50
51// Concrete states
52class CreatedState implements ShipmentState {
53 getStatus(): ShipmentStatus {
54 return ShipmentStatus.CREATED;
55 }
56
57 canTransitionTo(status: ShipmentStatus): boolean {
58 return [ShipmentStatus.PICKED_UP, ShipmentStatus.CANCELLED].includes(status);
59 }
60
61 getValidTransitions(): ShipmentStatus[] {
62 return [ShipmentStatus.PICKED_UP, ShipmentStatus.CANCELLED];
63 }
64
65 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void {
66 if (!this.canTransitionTo(newStatus)) {
67 throw new Error(`Cannot transition from CREATED to ${newStatus}`);
68 }
69
70 context.setState(this.createNextState(newStatus));
71 }
72
73 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}
84
85class PickedUpState implements ShipmentState {
86 getStatus(): ShipmentStatus {
87 return ShipmentStatus.PICKED_UP;
88 }
89
90 canTransitionTo(status: ShipmentStatus): boolean {
91 return [ShipmentStatus.IN_TRANSIT, ShipmentStatus.EXCEPTION].includes(status);
92 }
93
94 getValidTransitions(): ShipmentStatus[] {
95 return [ShipmentStatus.IN_TRANSIT, ShipmentStatus.EXCEPTION];
96 }
97
98 transitionTo(context: ShipmentContext, newStatus: ShipmentStatus): void {
99 if (!this.canTransitionTo(newStatus)) {
100 throw new Error(`Cannot transition from PICKED_UP to ${newStatus}`);
101 }
102
103 context.setState(this.createNextState(newStatus));
104 }
105
106 private createNextState(status: ShipmentStatus): ShipmentState {
107 return status === ShipmentStatus.IN_TRANSIT
108 ? new InTransitState()
109 : new ExceptionState();
110 }
111}
112
113// Repeat for InTransitState, OutForDeliveryState, DeliveredState, ExceptionState

2. Observer Pattern - Status Change Notifications

Create src/features/package-tracking/services/status-observer.ts:

typescript
1// Observer interface
2export interface StatusObserver {
3 update(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): void;
4}
5
6// Subject (Observable)
7export class ShipmentStatusSubject {
8 private observers: Set<StatusObserver> = new Set();
9
10 attach(observer: StatusObserver): void {
11 this.observers.add(observer);
12 }
13
14 detach(observer: StatusObserver): void {
15 this.observers.delete(observer);
16 }
17
18 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 );
22
23 await Promise.allSettled(promises);
24 }
25}
26
27// Concrete observers
28export 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}
44
45export class TrackingEventObserver implements StatusObserver {
46 update(shipmentId: string, oldStatus: ShipmentStatus, newStatus: ShipmentStatus): void {
47 // Create tracking event in history
48 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 };
56
57 saveTrackingEvent(event);
58 }
59
60 private getLocationForStatus(status: ShipmentStatus): string {
61 // Mock location data
62 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 };
68
69 return locations[status] || 'Unknown';
70 }
71
72 private getDescriptionForStatus(status: ShipmentStatus): string {
73 // Human-readable descriptions
74 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 };
82
83 return descriptions[status] || status;
84 }
85}
86
87// Usage in status update service
88export class StatusUpdateService {
89 private subject = new ShipmentStatusSubject();
90 private stateContexts = new Map<string, ShipmentContext>();
91
92 constructor() {
93 // Attach observers
94 this.subject.attach(new EventBusObserver());
95 this.subject.attach(new TrackingEventObserver());
96 }
97
98 async updateStatus(shipmentId: string, newStatus: ShipmentStatus): Promise<void> {
99 const context = this.getOrCreateContext(shipmentId);
100 const oldStatus = context.getState().getStatus();
101
102 // Attempt state transition
103 context.transitionTo(newStatus);
104
105 // Notify observers
106 await this.subject.notify(shipmentId, oldStatus, newStatus);
107 }
108
109 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 }
117
118 return this.stateContexts.get(shipmentId)!;
119 }
120}

3. Memento Pattern - Status History

Create src/features/package-tracking/services/status-memento.ts:

typescript
1// Memento
2export class StatusMemento {
3 constructor(
4 private readonly status: ShipmentStatus,
5 private readonly timestamp: Date,
6 private readonly location: string,
7 private readonly description: string
8 ) {}
9
10 getStatus(): ShipmentStatus {
11 return this.status;
12 }
13
14 getTimestamp(): Date {
15 return this.timestamp;
16 }
17
18 getLocation(): string {
19 return this.location;
20 }
21
22 getDescription(): string {
23 return this.description;
24 }
25}
26
27// Originator
28export class ShipmentOriginator {
29 private status: ShipmentStatus;
30 private location: string;
31 private description: string;
32
33 constructor(
34 status: ShipmentStatus,
35 location: string = '',
36 description: string = ''
37 ) {
38 this.status = status;
39 this.location = location;
40 this.description = description;
41 }
42
43 updateStatus(status: ShipmentStatus, location: string, description: string): void {
44 this.status = status;
45 this.location = location;
46 this.description = description;
47 }
48
49 save(): StatusMemento {
50 return new StatusMemento(
51 this.status,
52 new Date(),
53 this.location,
54 this.description
55 );
56 }
57
58 restore(memento: StatusMemento): void {
59 this.status = memento.getStatus();
60 this.location = memento.getLocation();
61 this.description = memento.getDescription();
62 }
63}
64
65// Caretaker
66export class StatusHistory {
67 private history: Map<string, StatusMemento[]> = new Map();
68
69 saveState(shipmentId: string, memento: StatusMemento): void {
70 if (!this.history.has(shipmentId)) {
71 this.history.set(shipmentId, []);
72 }
73
74 this.history.get(shipmentId)!.push(memento);
75 }
76
77 getHistory(shipmentId: string): StatusMemento[] {
78 return this.history.get(shipmentId) || [];
79 }
80
81 getLatest(shipmentId: string): StatusMemento | null {
82 const history = this.getHistory(shipmentId);
83 return history.length > 0 ? history[history.length - 1] : null;
84 }
85
86 rollback(shipmentId: string, steps: number = 1): StatusMemento | null {
87 const history = this.getHistory(shipmentId);
88
89 if (history.length <= steps) {
90 return null;
91 }
92
93 return history[history.length - steps - 1];
94 }
95}

4. Command Pattern - Exception Resolution

Create src/features/package-tracking/services/exception-commands.ts:

typescript
1// Command interface
2export interface ExceptionCommand {
3 execute(): Promise<void>;
4 undo(): Promise<void>;
5 getDescription(): string;
6}
7
8// Receiver
9export class ExceptionHandler {
10 async retryDelivery(shipmentId: string): Promise<void> {
11 console.log(`Scheduling retry delivery for ${shipmentId}`);
12 // Implementation
13 }
14
15 async returnToSender(shipmentId: string): Promise<void> {
16 console.log(`Initiating return to sender for ${shipmentId}`);
17 // Implementation
18 }
19
20 async holdAtFacility(shipmentId: string, facilityId: string): Promise<void> {
21 console.log(`Holding shipment ${shipmentId} at ${facilityId}`);
22 // Implementation
23 }
24
25 async requestAddressCorrection(shipmentId: string, newAddress: Address): Promise<void> {
26 console.log(`Updating address for ${shipmentId}`);
27 // Implementation
28 }
29}
30
31// Concrete commands
32export class RetryDeliveryCommand implements ExceptionCommand {
33 constructor(
34 private handler: ExceptionHandler,
35 private shipmentId: string
36 ) {}
37
38 async execute(): Promise<void> {
39 await this.handler.retryDelivery(this.shipmentId);
40 }
41
42 async undo(): Promise<void> {
43 // Cancel retry
44 }
45
46 getDescription(): string {
47 return `Retry delivery for shipment ${this.shipmentId}`;
48 }
49}
50
51export class ReturnToSenderCommand implements ExceptionCommand {
52 constructor(
53 private handler: ExceptionHandler,
54 private shipmentId: string
55 ) {}
56
57 async execute(): Promise<void> {
58 await this.handler.returnToSender(this.shipmentId);
59 }
60
61 async undo(): Promise<void> {
62 // Cancel return
63 }
64
65 getDescription(): string {
66 return `Return shipment ${this.shipmentId} to sender`;
67 }
68}
69
70// Invoker
71export class ExceptionCommandInvoker {
72 private commandHistory: ExceptionCommand[] = [];
73
74 async executeCommand(command: ExceptionCommand): Promise<void> {
75 await command.execute();
76 this.commandHistory.push(command);
77 }
78
79 async undoLastCommand(): Promise<void> {
80 const command = this.commandHistory.pop();
81 if (command) {
82 await command.undo();
83 }
84 }
85
86 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:

typescript
1// Subscribe to shipment creation events
2eventBus.subscribe<ShipmentCreatedPayload>(
3 EventType.SHIPMENT_CREATED,
4 async (event) => {
5 const { shipment } = event.payload;
6
7 // Initialize tracking
8 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 };
16
17 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

typescript
1describe('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 });
7
8 it('should reject invalid transitions', () => {
9 const context = new ShipmentContext('ship-1', ShipmentStatus.CREATED);
10 expect(() => context.transitionTo(ShipmentStatus.DELIVERED))
11 .toThrow();
12 });
13});
14
15describe('StatusObservers', () => {
16 it('should notify all observers on status change', async () => {
17 const subject = new ShipmentStatusSubject();
18 const mockObserver = { update: jest.fn() };
19
20 subject.attach(mockObserver);
21 await subject.notify('ship-1', ShipmentStatus.CREATED, ShipmentStatus.PICKED_UP);
22
23 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