Files
backend_fiesta/models/catalogueupload.go
2026-08-31 14:59:48 +05:30

112 lines
5.6 KiB
Go

package models
import "time"
// CatalogueUpload is our own receipt for a spreadsheet sent to the ingest
// service — the record that the upload happened, who sent it, and for which
// shop.
//
// It exists because nothing else keeps one. The ingest service holds the drop,
// but on its terms and not ours:
//
// 1. **A drop nobody acts on is deleted after seven days**
// (`BATCH_RETENTION_DAYS`). If an admin never presses Start, the only proof
// the upload ever happened disappears — including from the sender.
// 2. **We cannot list our own drops.** `GET /api/uploads/catalog` is scoped to
// the credential that sent them, production has no API keys configured, and
// the one admin account is a superuser over their whole application. So the
// list is not available to us and should not be.
// 3. **The batch id is the credential.** `GET /api/uploads/catalog/{batch_id}`
// is anonymous by design — holding the id is the proof of having sent the
// drop. Which means whoever holds the id can read the result, and nobody
// else can. Losing the id loses the result permanently.
//
// So the id is the thing worth keeping, and this row is where we keep it. Every
// status field beside it is a CACHE of what the ingest service last told us,
// written by whichever browser was polling. It is never the authority — the
// service is — and it exists so a receipt reads sensibly before anyone re-polls
// it.
//
// The tenant and branch are ours alone. The ingest service writes the GLOBAL
// catalogue and has no concept of either, so "which shop was this for" is a
// question only this row can answer.
type CatalogueUpload struct {
Uploadid int `json:"uploadid" gorm:"primaryKey;autoIncrement;column:uploadid"`
// Who the sheet was for. The ingest service knows neither.
Tenantid int `json:"tenantid" gorm:"column:tenantid"`
Locationid int `json:"locationid" gorm:"column:locationid"`
// The category every row was filed under, chosen at upload time. Worth
// keeping: a product filed outside the category the app browses is
// invisible to shoppers, and this is the only record of what was chosen.
Categoryid int `json:"categoryid" gorm:"column:categoryid"`
// The drop id, and the only thing here that cannot be reconstructed.
Batchid string `json:"batchid" gorm:"column:batchid"`
// The run an admin released the drop into, once they have. Cached from
// `released_to` so a receipt can be followed without re-walking the drop.
Runid string `json:"runid" gorm:"column:runid"`
Filename string `json:"filename" gorm:"column:filename"`
// The label the ingest inbox shows their admin. Stored so we can tell,
// afterwards, what they were looking at when they approved it.
Sender string `json:"sender" gorm:"column:sender"`
Uploadedby int `json:"uploadedby" gorm:"column:uploadedby"`
Uploadedname string `json:"uploadedname" gorm:"column:uploadedname"`
// Rows we parsed in the browser, before sending. Independent of anything
// the service reports, so a drop that never runs still says how big it was.
Rowcount int `json:"rowcount" gorm:"column:rowcount"`
// The sheet itself, as the console parsed it: SKU, price and opening stock
// per row, as JSON.
//
// Stored because the ingest service cannot hold it and nothing else can.
// Their pipeline writes the GLOBAL catalogue, which every merchant shares
// and which therefore carries no price and no stock; both live only in the
// sheet. Shelving is the step that joins the two, and it used to be possible
// only in the browser tab that did the upload, because that tab was the only
// place the parsed rows existed.
//
// That failed in the ordinary case rather than a rare one. A drop waits for
// their admin to release it and the run then takes minutes, so by the time
// there is anything to shelve the tab is usually gone — and the products sit
// in the global catalogue, unpriced and unstocked, with no way left to
// finish. Keeping the rows here is what lets the Uploads page complete it
// days later.
Sheetrows string `json:"sheetrows" gorm:"column:sheetrows;type:jsonb"`
// ── Cached from the ingest service, by whoever last polled ──────────────
Laststatus string `json:"laststatus" gorm:"column:laststatus;default:pending"`
// Their counts, so a settled receipt reads correctly with no network call.
Inserted int `json:"inserted" gorm:"column:inserted"`
Backfilled int `json:"backfilled" gorm:"column:backfilled"`
Skipped int `json:"skipped" gorm:"column:skipped"`
Rejected int `json:"rejected" gorm:"column:rejected"`
// ── Ours: the half the ingest service cannot do ────────────────────────
//
// Their pipeline writes the global catalogue, which every merchant shares
// and which therefore holds no price and no stock. Shelving is what turns
// "the product exists" into "this shop can sell it", and it is a separate
// action that can be left undone — so it is recorded separately.
Shelvedcount int `json:"shelvedcount" gorm:"column:shelvedcount"`
Skippedcount int `json:"skippedcount" gorm:"column:skippedcount"`
Shelvedat *time.Time `json:"shelvedat" gorm:"column:shelvedat"`
Created time.Time `json:"created" gorm:"column:created;autoCreateTime"`
Updated time.Time `json:"updated" gorm:"column:updated;autoUpdateTime"`
// Joined for display, never stored. A receipt outlives the page that made
// it, so it has to be able to name its own shop.
Tenantname string `json:"tenantname" gorm:"->;column:tenantname"`
Locationname string `json:"locationname" gorm:"->;column:locationname"`
}
func (CatalogueUpload) TableName() string {
return "catalogueuploads"
}