3.0 KiB
3.0 KiB
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
- Metering = per-tag,
0 = unmetered.tags.sms_allocated/used/period_start(idempotent).sms_allocated = 0skips credit checks → existing/transitional tags unaffected until an admin assigns a plan.sms_period_startrecords the anchor for the (future) renewal job. - Count only successful sends.
alertOwnerand the finder-contact send each incrementsms_usedonly after the sender returns success. Both count (per the product model: contact SMS uses the same meter). - Short codes: 6-char base36 random, unique (
url_shortened.codePK); created at alert time when location is present; mapscode → scan_id.GET /s/{code}loads the scan and renders the location page. - SMS message: with location, replace the raw maps URL with
where-woof.com/s/{code}(short link). Without location, message unchanged (no link). - Location page (
location.htmltemplate 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. - 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
make db-up(ALTERs +url_shortenedtable).- sqlc queries; Go handlers; template; admin field.
- 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).