Spaces:
Running
Running
File size: 5,326 Bytes
5af92d0 119e471 5af92d0 5d554bf 5af92d0 85ed433 5af92d0 119e471 85ed433 5af92d0 85ed433 5af92d0 85ed433 ccc44ff 85ed433 ccc44ff 75360ad 5af92d0 85ed433 5af92d0 ccc44ff 5af92d0 119e471 5af92d0 d2525ce ccc44ff d2525ce 119e471 d2525ce | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | // 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.'
}
|