Weekly Forecast — Personalized Shopping List
Weekly Forecast — Personalized Shopping List
Section titled “Weekly Forecast — Personalized Shopping List”1. Problem Statement
Section titled “1. Problem Statement”The AI Rewards Assistant has a 6.9% 7-day repeat rate — 93% of users don’t return within a week. 66% of conversations are single-turn. There is no recurring, time-bound reason to open the assistant.
The Weekly Forecast gives users a personalized, point-optimized shopping list delivered before their typical shopping day. It predicts what they’ll buy based on purchase history, surfaces the highest-earning opportunities (stacked offer+Shop first, then offers, then Shop-only), and delivers it as a push notification + episode in the assistant.
If we don’t build this, the assistant remains reactive — users only open it when they have a question, not as a regular part of their shopping routine.
2. Background & Context
Section titled “2. Background & Context”2.1 Current State
Section titled “2.1 Current State”The repurchase nudge pipeline (PLT-191) is live in production and provides the foundation:
- Neo4j purchase graph: ~45K users with PURCHASED relationships including timestamps, purchase count, average interval, and repurchase likelihood
- CCS enrichment: Returns product metadata, active offers, Fetch Shop availability, PPD points, offer points, merchant data, and images
- Scheduler: Batch processes candidates daily — Neo4j fetch → CCS enrichment → eligibility → selection → PreSend (LLM titles) → notification delivery
- Episode system: consumer-agent creates episodes with product-card components and markdown text
- Existing UI components: offer-shelf (3+ offers in scrollable shelf), product-card (single product with image/price/points), markdown text (headers, tables, emphasis)
The forecast reuses this entire pipeline but produces a multi-item episode instead of individual single-product DMs.
2.2 Key Decisions Already Made
Section titled “2.2 Key Decisions Already Made”- No new UI components — the forecast MUST render using only existing offer-shelf, product-card, and markdown text. This eliminates frontend development and iOS/Android release dependencies.
- Build as consumer-graph-worker handler — same architecture as
NotificationHandler, not a new service. - Three earning tiers — Stacked (offer+Shop), Offer-only, Shop-only. Items with no earning opportunity are excluded.
- PRD: Weekly Forecast PRD by Josh Gunning (2026-03-25, Draft)
2.3 Glossary
Section titled “2.3 Glossary”| Term | Definition |
|---|---|
| Stacked | Item has both an active offer AND is available on Fetch Shop — maximum earning potential |
| Offer-only | Item has an active offer but is not on Fetch Shop — earn by buying anywhere and scanning receipt |
| Shop-only | Item is on Fetch Shop but has no active offer — earn PPD points by buying through Shop |
| Forecast | A personalized, time-bound list of predicted purchases ranked by earning tier |
| Shopping day | The day of week a user most frequently makes purchases (detected from receipt history) |
| Episode | A persistent, structured conversation entry in consumer-agent with components and text |
3. Requirements
Section titled “3. Requirements”3.1 Functional Requirements
Section titled “3.1 Functional Requirements”- The system MUST generate a weekly forecast for each enrolled user containing 8-15 predicted purchase items.
- The system MUST classify each item into one of three tiers: Stacked (offer + Shop), Offer-only, or Shop-only.
- The system MUST exclude items with no earning opportunity (no offer AND not on Shop).
- The system MUST sort items by tier priority (Stacked > Offer > Shop), then by expected value within each tier.
- The system MUST deliver the forecast as a consumer-agent episode composed of:
- Markdown text for summary, section headers, points table, and CTAs
- offer-shelf components for Stacked and Offer-only tier items (when 3+ items exist in the tier)
- product-card components for remaining items or tiers with fewer than 3 items
- The system MUST detect each user’s peak shopping day from their 90-day purchase history.
- The system MUST generate the forecast 2 days before the user’s detected shopping day.
- The system MUST send a push notification when the forecast is ready: “Your weekly forecast is ready — earn up to X,XXX points this week”.
- The system MUST persist forecast metadata to DynamoDB for retrieval and analytics.
- The system MUST be gated behind the
weekly_forecast_enabledfeature flag. - The system SHOULD support manual refresh via user request in chat (re-runs forecast generation for that user).
- The system MAY fall back to Monday generation if no peak shopping day is detectable.
3.2 Non-Functional Requirements
Section titled “3.2 Non-Functional Requirements”- Forecast generation for the full user cohort (~45K users) MUST complete within 30 minutes.
- CCS enrichment MUST reuse existing caches (FPS, FIDORA, Button) — no cold enrichment of all items.
- DynamoDB forecast reads MUST respond in < 50ms at p95.
- Push notification delivery MUST use existing notification-service guardrails (quiet hours, quota, dedup).
- The system MUST NOT exceed current Neo4j resource utilization by more than 20% during forecast generation.
3.3 Acceptance Criteria
Section titled “3.3 Acceptance Criteria”- AC-1: Given a user with 10+ purchase history items and
weekly_forecast_enabled=true, when the forecast scheduler runs, then an episode is created containing markdown text + offer-shelf/product-card components with tier-sorted items. - AC-2: Given a forecast episode, when the user opens it in the app, then all offer-shelf and product-card components render correctly using existing component rendering (no new client code).
- AC-3: Given a user who typically shops on Saturday, when the forecast scheduler runs on Thursday, then a forecast is generated and a push notification is sent.
- AC-4: Given a forecast with 4 Stacked items and 5 Offer items, when rendered, then Stacked items appear in an offer-shelf first, followed by Offer items in a second offer-shelf, with markdown headers separating tiers.
- AC-5: Given a user not enrolled in
weekly_forecast_enabled, when the forecast scheduler runs, then no forecast is generated for that user. - AC-6: Given a forecast with 2 Shop-only items (below offer-shelf minimum of 3), when rendered, then those items appear as individual product-cards, not an offer-shelf.
4. Solution Design
Section titled “4. Solution Design”4.1 Architecture
Section titled “4.1 Architecture”consumer-graph-worker├── ForecastHandler (new, implements NotificationTypeHandler interface)│ ├── FetchCandidates() — reuses existing Neo4j repurchase query│ ├── BatchEnrich() — reuses existing CCS batch enrichment│ ├── ClassifyTiers() — new: Stacked/Offer/Shop classification│ ├── BuildForecastEpisode()— new: assembles markdown + offer-shelf + product-card│ ├── PreSend() — reuses LLM short title generation│ └── Send() — creates episode + sends push notification│├── ShoppingDayDetector (new)│ └── DetectPeakDay() — Neo4j query for modal day-of-week│└── Scheduler (existing orchestrator) └── Runs ForecastHandler on per-user schedule (2 days before shopping day)
consumer-context-service└── GET /v1/users/{user_id}/forecast — reads from DynamoDB, returns forecast metadata
consumer-agent└── POST /episodes (existing) — creates forecast episode with components
DynamoDB└── {env}-weekly-forecasts table — PK: user_id, SK: week_start4.2 Data Model
Section titled “4.2 Data Model”DynamoDB: {env}-weekly-forecasts
{ "user_id": "67638090281942c242861d61", "week_start": "2026-04-07", "created_at": "2026-04-05T03:15:00Z", "episode_id": "abc123-...", "shopping_day": 6, "total_points": 12450, "item_count": 10, "tiers": { "stacked": 3, "offer": 4, "shop": 3 }, "items": [ { "fido_id": "b6dde658-...", "short_name": "Tyson Oven Roasted Chicken", "tier": "stacked", "offer_points": 2000, "ppd_points": 2040, "total_points": 4040, "offer_id": "og-12345" } ]}Neo4j: Shopping day detection query
MATCH (u:User {userId: $userId})-[p:PURCHASED]->()WHERE p.lastPurchaseDate > datetime() - duration('P90D')WITH u, datetime(p.lastPurchaseDate).dayOfWeek AS dow, count(*) AS freqRETURN dow, freqORDER BY freq DESCLIMIT 14.3 API Contracts
Section titled “4.3 API Contracts”CCS: GET /v1/users/{user_id}/forecast
Response:
{ "user_id": "67638090281942c242861d61", "week_start": "2026-04-07", "episode_id": "abc123-...", "shopping_day": "Saturday", "total_points": 12450, "item_count": 10, "tiers": { "stacked": {"count": 3, "points": 5200}, "offer": {"count": 4, "points": 4750}, "shop": {"count": 3, "points": 2500} }, "created_at": "2026-04-05T03:15:00Z"}Errors:
404: No forecast exists for this user/week403: User not enrolled inweekly_forecast_enabled
4.4 Key Implementation Details
Section titled “4.4 Key Implementation Details”Tier classification uses fields already in the CCS enrichment response:
func classifyTier(e *ProductEnrichResponse) string { switch { case e.OfferActive && e.IsOnFetchShop: return "stacked" case e.OfferActive: return "offer" case e.IsOnFetchShop: return "shop" default: return "" // excluded }}Episode assembly — the forecast episode is built from markdown text blocks interleaved with existing components:
# Your Weekly Forecast**Week of April 7** · Based on your purchase history
You could earn up to **12,450 points** this week across 10 items.
| Tier | Items | Points ||------|-------|--------|| Offer + Shop | 3 | 5,200 || Active Offers | 4 | 4,750 || Fetch Shop | 3 | 2,500 |
---
## Best Deals — Offer + Shop*These items have an active offer AND are on Fetch Shop for maximum points.*→ offer-shelf component with 3 stacked-tier offer IDs
## Active Offers*Buy these anywhere and scan your receipt to earn.*→ offer-shelf component with 4 offer-tier offer IDs
## Available on Fetch Shop*No active offer right now, but earn points buying through Fetch Shop.*→ product-card components for each shop-only item
---*Tap any item to activate its offer or add it to your Fetch Shop cart.*Offer-shelf minimum: offer-shelf requires 3+ items. If a tier has < 3 items, render as individual product-cards instead.
Points calculation (from existing enrichment):
- Stacked:
OfferPoints + PPDPoints - Offer-only:
OfferPoints - Shop-only:
PPDPoints
Scheduling: The ForecastHandler runs within the existing scheduler orchestrator but with per-user scheduling based on detected shopping day. Implementation options:
- (A) Run daily, check each user’s shopping day, skip if not 2 days before → simplest, reuses existing daily batch pattern
- (B) Per-user cron-like scheduling → more complex, better efficiency
- Recommend (A) for Phase 1.
5. Dependencies
Section titled “5. Dependencies”5.1 Spec Dependencies
Section titled “5.1 Spec Dependencies”| Spec ID | What We Need From It | Why |
|---|---|---|
| (none) | — | Forecast builds entirely on existing production infrastructure |
5.2 External Dependencies
Section titled “5.2 External Dependencies”| Dependency | Owner | Status | Blocker? |
|---|---|---|---|
| Neo4j purchase graph (prod) | consumer-graph-worker | Live | No |
| CCS enrichment API (prod) | consumer-context-service | Live | No |
| consumer-agent episode builder | consumer-agent | Live | No |
| notification-service | notification-service | Live | No |
| offer-shelf component rendering (iOS) | Mobile | Live | No |
| product-card component rendering (iOS) | Mobile | Live | No |
Feature Flipper weekly_forecast_enabled | Platform | Not created | No (trivial) |
DynamoDB table {env}-weekly-forecasts | Infra (FSD) | Not created | No (FSD YAML) |
6. Risks & Open Questions
Section titled “6. Risks & Open Questions”| # | Risk / Question | Impact | Mitigation / Answer |
|---|---|---|---|
| 1 | Neo4j load during batch forecast generation | Medium | Run during off-peak (3 AM UTC, same as nudges). Stagger users. Neo4j prod (r6i.xlarge, 32GB) is at <10% CPU today. |
| 2 | Offer staleness — offers expire mid-week after forecast is created | Low | offer-shelf handles expired offers gracefully (they just don’t render). Phase 2 adds manual refresh. |
| 3 | Offer-shelf requires 3+ items per tier — some tiers may have < 3 | Medium | Fall back to product-card for tiers with < 3 items. Document in episode assembly logic. |
| 4 | CCS enrichment volume (~45K users × 15 items = ~675K enrichments) | Medium | Existing FPS/FIDORA/Button caches absorb most lookups. Batch enrich. Spread over 30-min window. |
| 5 | How many items per forecast? | Open | PRD doesn’t specify. Recommend 8-15 max. Too many = cognitive overload. Too few = not useful. |
| 6 | Cold start users (< 4 receipts) | Open | Out of scope for Phase 1. Follow-up spec for popular items + active offers fallback. |
| 7 | Relationship to individual repurchase nudges | Open | Recommend coexist for Phase 1 — nudges are daily single-product, forecast is weekly multi-product. Consolidation in Phase 2. |
| 8 | Manual refresh — how does it work? | Open | User says “refresh my forecast” → consumer-agent tool call → CCS triggers re-generation for that user → new episode created. |
7. Testing Strategy
Section titled “7. Testing Strategy”- Unit tests: Tier classification logic, shopping day detection query parsing, episode assembly (markdown + component ordering), points calculation.
- Integration tests:
- ForecastHandler end-to-end with mock Neo4j + mock CCS → verify episode structure
- DynamoDB forecast persistence and retrieval
- Shopping day detection against seeded Neo4j data
- Manual validation:
- Trigger forecast for test user in staging → verify push received → open episode → confirm offer-shelf and product-card render correctly
- Verify offer-shelf items are tappable and activate offers
- Verify product-card items link to Fetch Shop
- Test with < 3 items in a tier → confirm fallback to product-cards
8. Rollout & Observability
Section titled “8. Rollout & Observability”8.1 Rollout Plan
Section titled “8.1 Rollout Plan”- Deploy
ForecastHandlerwithweekly_forecast_enabledflag OFF - Enable for 5 internal test users → validate episode rendering, push delivery
- Enable for 100 users (1-week soak) → monitor engagement, offer activation, points earned
- Ramp to 1K → 10K → full cohort over 3 weeks
- Monitor 7-day repeat rate, turns per conversation, offer activation per forecast view
8.2 Metrics & Alerts
Section titled “8.2 Metrics & Alerts”| Metric | Source | Alert Threshold |
|---|---|---|
forecast_generated (counter) | consumer-graph-worker | < 80% of enrolled users per run |
forecast_generation_duration_ms | consumer-graph-worker | p95 > 30 minutes total |
forecast_episode_created (counter) | consumer-agent | Divergence from forecast_generated > 5% |
forecast_push_accepted (counter) | notification-service | < 70% acceptance rate |
forecast_tier_distribution (histogram) | consumer-graph-worker | Stacked tier < 10% of items (offer/Shop data issue) |
| Neo4j CPU during forecast window | Grafana | > 30% (currently < 10%) |
8.3 Rollback Plan
Section titled “8.3 Rollback Plan”- Set
weekly_forecast_enabledto OFF — stops all forecast generation immediately - Existing episodes remain viewable (they’re just offer-shelves and product-cards)
- No data migration needed — DynamoDB table can be left in place or dropped
9. Appendix
Section titled “9. Appendix”PRD Reference
Section titled “PRD Reference”PRD: Weekly Forecast — Personalized Shopping List in the AI Rewards Assistant
Key PRD goals:
- Increase 7-day repeat rate from 6.9% → 15%+
- Increase avg turns per conversation from 1.75 → 3.0+
- Drive 2+ offer activations per forecast view per week
Related PRDs
Section titled “Related PRDs”- PRD: Proactive Repurchase Nudges (PLT-191) — foundation pipeline, live in prod
- PRD: Post-Scan Earnings Coach (PLT-535) — companion feature
- PRD: Offer Autopilot (PLT-537) — companion feature
Engagement Flywheel
Section titled “Engagement Flywheel”The Weekly Forecast is part of a three-feature system designed to create a recurring engagement loop:
- User scans receipt → Post-Scan Earnings Coach shows what they earned/missed
- Offer Autopilot auto-activates missed offers
- Weekly Forecast shows optimized shopping list for next week
- User shops with activated offers → back to step 1
Research Document
Section titled “Research Document”Full implementation analysis with capability map and gap assessment: docs/rfd-weekly-forecast.md