groceries: stats page with nightly snapshot (battles, basket, promos, CPI, range)
All checks were successful
Deploy production / deploy (push) Successful in 1m19s

This commit is contained in:
2026-10-25 20:38:55 +00:00
parent c4397e9d59
commit a6ef263c7e
11 changed files with 35731 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import {
date,
index,
integer,
jsonb,
numeric,
real,
text,
@@ -277,6 +278,51 @@ export const grocerySynonyms = groceriesSchema.table(
]
);
/**
* Nightly analytics for /trackers/groceries/stats (computeStats in
* src/lib/server/groceries/stats.ts, run after import + regroup + taxonomy under the import lock).
* One row per day per scope ('food' = edible only, 'all' = incl. drugstore/household). `data` holds
* the overall section plus one per top category; `series` (daily chained price index) is only kept
* on recent rows, older rows are pruned to the overall section so history stays small.
*/
export const groceryStatsSnapshots = groceriesSchema.table(
'stats_snapshots',
{
id: id(),
computedOn: date('computed_on').notNull(),
scope: text('scope').notNull(),
computedAt: timestamptz('computed_at').notNull().defaultNow(),
durationMs: integer('duration_ms'),
data: jsonb('data').notNull(),
series: jsonb('series')
},
(t) => [
uniqueIndex('stats_snapshots_day_scope_key').on(t.computedOn, t.scope),
check('stats_snapshots_scope_check', sql`${t.scope} IN ('food','all')`)
]
);
/**
* Pack size history (a row only when an item's size changes), written by computeStats so
* shrinkflation (smaller pack, same or higher price) can be detected. item_prices has no sizes.
*/
export const groceryItemSizes = groceriesSchema.table(
'item_sizes',
{
id: id(),
itemId: integer('item_id')
.notNull()
.references(() => groceryItems.id, { onDelete: 'cascade' }),
observedOn: date('observed_on').notNull(),
unit: text('unit').$type<GroceryUnit>(),
unitAmount: numeric('unit_amount', { precision: 12, scale: 4, mode: 'number' }),
sizeText: text('size_text'),
priceCents: integer('price_cents'),
createdAt: timestamptz('created_at').notNull().defaultNow()
},
(t) => [uniqueIndex('item_sizes_item_day_key').on(t.itemId, t.observedOn)]
);
export type GroceryStore = typeof groceryStores.$inferSelect;
export type GroceryGroup = typeof groceryGroups.$inferSelect;
export type GroceryItem = typeof groceryItems.$inferSelect;