Spaces:
Running
Running
| // Lawn Estimator β quote metering / usage | |
| // | |
| // Render at https://dbdiagram.io β render this file on its own (a `tenants` stub below lets it | |
| // stand alone). tenants.dbml holds the full `tenants` table; don't concatenate the two files. | |
| // | |
| // STORAGE: LIVE via db.RunLedger over db.Database, which picks the backend at startup by | |
| // DATABASE_URL (P1d): Postgres (Neon) in prod β durable across redeploys β else a local | |
| // SQLite file (:memory: in tests). The `runs` table below replaced the in-memory | |
| // MeteringStore, so the billing rule + per-tenant usage persist. One row per completed | |
| // quote, so billing AND ops (plan Β§6.7) share one source of truth. The SQL is written once; | |
| // Database papers over the SQLite/Postgres dialect (placeholders, id/timestamp types). | |
| Project lawn_estimator_metering { | |
| database_type: 'PostgreSQL' | |
| Note: 'Per-tenant quote metering + run ledger. Anti-arbitrage: every surface records here.' | |
| } | |
| // `tenants` is defined FULLY in tenants.dbml. dbdiagram.io loads one file at a time, so this | |
| // minimal stub lets THIS file render standalone. Don't paste both files together (that would | |
| // double-define `tenants`). | |
| Table tenants { | |
| id varchar [pk, note: 'Stub for standalone rendering β full definition in tenants.dbml.'] | |
| } | |
| Table runs { | |
| id integer [pk, increment] | |
| tenant_id varchar [not null, ref: > tenants.id] | |
| surface varchar [not null, note: 'single (widget / single /quote / API) | batch'] | |
| address varchar [not null, note: 'geocoder formatted address'] | |
| address_norm varchar [not null, note: 'case/space-normalized address β the dedup key for single quotes'] | |
| zip varchar [null, note: 'resolved 5-digit ZIP'] | |
| billable boolean [not null, note: 'batch β always true (duplicates included); single β true only the first time this tenant quotes this address (a customer re-submit is free; becomes "cache miss" once the measurement cache lands)'] | |
| method varchar [null, note: 'lidar+rgb | rgb-only (observability)'] | |
| confidence varchar [null, note: 'high | medium (observability)'] | |
| lawn_sqft decimal [null, note: 'measured lawn area β the original for a 2e edit, and a billing/analytics input'] | |
| duration_s decimal [null, note: 'end-to-end pipeline seconds (observability / latency)'] | |
| result_id varchar [null, note: 'links to the viz / overlay result; the 2e edit finds a run by this'] | |
| outcome varchar [not null, default: 'ok', note: 'ok | rejected (client bad/out-of-area address) | error (our pipeline) β P3 observability'] | |
| error_class varchar [null, note: 'the exception type on a failed run'] | |
| created_at timestamp [not null, default: `now()`] | |
| indexes { | |
| (tenant_id, address_norm) [note: 'single-quote dedup + per-tenant usage rollup'] | |
| (tenant_id, created_at) [note: 'usage within a billing period'] | |
| (tenant_id, result_id) [note: 'measurement-edit lookup (2e)'] | |
| } | |
| Note: 'One completed quote (out-of-area / failed quotes are NOT recorded yet β recording failures with an outcome/error column is a P3 observability add). Billable usage for a tenant = COUNT(*) WHERE billable within the billing window. Doubles as the run ledger (plan Β§6.7); add outcome / cost columns as observability + billing land.' | |
| } | |
| Table measurement_edits { | |
| id integer [pk, increment] | |
| tenant_id varchar [not null, ref: > tenants.id] | |
| result_id varchar [not null, note: 'the runs row this corrects; must be a surface=batch run (enforced at the endpoint β customer/widget quotes are LOCKED)'] | |
| address varchar [null] | |
| original_sqft decimal [null, note: 'the measured area before the override (from runs.lawn_sqft)'] | |
| edited_sqft decimal [not null, note: 'the operator-corrected area; the quote is re-priced on this'] | |
| user_id integer [null, note: 'the operator (users.id β see dashboard-auth.dbml)'] | |
| created_at timestamp [not null, default: `now()`] | |
| indexes { | |
| (tenant_id, created_at) | |
| } | |
| Note: 'Operator overrides of BATCH-quote measured areas (2e). Append-only β original vs edited is the Β§3.2 accuracy record. Customer/widget quotes are never editable.' | |
| } | |
| Table leads { | |
| id integer [pk, increment] | |
| tenant_id varchar [not null, ref: > tenants.id] | |
| source varchar [not null, note: 'widget (lead popup) | quote (a /quote carrying an email) | batch'] | |
| address varchar [not null] | |
| name varchar [null] | |
| email varchar [null] | |
| phone varchar [null] | |
| zip varchar [null, note: 'resolved 5-digit ZIP (only when captured after geocode)'] | |
| lawn_sqft decimal [null, note: 'null when captured before measurement (widget popup fires in parallel)'] | |
| price_total decimal [null, note: 'bookable total quoted, when pricing was computed'] | |
| confidence varchar [null, note: 'high | medium β the measurement confidence'] | |
| result_id varchar [null, note: 'links to the viz / overlay result'] | |
| created_at timestamp [not null, default: `now()`] | |
| indexes { | |
| (tenant_id, created_at) [note: 'the dashboard leads list, newest first (P2c)'] | |
| } | |
| Note: 'Captured leads (P1b, db.LeadStore). APPEND-ONLY: a row is written the moment the widget collects contact (before measurement β sqft/price/zip null) AND when a /quote carries an email (after measurement β filled). Dedup/merge is a dashboard concern (P2c), not enforced here. The partner notification inbox stays the redundant durable store; this table is the queryable one.' | |
| } | |