openspec: sms-metering-location-link change

This commit is contained in:
2026-08-08 14:34:23 +10:00
parent 27826c6d93
commit db1c358fe7
7 changed files with 142 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
## Context
docs/PRODUCT-MODEL.md established: SMS is the only real variable cost (~6¢ prepaid), plans meter it (10 SMS/yr per tag, family pools), and the alert SMS should carry a short link to a map page rather than raw coordinates. This change implements metering + the location-link flow. The existing throttles (250 m/10 min, fingerprint 24 h, daily/IP caps) remain — metering is an additional, separate gate.
## Goals / Non-Goals
**Goals:**
- Metered tags stop sending when credits exhaust (scan recording continues).
- Every alert-with-location SMS carries a short URL to a map page.
- Admin can set/refill credits (Filament field) and see usage.
**Non-Goals:**
- Renewal/expiry automation + unlock messaging ("pay to renew") — next slice (needs email infra).
- Family pooled SMS across accounts — later (per-tag metering now).
- Top-up purchases (Stripe) — manual admin top-ups now.
- i18n on the location page — later.
## Decisions
1. **Metering = per-tag, `0 = unmetered`.** `tags.sms_allocated/used/period_start` (idempotent). `sms_allocated = 0` skips credit checks → existing/transitional tags unaffected until an admin assigns a plan. `sms_period_start` records the anchor for the (future) renewal job.
2. **Count only successful sends.** `alertOwner` and the finder-contact send each increment `sms_used` only after the sender returns success. Both count (per the product model: contact SMS uses the same meter).
3. **Short codes**: 6-char base36 random, unique (`url_shortened.code` PK); created at alert time when location is present; maps `code → scan_id`. `GET /s/{code}` loads the scan and renders the location page.
4. **SMS message**: with location, replace the raw maps URL with `where-woof.com/s/{code}` (short link). Without location, message unchanged (no link).
5. **Location page** (`location.html` template set): Google Maps embed iframe (`https://www.google.com/maps?q={lat},{lng}&output=embed`), finder phone if present, tag-page link, scan timestamp. Private by unguessable code.
6. **Admin**: TagResource — `sms_allocated` (numeric, editable = top-up), `sms_used` (view-only), `sms_period_start` (view). Eloquent already maps the new columns (fillable + cast).
## Risks / Trade-offs
- [Unmetered default means metering isn't enforced until admin configures] → intentional transitional state; the mechanism + admin field ship now, plan assignment comes with the billing slice.
- [Short code collision] → retry loop on insert (regenerate up to 3×).
- [Location page exposes finder location to anyone with the code] → codes are 6-char random (unguessable); validity window considered a later concern.
## Migration Plan
1. `make db-up` (ALTERs + `url_shortened` table).
2. sqlc queries; Go handlers; template; admin field.
3. Verify (new suite + regressions); deploy (frontend binary + restart; admin rebuild).
## Open Questions
- Whether contact-SMS should also get the short link — decided yes for consistency once the page exists (it shows the same location).