# Product Requirements Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Problem statement is specific and measurable
- [x] Problem is validated by evidence (not assumptions)
- [x] Context → Problem → Solution flow makes sense
- [x] Every persona has at least one user journey
- [x] All MoSCoW categories addressed (Must/Should/Could/Won't)
- [x] Every feature has testable acceptance criteria
- [x] Every metric has corresponding tracking events
- [x] No feature redundancy (check for duplicates)
- [x] No contradictions between sections
- [x] No technical implementation details included
- [x] A new team member could understand this PRD

---

## Definitions

> **Critical definitions that govern measurement and behavior throughout this specification.**

### Wait Time Definitions

| Term | Definition |
|------|------------|
| **Predicted Wait Time** | The estimated minutes displayed to the customer at check-in, calculated using the enhanced factors in this spec |
| **Actual Wait Time** | `sortStarted - timeEntered` in minutes. If `sortStarted` is null or invalid, use `timeStarted - timeEntered`. See edge cases below. |
| **Data Window** | Rolling 30-day period from current date for all historical calculations |
| **Minimum Data Threshold** | 50 completed transactions within the data window for a given time slot |

### Wait Time Edge Cases

| Scenario | Handling |
|----------|----------|
| Customer abandons (walks away before service) | Exclude from actual wait calculations; transaction marked `abandoned` |
| Transaction canceled by staff | Exclude from actual wait calculations |
| Transaction paused (on hold) | Include full elapsed time; pauses are part of customer's perceived wait |
| `sortStarted` is `0000-00-00 00:00:00` | Fall back to `timeStarted` as end timestamp |
| Both timestamps invalid | Exclude from calculations; log data quality issue |
| Actual wait > 480 minutes (8 hours) | Cap at 480 minutes; flag as outlier for review |

### Queue Depth Definition

**Queue Depth** = Count of transactions in `buyQueue` where:
- `isProcessed = 0` (not yet completed)
- AND (`remote = 0` OR `hasDroppedOff = 1`) (in-store or dropped off)
- AND `status NOT IN ('canceled', 'abandoned', 'on_hold')`

**Excluded from queue depth:**
- Remote transactions not yet dropped off
- Canceled transactions
- Abandoned transactions
- Transactions on hold (paused by staff)

### Factor Combination Rules

When multiple factors are calculated, they combine **multiplicatively** in this order:

```
Final Estimate = Base Estimate × Dynamic Factor × Queue Multiplier × Efficiency Factor

Where:
- Base Estimate = containers × minutesPerContainer
- Dynamic Factor = time slot adjustment (0.5 to 2.0)
- Queue Multiplier = congestion adjustment (1.0 to 2.0)
- Efficiency Factor = staff speed adjustment (0.5 to 2.0)
```

**Missing Factor Handling:**
| Factor | Missing Data Scenario | Default Value |
|--------|----------------------|---------------|
| Dynamic Factor | < 50 transactions in 30-day window for time slot | 1.0 (static `waitTimeFactor` from store config) |
| Queue Multiplier | Cannot query queue | 1.0 |
| Efficiency Factor | No on-duty employees found, or no employee has metrics | 1.0 |

---

## Product Overview

### Vision

Deliver wait time estimates that customers can trust, improving from ~35-40% error to ~25-30% error using smart data aggregation and real-time adjustments.

### Problem Statement

**Current State:** The estimated wait time displayed to customers uses a static formula: `(containers × minutesPerContainer) × waitTimeFactor`. This produces estimates with Mean Absolute Percentage Error (MAPE) of approximately 35-40%.

**Pain Points:**
1. **Static factors don't adapt** - The same `waitTimeFactor` applies at 10am Monday and 2pm Saturday, despite vastly different traffic patterns
2. **Queue depth ignored** - A customer with 2 people ahead gets the same estimate formula as one with 20 people ahead
3. **Configuration drift** - Manually-set `minutesPerContainer` values become inaccurate as store operations evolve
4. **Staff efficiency invisible** - Fast and slow employees process at different rates, but this isn't reflected in estimates

**Consequence:** Customers arrive expecting one wait time and experience another. This erodes trust, increases walkaways, and generates complaints. Survey data shows wait time is strongly correlated with NPS scores.

**Business Impact:** Based on existing NPS correlation data (r = -0.35 to -0.45), improving wait time accuracy is expected to lift NPS by 5-10 points for stores with high wait time variability.

### Value Proposition

By leveraging 4+ years of historical transaction data already captured in the system, we can significantly improve prediction accuracy without external services or ML infrastructure. The improvements:

1. **Use data we already have** - Heatmap data, employee metrics, and queue patterns are already tracked
2. **Immediate impact** - 10-15% improvement in prediction accuracy
3. **Foundation for ML** - Better baseline metrics and data patterns for future ML implementation

## User Personas

### Primary Persona: Store Manager (Sarah)

- **Demographics:** 28-45 years old, manages daily operations, moderate technical comfort
- **Goals:** Keep customers happy, reduce complaints, optimize staffing decisions
- **Pain Points:**
  - Customers complain about inaccurate wait times
  - Can't explain why estimates were wrong when reviewing with staff
  - Manually adjusts `waitTimeFactor` through trial and error

### Secondary Persona: Buyer (Customer)

- **Demographics:** 18-65 years old, visiting store to sell items, varying patience levels
- **Goals:** Know how long they'll actually wait, plan their time accordingly
- **Pain Points:**
  - Told "25-35 minutes" but waited 50 minutes
  - Can't trust displayed estimates to make decisions (leave and come back vs wait)
  - No visibility into why the wait is what it is

### Tertiary Persona: Corporate Operations Manager

- **Demographics:** 35-50 years old, oversees multiple stores, data-driven decision maker
- **Goals:** Standardize operations, improve customer satisfaction metrics, reduce manual tuning
- **Pain Points:**
  - Every store has different `waitTimeFactor` settings with no clear reasoning
  - Can't compare wait time accuracy across stores
  - No systematic way to improve prediction quality

## User Journey Maps

### Primary Journey: Customer Receives Accurate Estimate

1. **Arrival:** Customer enters store with items to sell
2. **Check-in:** System generates wait time estimate using enhanced factors
3. **Communication:** Customer sees "Estimated wait: 25-35 minutes"
4. **Experience:** Customer waits and is served within the displayed range
5. **Satisfaction:** Customer trusts the system for future visits

**Success Criteria:** Estimate is accurate within ±10 minutes 70% of the time (up from ~55%)

### Secondary Journey: Manager Reviews Prediction Quality

1. **Weekly Review:** Manager accesses wait time analytics dashboard
2. **Analysis:** Views heatmap showing actual vs predicted patterns
3. **Insight:** System shows which time slots have highest prediction errors
4. **Action:** Manager understands patterns without manual factor adjustment
5. **Outcome:** Prediction accuracy improves automatically over time

## Feature Requirements

### Must Have Features

#### Feature 1: Dynamic Wait Time Factor by Hour/Day

- **User Story:** As a customer, I want wait time estimates that account for how busy this specific time slot typically is, so that estimates reflect actual patterns.
- **Acceptance Criteria:**
  - [ ] System retrieves historical average wait time for current day-of-week (0=Sunday, 6=Saturday) and hour (0-23)
  - [ ] Data window: rolling 30 days from current date
  - [ ] Dynamic factor calculated as: `slot_average_wait / overall_30day_average_wait`
  - [ ] Factor bounded between 0.5 and 2.0
  - [ ] Falls back to store's static `waitTimeFactor` if time slot has < 50 transactions in 30-day window
  - [ ] Results cached with 24-hour TTL, refreshed nightly at 2am store local time

#### Feature 2: Queue Depth Multiplier

- **User Story:** As a customer, I want the system to account for how many people are ahead of me, so longer queues produce appropriately longer estimates.
- **Acceptance Criteria:**
  - [ ] Queue depth calculated per definition above (active, in-store, not canceled/abandoned/on-hold)
  - [ ] Multiplier formula: `1.0 + (queue_depth / 10) × 0.15`
  - [ ] Multiplier capped at maximum of 2.0 (equivalent to 66+ people ahead)
  - [ ] Queue depth = 0 produces multiplier of 1.0
  - [ ] Queue depth = 10 produces multiplier of 1.15
  - [ ] Queue depth = 30 produces multiplier of 1.45

### Should Have Features

#### Feature 3: Rolling Average Calibration

- **User Story:** As a store manager, I want the base minutes-per-container setting to automatically stay accurate, so I don't have to manually tune it.
- **Acceptance Criteria:**
  - [ ] Weekly background job (Sunday 3am store local time) calculates actual average container processing time from past 30 days
  - [ ] Minimum 100 completed transactions required for calculation
  - [ ] Setting auto-updated if calculated value differs by > 15% from current setting
  - [ ] Store can opt-out with lock flag (`mpcLocked = true` in stores table)
  - [ ] All changes logged with: old value, new value, transaction count, date

#### Feature 4: Employee Efficiency Weighting

- **User Story:** As a customer, I want my wait estimate to reflect the actual speed of the staff currently working, so fast teams produce shorter estimates.
- **Acceptance Criteria:**
  - [ ] System queries on-duty employees from WhenIWork integration (if available) or scheduled shifts
  - [ ] Employee efficiency sourced from `employees.averagePerContainer` field
  - [ ] Efficiency factor = `store_minutesPerContainer / average(on_duty_employee_averages)`
  - [ ] Factor bounded between 0.5 and 2.0
  - [ ] **Fallback precedence** (in order):
    1. Use WhenIWork on-duty list if available and has ≥ 1 employee with metrics
    2. Use scheduled shift employees if WhenIWork unavailable and ≥ 1 has metrics
    3. Use all active employees' average if no schedule data and ≥ 3 have metrics
    4. Default to factor = 1.0 if none of above conditions met
  - [ ] Employee metrics considered "stale" if not updated in 30 days; excluded from calculation

### Could Have Features

#### Feature 5: Prediction Accuracy Logging

- **User Story:** As a corporate manager, I want to track how accurate our predictions are over time, so I can measure improvement.
- **Acceptance Criteria:**
  - [ ] Predicted wait time logged at check-in with `buy_id` linkage
  - [ ] Actual wait time calculated when `sortStarted` or `timeStarted` is recorded
  - [ ] Daily aggregation calculates MAPE per store (exclude abandoned/canceled)
  - [ ] Historical accuracy data retained for 12 months

### Won't Have (This Phase)

- **ML-based predictions** - Future phase after quick wins prove value
- **Real-time external data integration** - Weather, traffic, events
- **Customer-specific predictions** - Based on their transaction history
- **Multi-queue predictions** - Different wait times for different transaction types
- **Mobile app push notifications** - "Your turn is coming up"
- **Dashboard UI changes** - Analytics UI is out of scope for Release 1
- **DST/Timezone edge cases** - Store timezone assumed stable; DST transitions may cause minor anomalies on transition days

## Detailed Feature Specifications

### Feature: Dynamic Wait Time Factor by Hour/Day

**Description:** Replace static `waitTimeFactor` with a dynamic factor that reflects historical patterns for the current time slot.

**User Flow:**
1. Customer checks in with items
2. System looks up current day-of-week (0=Sunday, 6=Saturday) and hour (0-23)
3. System retrieves cached historical average for this time slot
4. System calculates dynamic factor = (slot_average / overall_average)
5. Factor is bounded to 0.5-2.0 range
6. Factor is applied to container-based estimate multiplicatively

**Business Rules:**
- Rule 1: Data window is rolling 30 days from current date
- Rule 2: Minimum threshold is 50 completed transactions for the time slot within the data window
- Rule 3: If threshold not met, use store's static `waitTimeFactor` instead
- Rule 4: Dynamic factor bounded between 0.5 (minimum) and 2.0 (maximum)
- Rule 5: Cache TTL is 24 hours, refreshed nightly at 2am store local time
- Rule 6: If cache unavailable, fall back to static factor (never block on cache)

**Edge Cases:**
| Scenario | Handling |
|----------|----------|
| Store is new (< 30 days data) | Use static factor |
| Time slot has 30-49 transactions | Expand window to ±1 hour (e.g., 2pm-4pm instead of 3pm-4pm); if still < 50, use static |
| Store hours changed recently | No special handling; recent data naturally dominates 30-day window |
| Holiday/special event | Standard calculation; won't detect anomalies this phase |
| Store closed during normal hours | Time slots with 0 transactions excluded; no factor calculated |

### Feature: Queue Depth Multiplier

**Description:** Adjust estimates upward when more transactions are ahead in the queue.

**Business Rules:**
- Rule 1: Multiplier formula = `1.0 + (queue_depth / 10) × 0.15`
- Rule 2: Maximum multiplier = 2.0 (caps at 66+ people ahead)
- Rule 3: Queue depth per definition in Definitions section above

**Edge Cases:**
| Scenario | Handling |
|----------|----------|
| Queue depth = 0 | Multiplier = 1.0 (no adjustment) |
| Transaction status changes after estimate | No recalculation; estimate is point-in-time |
| Database query fails | Default to multiplier = 1.0; log error |

## Dependencies

### External Systems

| System | Dependency Type | Fallback |
|--------|-----------------|----------|
| WhenIWork API | On-duty employee list for efficiency weighting | Use scheduled shifts, then active employees |
| Redis | Factor caching | Fall back to static factors; accept performance hit |
| TaskEngine | Weekly calibration job, nightly cache refresh | Manual calibration if jobs fail |

### Internal Data Sources

| Table | Required Fields | Purpose |
|-------|-----------------|---------|
| `buyQueue` | `timeEntered`, `sortStarted`, `timeStarted`, `isProcessed`, `status` | Historical wait time data |
| `stores` | `minutesPerContainer`, `waitTimeFactor`, `mpcLocked` | Store configuration |
| `employees` | `averagePerContainer`, `active`, `wiwID` | Employee efficiency |
| `statsStoreDaily` | `avgDelay`, `numBuys` | Pre-aggregated daily stats |

### Data Quality Requirements

- `buyQueue.timeEntered` must be accurate within 1 minute of actual check-in
- At least 50% of transactions must have valid `sortStarted` or `timeStarted` for meaningful analysis
- Employee `averagePerContainer` should be updated at least monthly for accurate efficiency weighting

## Success Metrics

### Key Performance Indicators

| Metric | Baseline | Target | Measurement Method |
|--------|----------|--------|-------------------|
| **MAPE** | ~35-40% | ~25-30% | `abs(predicted - actual) / actual × 100`, aggregated daily |
| **±10min Accuracy** | ~55% | 70% | % of predictions within 10 minutes of actual |
| **NPS Correlation** | r = -0.40 | r = -0.30 | Pearson correlation from `customerSurvey` join |
| **Manual Adjustments** | ~4 per store/month | ~2 per store/month | Count of `waitTimeFactor` changes in admin logs |

### Tracking Requirements

| Event | Properties | Purpose |
|-------|------------|---------|
| `wait_time_estimated` | `buy_id`, `predicted_minutes`, `dynamic_factor`, `queue_multiplier`, `efficiency_factor`, `queue_depth`, `store_id`, `timestamp` | Capture prediction inputs |
| `wait_time_actual` | `buy_id`, `actual_minutes`, `store_id`, `timestamp` | Capture actual outcome |
| `wait_time_accuracy` | `store_id`, `date`, `mape`, `accuracy_10min_pct`, `sample_size` | Daily aggregation |
| `calibration_executed` | `store_id`, `old_mpc`, `new_mpc`, `transaction_count`, `timestamp` | Track auto-calibration |

---

## Constraints and Assumptions

### Constraints

- **Performance:** Wait time calculation must complete in < 100ms
- **Backward compatibility:** Must support stores that disable new features via feature flags
- **Data availability:** Some stores may have sparse historical data; graceful degradation required

### Assumptions

- **Historical data quality:** Existing `buyQueue` timestamps are accurate within 1 minute
- **Employee data availability:** WhenIWork integration provides on-duty information for participating stores
- **Pattern stability:** Day/hour patterns are relatively consistent week-over-week for mature stores
- **Store timezone stability:** Store timezone does not change frequently; DST transitions not specially handled

## Risks and Mitigations

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Historical data has systematic errors | High | Low | Validate data quality before launch; compare predicted vs actual patterns |
| Dynamic factors cause worse predictions for some stores | Medium | Medium | Feature flags per store; A/B testing before full rollout; bounded factor range |
| Cache misses cause performance degradation | Medium | Low | Graceful fallback to static factors; cache warming on deploy |
| Employee efficiency data is stale or inaccurate | Low | Medium | 30-day staleness threshold; optional feature; fallback to baseline |
| Auto-calibration changes values managers rely on | Medium | Medium | Notification on change; lock flag to opt-out; change logging |

## Open Questions

- [x] ~~What is the current baseline MAPE?~~ → Estimated 35-40% based on analysis
- [x] ~~How frequently should calibration run?~~ → Weekly (Sundays 3am) via TaskEngine job
- [x] ~~What defines "actual wait time"?~~ → `sortStarted - timeEntered` with fallback to `timeStarted`
- [ ] Should stores see their prediction accuracy in the dashboard? (Phase 2 consideration)
- [ ] What notification should managers receive when auto-calibration changes a value?

---

## Supporting Research

### Competitive Analysis

Most buy-back services (GameStop, Decluttr) provide static "processing time" ranges or no estimates. Dynamic, data-driven wait time predictions would be a competitive differentiator.

### User Research

- Customer surveys show wait time strongly correlates with NPS (r = -0.40 from existing `WaitTimeService` analysis)
- Manager feedback indicates desire for less manual tuning
- Corporate has expressed interest in cross-store prediction accuracy metrics

### Market Data

- Healthcare sector uses similar ML-based wait predictions with 25% RMSE improvements
- Bank queue prediction systems achieve MAE of ~3.35 minutes
- Retail queue optimization is a growing focus area (2024-2025 trends)

### Internal Data Assets

| Asset | Location | Value for This Feature |
|-------|----------|------------------------|
| 4+ years transaction history | `buyQueue` table | HIGH - patterns by day/hour |
| Daily aggregates | `statsStoreDaily` | HIGH - pre-computed baselines |
| Employee efficiency metrics | `employees.averagePerContainer` | MEDIUM - staff speed data |
| Survey correlation data | `customerSurvey` | MEDIUM - satisfaction linkage |
| Heatmap analytics | `WaitTimeService` | HIGH - already computing patterns |

---

## Appendix: Implementation Notes

> *These notes provide context for the Solution Design phase. They are not requirements.*

**Suggested Stack:**
- Core calculation in `EstimatedWaitTime.php` (existing class)
- Factor caching via Redis (existing infrastructure)
- Background jobs via TaskEngine (existing scheduler)
- Data aggregation leveraging `WaitTimeRepository` (existing)

**Key Files to Review:**
- `userfrosting/src/BuyerKiosk/Core/EstimatedWaitTime.php` - Current calculation
- `userfrosting/src/BuyerKiosk/Analytics/Services/WaitTimeService.php` - Heatmap/analytics
- `userfrosting/src/BuyerKiosk/Analytics/Repositories/WaitTimeRepository.php` - SQL queries

**Feature Flag Suggestion:**
- `dynamicWaitFactor.enabled` - Enable dynamic factors for a store
- `autoCalibration.enabled` - Enable auto-calibration for a store
- `efficiencyWeighting.enabled` - Enable employee efficiency weighting
