# Cron Push Alerts Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add three cron-secret HTTP endpoints that write inbox rows and send FCM for rain, grouped mall activity, and the 6AM crop-price ping.

**Architecture:** `Internal::AlertsController` checks `X-Cron-Secret` against `ENV["CRON_SECRET"]`. Each action calls one runner (`WeatherAlertRunner`, `MallAlertRunner`, `CropPriceAlertRunner`). Runners group work, call `NotificationRecorder.create!` with title/body/`data.screen`, and skip when a recent row of that kind already exists. Call/share/boost no longer create inbox+FCM; they stay on `mall_engagements`. Listing show records one `view` per user per day.

**Tech Stack:** Rails 7.1, RSpec, FactoryBot, existing `FcmSender` / `NotificationRecorder`, Open-Meteo HTTP, GitHub Actions cron.

## Global Constraints

- Auth header is `X-Cron-Secret`; value is `CRON_SECRET` (`GrowKit-` + hex). No user JWT on these three routes.
- FCM `data` values are strings. Include `screen`, `type`, `notificationId`.
- No per-tap call/share/boost/view push. Offers and reports stay instant.
- Dedup: rain 18h, mall digest window 24h, reminder once per unread digest, quiet listing 7 days, empty stall 3 days, crop prices once per IST day.
- Open-Meteo only for weather. No data.gov in this pass.
- Copy is English v1 from `docs/superpowers/specs/2026-09-20-cron-push-alerts-design.md`.

## File map

- Create: `app/controllers/internal/alerts_controller.rb`
- Create: `app/services/open_meteo_forecast.rb`
- Create: `app/services/weather_alert_runner.rb`
- Create: `app/services/mall_alert_runner.rb`
- Create: `app/services/crop_price_alert_runner.rb`
- Create: `app/services/listing_view_recorder.rb`
- Create: `spec/requests/internal_alerts_spec.rb`
- Create: `spec/services/weather_alert_runner_spec.rb`
- Create: `spec/services/mall_alert_runner_spec.rb`
- Create: `spec/requests/mall_listing_views_spec.rb`
- Create: `.github/workflows/cron-alerts.yml`
- Modify: `config/routes.rb`, `app/models/notification.rb`, `app/services/notification_recorder.rb`, `app/models/mall_engagement.rb`, `app/controllers/mall_listings_controller.rb`, `spec/services/notification_recorder_spec.rb`, `docs/api.md`, `docs/app-contract.md`

---

### Task 1: Cron-secret routes and `{ ok, sent }`

**Files:** routes, `Internal::AlertsController`, `spec/requests/internal_alerts_spec.rb`

- [ ] Failing request spec: missing header → 401; wrong secret → 401; valid secret on weather/mall/crop-prices → 200 `{ "ok": true, "sent": 0 }` when nobody qualifies.
- [ ] Implement controller `require_cron_secret!` with `secure_compare`. Blank `CRON_SECRET` → 401.
- [ ] Routes: `POST /internal/alerts/weather`, `/mall`, `/crop-prices`.

### Task 2: NotificationRecorder supports cron kinds + screen data

**Files:** `app/models/notification.rb`, `app/services/notification_recorder.rb`

- [ ] Add kinds: `weather_rain mall_digest mall_reminder listing_quiet mall_nudge crop_prices`.
- [ ] `create!` accepts `title:`, `body:`, `data:`.
- [ ] FCM data includes `screen` and `type`. Inbox `count` for `mall_digest`.
- [ ] `from_engagement!` records nothing (call/share/boost stay in `mall_engagements` only).
- [ ] Update `notification_recorder_spec`: engagement does not deliver FCM; `create!` with screen does.

### Task 3: Weather runner

**Files:** `open_meteo_forecast.rb`, `weather_alert_runner.rb`, specs

- [ ] Group users with device + active lat/lng by 2-decimal coords. One Open-Meteo fetch per group.
- [ ] Rain if any hour in 6–24h has probability ≥ 50 or precipitation ≥ 0.5.
- [ ] Dedup 18h. Copy uses village + evening/morning/afternoon.
- [ ] Stub `OpenMeteoForecast.fetch` in tests. Two users same rounded lat/lng → one HTTP fetch, two pushes.

### Task 4: Mall runner + listing views

**Files:** `mall_alert_runner.rb`, `listing_view_recorder.rb`, `mall_engagement.rb`, `mall_listings_controller.rb`

- [ ] `GET /mall/listings/:id` records unique IST-day `view` when signed-in and not owner.
- [ ] Digest: unique actors per type since last `mall_digest` or 24h. One push per seller. `screen: listing` if one listing else `inbox`.
- [ ] Unread `mall_digest` older than 24h → one `mall_reminder`.
- [ ] Quiet listing: active, age > 24h, zero engagements in 24h, one per 7 days.
- [ ] Empty stall: signed-up, zero listings, device, no `mall_nudge` in 3 days.

### Task 5: Crop prices + GitHub cron + docs

- [ ] Broadcast to signed-up users with a device; one per IST day; `screen: crop_prices`.
- [ ] GitHub Actions POSTs the three URLs with `X-Cron-Secret: ${{ secrets.CRON_SECRET }}`.
- [ ] Document in `docs/api.md` and `docs/app-contract.md`.
