diff --git a/openspec/changes/sms-metering-location-link/.openspec.yaml b/openspec/changes/sms-metering-location-link/.openspec.yaml new file mode 100644 index 0000000..913564e --- /dev/null +++ b/openspec/changes/sms-metering-location-link/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-08 diff --git a/openspec/changes/sms-metering-location-link/README.md b/openspec/changes/sms-metering-location-link/README.md new file mode 100644 index 0000000..43c4674 --- /dev/null +++ b/openspec/changes/sms-metering-location-link/README.md @@ -0,0 +1,3 @@ +# sms-metering-location-link + +SMS credits per tag (allocated/used, admin top-ups) + short URL + owner location page (map embed + finder details); credits exhaust -> block diff --git a/openspec/changes/sms-metering-location-link/design.md b/openspec/changes/sms-metering-location-link/design.md new file mode 100644 index 0000000..9bf663c --- /dev/null +++ b/openspec/changes/sms-metering-location-link/design.md @@ -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). diff --git a/openspec/changes/sms-metering-location-link/proposal.md b/openspec/changes/sms-metering-location-link/proposal.md new file mode 100644 index 0000000..8a69a17 --- /dev/null +++ b/openspec/changes/sms-metering-location-link/proposal.md @@ -0,0 +1,30 @@ +## Why + +The product model (docs/PRODUCT-MODEL.md) makes SMS the only real variable cost, metered per plan. This change delivers the two revenue-critical pieces: **SMS credit metering** (allocated vs used per tag, admin top-ups, block when exhausted) and the **location-link flow** (alert SMS carries a short URL → a map page with the finder's location + details). This is the slice that makes the $5/10-SMS plans enforceable and gives owners the "where is my dog" page. + +## What Changes + +- **Metering schema**: `tags` gains `sms_allocated`, `sms_used`, `sms_period_start` (idempotent ALTERs). **`sms_allocated = 0` means unmetered/transitional** (existing tags keep working); a positive allocation is enforced. +- **Metering in the alert path**: when an alert or finder-contact SMS is about to be sent, `sms_used < sms_allocated` is required (only when allocation > 0). Successful sends increment `sms_used`. Exhausted → scans recorded, no SMS (unlock/renewal messaging is a follow-up slice — needs email). +- **Short URLs**: `url_shortened` table (code → scan_id). Alert SMS with a location now links `https://where-woof.com/s/{code}` instead of the full maps URL (keeps SMS in budget, hides raw coords). +- **Location page**: `GET /s/{code}` renders the owner's location page — embedded Google Map of the finder's position, finder's phone (if left), tag-page link, timestamp. Keyed by the unguessable code. +- **Admin**: TagResource gains SMS credit fields (edit `sms_allocated` = manual top-up; view `sms_used`). + +## Capabilities + +### New Capabilities + +- `sms-metering`: per-tag credit allocation/usage, enforcement in the alert path, admin top-up. +- `location-link`: short URLs + owner location page (map embed + finder details). + +### Modified Capabilities + +- `sms-alerting`: ADDED — credits required; sent-SMS counter; SMS message uses the short link. +- `database`: ADDED — tags credit columns + `url_shortened` table. + +## Impact + +- **Go**: scan.go (credit checks + counter + short-link in message), new `shortlink.go` handler + `location` template set, queries (GetShortCode, InsertShortCode, GetScanByID, AddSmsUsed), config none. +- **Schema**: idempotent ALTERs + `url_shortened` table; `make db-up`. +- **Admin**: TagResource fields. +- **SMS message change**: location line becomes the short link. diff --git a/openspec/changes/sms-metering-location-link/specs/location-link/spec.md b/openspec/changes/sms-metering-location-link/specs/location-link/spec.md new file mode 100644 index 0000000..63f83ac --- /dev/null +++ b/openspec/changes/sms-metering-location-link/specs/location-link/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Short URL per alert +When an alert SMS is sent with a location, a short code SHALL be created mapping to the scan, and the SMS SHALL contain `https://where-woof.com/s/{code}` as the location link. + +#### Scenario: Alert with location +- **WHEN** an alert is sent for a scan with coordinates +- **THEN** a short code exists for the scan and the SMS links to it + +### Requirement: Location page +`GET /s/{code}` SHALL render the owner's location page: embedded map of the scan's coordinates, the finder's phone if provided, a link to the tag page, and the scan time. + +#### Scenario: Owner opens the link +- **WHEN** the owner opens a valid short link +- **THEN** the map and finder details are shown + +#### Scenario: Unknown code +- **WHEN** the code does not exist +- **THEN** a not-found response is returned diff --git a/openspec/changes/sms-metering-location-link/specs/sms-metering/spec.md b/openspec/changes/sms-metering-location-link/specs/sms-metering/spec.md new file mode 100644 index 0000000..35f0081 --- /dev/null +++ b/openspec/changes/sms-metering-location-link/specs/sms-metering/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Per-tag SMS credits +Each tag SHALL track `sms_allocated`, `sms_used`, and `sms_period_start`. An allocation of 0 SHALL mean unmetered (no credit limit). + +#### Scenario: Allocated plan +- **WHEN** an admin sets `sms_allocated = 10` on a tag +- **THEN** the tag may send up to 10 SMS in the period + +#### Scenario: Unmetered default +- **WHEN** a tag has `sms_allocated = 0` +- **THEN** credit checks do not apply (transitional behaviour) + +### Requirement: Credits required to send +Before sending an alert or finder-contact SMS on a metered tag, `sms_used < sms_allocated` SHALL hold. Each successful send SHALL increment `sms_used`. When exhausted, scans are recorded but no SMS is sent. + +#### Scenario: Send consumes a credit +- **WHEN** an alert SMS is sent on a metered tag +- **THEN** `sms_used` increments by one + +#### Scenario: Credits exhausted +- **WHEN** `sms_used >= sms_allocated` on a metered tag +- **THEN** further scans record but no SMS is sent diff --git a/openspec/changes/sms-metering-location-link/tasks.md b/openspec/changes/sms-metering-location-link/tasks.md new file mode 100644 index 0000000..b7be098 --- /dev/null +++ b/openspec/changes/sms-metering-location-link/tasks.md @@ -0,0 +1,24 @@ +## 1. Schema & Queries + +- [ ] 1.1 `db/schema.sql`: idempotent ALTERs `tags.sms_allocated/used/period_start`; `url_shortened` table; `make db-up` +- [ ] 1.2 `queries.sql`: `InsertShortCode`, `GetShortCode` (:one), `GetScanByID` (:one), `AddSmsUsed` (:exec, `sms_used = sms_used + 1`); `make generate`; build + +## 2. Metering + +- [ ] 2.1 `scan.go` shouldAlert: credit check (`SmsAllocated > 0 && SmsUsed >= SmsAllocated` → false); after successful alert send → `AddSmsUsed` +- [ ] 2.2 `FinderContact`: same credit check + increment on successful send + +## 3. Location Link + +- [ ] 3.1 Short-code generator + `InsertShortCode` at alert time (when location present); `alertMessage` uses `where-woof.com/s/{code}` +- [ ] 3.2 `ServeShort` handler (`GET /s/{code}` → scan → location page); `location.html` template (map iframe + finder phone + tag link + time); register route + template set + +## 4. Admin + +- [ ] 4.1 TagResource: `sms_allocated` (numeric, editable), `sms_used` (view), `sms_period_start` (view) + +## 5. Verification + +- [ ] 5.1 New suite: metered tag with credits → alert + credit consumed; exhausted → no alert (scan recorded); unmetered (0) → alerts; alert SMS contains `where-woof.com/s/`; short link → location page 200 with map + finder details; unknown code → not found +- [ ] 5.2 Regressions (22+19+12+6) green; deploy frontend + admin; live check +- [ ] 5.3 `openspec validate sms-metering-location-link`; commit