openspec: sms-metering-location-link change
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-08
|
||||
3
openspec/changes/sms-metering-location-link/README.md
Normal file
3
openspec/changes/sms-metering-location-link/README.md
Normal file
@@ -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
|
||||
41
openspec/changes/sms-metering-location-link/design.md
Normal file
41
openspec/changes/sms-metering-location-link/design.md
Normal 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).
|
||||
30
openspec/changes/sms-metering-location-link/proposal.md
Normal file
30
openspec/changes/sms-metering-location-link/proposal.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
24
openspec/changes/sms-metering-location-link/tasks.md
Normal file
24
openspec/changes/sms-metering-location-link/tasks.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user