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.'
}