Skip to content

Weekly Forecast — Personalized Shopping List

Weekly Forecast — Personalized Shopping List

Section titled “Weekly Forecast — Personalized Shopping List”

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.

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.

  1. 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.
  2. Build as consumer-graph-worker handler — same architecture as NotificationHandler, not a new service.
  3. Three earning tiers — Stacked (offer+Shop), Offer-only, Shop-only. Items with no earning opportunity are excluded.
  4. PRD: Weekly Forecast PRD by Josh Gunning (2026-03-25, Draft)
TermDefinition
StackedItem has both an active offer AND is available on Fetch Shop — maximum earning potential
Offer-onlyItem has an active offer but is not on Fetch Shop — earn by buying anywhere and scanning receipt
Shop-onlyItem is on Fetch Shop but has no active offer — earn PPD points by buying through Shop
ForecastA personalized, time-bound list of predicted purchases ranked by earning tier
Shopping dayThe day of week a user most frequently makes purchases (detected from receipt history)
EpisodeA persistent, structured conversation entry in consumer-agent with components and text
  1. The system MUST generate a weekly forecast for each enrolled user containing 8-15 predicted purchase items.
  2. The system MUST classify each item into one of three tiers: Stacked (offer + Shop), Offer-only, or Shop-only.
  3. The system MUST exclude items with no earning opportunity (no offer AND not on Shop).
  4. The system MUST sort items by tier priority (Stacked > Offer > Shop), then by expected value within each tier.
  5. 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
  6. The system MUST detect each user’s peak shopping day from their 90-day purchase history.
  7. The system MUST generate the forecast 2 days before the user’s detected shopping day.
  8. 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”.
  9. The system MUST persist forecast metadata to DynamoDB for retrieval and analytics.
  10. The system MUST be gated behind the weekly_forecast_enabled feature flag.
  11. The system SHOULD support manual refresh via user request in chat (re-runs forecast generation for that user).
  12. The system MAY fall back to Monday generation if no peak shopping day is detectable.
  1. Forecast generation for the full user cohort (~45K users) MUST complete within 30 minutes.
  2. CCS enrichment MUST reuse existing caches (FPS, FIDORA, Button) — no cold enrichment of all items.
  3. DynamoDB forecast reads MUST respond in < 50ms at p95.
  4. Push notification delivery MUST use existing notification-service guardrails (quiet hours, quota, dedup).
  5. The system MUST NOT exceed current Neo4j resource utilization by more than 20% during forecast generation.
  • 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.
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_start

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 freq
RETURN dow, freq
ORDER BY freq DESC
LIMIT 1

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/week
  • 403: User not enrolled in weekly_forecast_enabled

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.
Spec IDWhat We Need From ItWhy
(none)Forecast builds entirely on existing production infrastructure
DependencyOwnerStatusBlocker?
Neo4j purchase graph (prod)consumer-graph-workerLiveNo
CCS enrichment API (prod)consumer-context-serviceLiveNo
consumer-agent episode builderconsumer-agentLiveNo
notification-servicenotification-serviceLiveNo
offer-shelf component rendering (iOS)MobileLiveNo
product-card component rendering (iOS)MobileLiveNo
Feature Flipper weekly_forecast_enabledPlatformNot createdNo (trivial)
DynamoDB table {env}-weekly-forecastsInfra (FSD)Not createdNo (FSD YAML)
#Risk / QuestionImpactMitigation / Answer
1Neo4j load during batch forecast generationMediumRun during off-peak (3 AM UTC, same as nudges). Stagger users. Neo4j prod (r6i.xlarge, 32GB) is at <10% CPU today.
2Offer staleness — offers expire mid-week after forecast is createdLowoffer-shelf handles expired offers gracefully (they just don’t render). Phase 2 adds manual refresh.
3Offer-shelf requires 3+ items per tier — some tiers may have < 3MediumFall back to product-card for tiers with < 3 items. Document in episode assembly logic.
4CCS enrichment volume (~45K users × 15 items = ~675K enrichments)MediumExisting FPS/FIDORA/Button caches absorb most lookups. Batch enrich. Spread over 30-min window.
5How many items per forecast?OpenPRD doesn’t specify. Recommend 8-15 max. Too many = cognitive overload. Too few = not useful.
6Cold start users (< 4 receipts)OpenOut of scope for Phase 1. Follow-up spec for popular items + active offers fallback.
7Relationship to individual repurchase nudgesOpenRecommend coexist for Phase 1 — nudges are daily single-product, forecast is weekly multi-product. Consolidation in Phase 2.
8Manual refresh — how does it work?OpenUser says “refresh my forecast” → consumer-agent tool call → CCS triggers re-generation for that user → new episode created.
  • 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
  1. Deploy ForecastHandler with weekly_forecast_enabled flag OFF
  2. Enable for 5 internal test users → validate episode rendering, push delivery
  3. Enable for 100 users (1-week soak) → monitor engagement, offer activation, points earned
  4. Ramp to 1K → 10K → full cohort over 3 weeks
  5. Monitor 7-day repeat rate, turns per conversation, offer activation per forecast view
MetricSourceAlert Threshold
forecast_generated (counter)consumer-graph-worker< 80% of enrolled users per run
forecast_generation_duration_msconsumer-graph-workerp95 > 30 minutes total
forecast_episode_created (counter)consumer-agentDivergence from forecast_generated > 5%
forecast_push_accepted (counter)notification-service< 70% acceptance rate
forecast_tier_distribution (histogram)consumer-graph-workerStacked tier < 10% of items (offer/Shop data issue)
Neo4j CPU during forecast windowGrafana> 30% (currently < 10%)
  1. Set weekly_forecast_enabled to OFF — stops all forecast generation immediately
  2. Existing episodes remain viewable (they’re just offer-shelves and product-cards)
  3. No data migration needed — DynamoDB table can be left in place or dropped

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

The Weekly Forecast is part of a three-feature system designed to create a recurring engagement loop:

  1. User scans receipt → Post-Scan Earnings Coach shows what they earned/missed
  2. Offer Autopilot auto-activates missed offers
  3. Weekly Forecast shows optimized shopping list for next week
  4. User shops with activated offers → back to step 1

Full implementation analysis with capability map and gap assessment: docs/rfd-weekly-forecast.md