package models import "time" // DemandForecast is one zone-day the engine expects. // // ─── Why this is stored rather than computed on read ─────────────────────── // // The forecast is produced in Python (AI_engine/prediction), because the model // is: Prophet where it beats a seasonal baseline, the baseline otherwise. The // console and any staffing decision need it in Go. So the engine writes here // and the backend serves it — the same split the agent registry and the // decision log already use, and the reason the /internal/* surface exists. // // It is also the honest shape: a forecast is a thing produced at a moment by a // model, not a function of the current table. Recomputing it on every read // would make yesterday's number unrecoverable, which is exactly what you want // when asking "was the forecast any good". type DemandForecast struct { Forecastid int `json:"forecastid" gorm:"primaryKey;column:forecastid;autoIncrement"` // Zone is the first three digits of a pickup pincode — the same grain the // hub console scopes on (pickuppincode LIKE '641%') and the same grain // internal/prediction calibrates ETA at. Keeping one definition of "zone" // across both is deliberate. Zone string `json:"zone" gorm:"column:zone;size:8;not null;uniqueIndex:uq_demandforecast_zone_day,priority:1"` // Forday is the day being predicted, not the day it was predicted on. Forday time.Time `json:"forday" gorm:"column:forday;not null;uniqueIndex:uq_demandforecast_zone_day,priority:2;index"` Expectedbookings int `json:"expectedbookings" gorm:"column:expectedbookings;not null"` // Model and Reason record WHICH model produced this and why it was chosen — // "prophet beat the weekly baseline over 12 folds (18% lower MAE)", or // "prophet is not installed in this image". Stored because the choice is // made per zone from that zone's own history, so without it nobody can tell // whether a bad forecast came from a bad model or from a thin series. Model string `json:"model" gorm:"column:model;size:32;not null"` Reason string `json:"reason" gorm:"column:reason"` // Observations is how many days of history the forecast was fitted on, and // Baselinemae/Modelmae are the backtest scores. A forecast with 31 // observations and a model barely beating the baseline deserves less trust // than one with 400, and this is what lets a reader see that rather than // taking the number at face value. Observations int `json:"observations" gorm:"column:observations;default:0"` Baselinemae *float64 `json:"baselinemae" gorm:"column:baselinemae"` Modelmae *float64 `json:"modelmae" gorm:"column:modelmae"` Generatedat time.Time `json:"generatedat" gorm:"column:generatedat;not null"` } func (DemandForecast) TableName() string { return "demandforecast" }