package models import "time" // Schema for the customer app (doormile_cx) surface. // // The one structural idea here is that a customer books a PICKUP, not a // shipment: one booking fans out to 1..N destinations, and each destination // becomes its own consignment with its own tracking number when the miler // completes the pickup. A single-destination booking is the same shape, not a // special case — pickupbookings keeps its flat delivery* columns mirrored from // destination 0 so the miler app, the hub console and the routing code keep // reading the row they already read. // ServiceableState is a state the customer may choose a destination in. The // picker hides any state with no available district, but the row stays so ops // can open one without an app release. type ServiceableState struct { Statecode string `json:"statecode" gorm:"primaryKey;column:statecode;size:8"` Statename string `json:"statename" gorm:"column:statename;not null"` // Transittag is display copy shown under the state name ("Ultra-fast // transit", "Opening soon"). Capped at 22 characters by the design. Transittag string `json:"transittag" gorm:"column:transittag;size:22"` Displayorder int `json:"displayorder" gorm:"column:displayorder;default:0"` Status string `json:"status" gorm:"column:status;default:Active"` // Active, InActive Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"` } func (ServiceableState) TableName() string { return "serviceablestates" } // ServiceableDistrict is one district inside a state. Unavailable districts are // returned too: the picker filters them out but the app shows their names in a // quiet "coming soon" line, so dropping them would lose real copy. type ServiceableDistrict struct { Districtcode string `json:"districtcode" gorm:"primaryKey;column:districtcode;size:16"` Statecode string `json:"statecode" gorm:"column:statecode;size:8;index;not null"` Districtname string `json:"districtname" gorm:"column:districtname;not null"` Available bool `json:"available" gorm:"column:available;default:false"` // Note says why a district is not available — "Opening soon", "Paused this // week". Only meaningful when Available is false. Note string `json:"note" gorm:"column:note;size:60"` // Hubid is the base that serves this district. Nullable: a district can be // announced before its hub exists. Hubid *int `json:"hubid" gorm:"column:hubid"` // Promise is the delivery promise shown on the destination card, // "Next-day delivery" / "2-day delivery". Promise string `json:"promise" gorm:"column:promise;size:40"` // Centre coordinates price and route a destination before any street // address exists — only state and district are required at booking time, so // this is frequently the only geography a destination has. Centrelatitude float64 `json:"centrelatitude" gorm:"column:centrelatitude"` Centrelongitude float64 `json:"centrelongitude" gorm:"column:centrelongitude"` Pincodeprefix string `json:"pincodeprefix" gorm:"column:pincodeprefix;size:6"` Displayorder int `json:"displayorder" gorm:"column:displayorder;default:0"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"` } func (ServiceableDistrict) TableName() string { return "serviceabledistricts" } // PickupSlotTemplate is a recurring daily pickup window. The customer-facing // slot id is minted per date from the template code, so a slot always resolves // back to a real preferredpickupfrom/to on the booking — a slot ops cannot // staff is worse than no slot. type PickupSlotTemplate struct { Slottemplateid int `json:"slottemplateid" gorm:"primaryKey;column:slottemplateid"` // Code is the stable part of the slot id: "t1" renders as // "slot_20260905_t1". Code string `json:"code" gorm:"column:code;size:16;not null"` Starthour int `json:"starthour" gorm:"column:starthour;not null"` Startminute int `json:"startminute" gorm:"column:startminute;default:0"` Endhour int `json:"endhour" gorm:"column:endhour;not null"` Endminute int `json:"endminute" gorm:"column:endminute;default:0"` // Capacity is how many pickups this window absorbs in one zone. Capacity int `json:"capacity" gorm:"column:capacity;default:20"` // Applocationid scopes a template to one city. Null means every city. Applocationid *int `json:"applocationid" gorm:"column:applocationid;index"` // Tag is the single promotional label the design allows on at most one slot // ("Fastest pickup"). Caption is the softer line under it. Tag string `json:"tag" gorm:"column:tag;size:40"` Caption string `json:"caption" gorm:"column:caption;size:60"` Displayorder int `json:"displayorder" gorm:"column:displayorder;default:0"` Status string `json:"status" gorm:"column:status;default:Active"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"` } func (PickupSlotTemplate) TableName() string { return "pickupslottemplates" } // CustomerBookingLimit caps a pickup. Kept in the database rather than in the // app so ops can vary it by city or tier without an app release. A row with a // null applocationid is the global default. type CustomerBookingLimit struct { Limitid int `json:"limitid" gorm:"primaryKey;column:limitid"` Applocationid *int `json:"applocationid" gorm:"column:applocationid;index"` Maxpackages int `json:"maxpackages" gorm:"column:maxpackages;default:20"` Maxdestinations int `json:"maxdestinations" gorm:"column:maxdestinations;default:5"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"` } func (CustomerBookingLimit) TableName() string { return "customerbookinglimits" } // BookingDestination is one place inside a pickup booking, and the row that // becomes a consignment at pickup completion. This is the table that makes // "one visit, N orders" expressible: before it a booking carried exactly one // delivery address in its own columns and there was nowhere to put the second. type BookingDestination struct { Bookingdestinationid int `json:"bookingdestinationid" gorm:"primaryKey;column:bookingdestinationid"` Bookingid int `json:"bookingid" gorm:"column:bookingid;index;not null"` // Seq is the 0-based position within the booking and is the {index} in // PATCH /customer/bookings/{ref}/destinations/{index}. Stable for the life // of the booking — destinations are never reordered, because the customer // addresses them by position. Seq int `json:"seq" gorm:"column:seq;not null"` // Only state and district are required at booking time. Names are stored // alongside the codes rather than joined at read time: the client renders // "Chennai, Tamil Nadu" straight from the booking and never looks a code // up, and a district renamed later must not silently rewrite an address the // customer already agreed to. Statecode string `json:"statecode" gorm:"column:statecode;size:8;not null"` Statename string `json:"statename" gorm:"column:statename;not null"` Districtcode string `json:"districtcode" gorm:"column:districtcode;size:16;not null"` Districtname string `json:"districtname" gorm:"column:districtname;not null"` Packagecount int `json:"packagecount" gorm:"column:packagecount;default:1"` // Everything below is optional at booking time and may be completed by the // customer afterwards or by the miler at the door. Empty means "not added // yet", which the UI states explicitly rather than hiding. Street string `json:"street" gorm:"column:street"` Building string `json:"building" gorm:"column:building"` Landmark string `json:"landmark" gorm:"column:landmark"` Recipientname string `json:"recipientname" gorm:"column:recipientname"` Recipientphone string `json:"recipientphone" gorm:"column:recipientphone"` Instructions string `json:"instructions" gorm:"column:instructions"` Pinlatitude *float64 `json:"pinlatitude" gorm:"column:pinlatitude"` Pinlongitude *float64 `json:"pinlongitude" gorm:"column:pinlongitude"` Pincode string `json:"pincode" gorm:"column:pincode;size:10"` // Codamount is money the miler collects at this door on the customer's // behalf. Doormile is the carrier, never the seller — this is the // customer's own collection, and it is per destination because it is // collected per delivery. Codamount float64 `json:"codamount" gorm:"column:codamount;default:0"` // Set at pickup completion, when this destination becomes an order. Consignmentid *int `json:"consignmentid" gorm:"column:consignmentid;index"` Trackingno string `json:"trackingno" gorm:"column:trackingno;size:32;index"` // Stage is the per-order stage from order_created onward. Empty until the // order exists; the booking's own stage governs everything before that. Stage string `json:"stage" gorm:"column:stage;size:24"` Expecteddeliveryat *time.Time `json:"expecteddeliveryat" gorm:"column:expecteddeliveryat"` Deliveredat *time.Time `json:"deliveredat" gorm:"column:deliveredat"` // Verification is what the miler recorded at the door: the weight the price // actually settled on, and who recorded it. Photos live in // bookingparcelphotos, one row per package. Verifiedweightkg *float64 `json:"verifiedweightkg" gorm:"column:verifiedweightkg"` Verifiedat *time.Time `json:"verifiedat" gorm:"column:verifiedat"` Verifiedbyuserid *int `json:"verifiedbyuserid" gorm:"column:verifiedbyuserid"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"` } func (BookingDestination) TableName() string { return "bookingdestinations" } // BookingParcelPhoto is a photograph the miler took of a package at the door. // It is the evidence half of the receipt — a settled weight without the photo // is a number the customer has no way to check. type BookingParcelPhoto struct { Photoid int `json:"photoid" gorm:"primaryKey;column:photoid"` Bookingid int `json:"bookingid" gorm:"column:bookingid;index;not null"` Bookingdestinationid *int `json:"bookingdestinationid" gorm:"column:bookingdestinationid;index"` // Objectkey is the storage key. The customer is served a short-lived signed // URL derived from it, never a permanent public link — a parcel photo can // show the inside of someone's doorway. Objectkey string `json:"objectkey" gorm:"column:objectkey;not null"` Capturedbyuserid *int `json:"capturedbyuserid" gorm:"column:capturedbyuserid"` Capturedat time.Time `json:"capturedat" gorm:"column:capturedat;default:CURRENT_TIMESTAMP"` } func (BookingParcelPhoto) TableName() string { return "bookingparcelphotos" } // BookingStageEvent is the append-only audit log every customer timeline is // built from: one row per stage actually reached, with the real time it was // reached and who caused it. Nothing here is synthesised or backfilled — a // timeline with invented timestamps is worse than a short one, because the // customer cannot tell which entries are real. type BookingStageEvent struct { Stageeventid int `json:"stageeventid" gorm:"primaryKey;column:stageeventid"` Bookingid int `json:"bookingid" gorm:"column:bookingid;index;not null"` // Bookingdestinationid is null for the booking-level stages (booked through // order_created) and set for the per-order ones (in_transit onward), which // may differ between destinations of the same booking. Bookingdestinationid *int `json:"bookingdestinationid" gorm:"column:bookingdestinationid;index"` Stage string `json:"stage" gorm:"column:stage;size:24;not null"` // Actortype/Actorid answer "who did this" — miler, ops user, the customer, // or the system. Required by the audit rule, and the only way to explain a // cancellation to a customer who did not make it. Actortype string `json:"actortype" gorm:"column:actortype;size:16;not null"` Actorid *int `json:"actorid" gorm:"column:actorid"` Source string `json:"source" gorm:"column:source;size:64"` Remarks string `json:"remarks" gorm:"column:remarks"` Occurredat time.Time `json:"occurredat" gorm:"column:occurredat;index;not null"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` } func (BookingStageEvent) TableName() string { return "bookingstageevents" } // CustomerRefreshToken backs the 60-day session. Stored hashed: a leaked // database row must not itself be a usable credential. Rotation is recorded via // Replacedbyid so a replayed old token identifies the chain it came from. type CustomerRefreshToken struct { Refreshtokenid int `json:"refreshtokenid" gorm:"primaryKey;column:refreshtokenid"` Appcustomerid int `json:"appcustomerid" gorm:"column:appcustomerid;index;not null"` Tokenhash string `json:"-" gorm:"column:tokenhash;size:64;uniqueIndex;not null"` Expiresat time.Time `json:"expiresat" gorm:"column:expiresat;not null"` Revokedat *time.Time `json:"revokedat" gorm:"column:revokedat"` Replacedbyid *int `json:"replacedbyid" gorm:"column:replacedbyid"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` } func (CustomerRefreshToken) TableName() string { return "customerrefreshtokens" } // CustomerDevice is a push registration. One row per device token, not per // customer: a customer with a phone and a tablet must get the notification on // both, and appcustomers.device_token could only ever hold the last one. type CustomerDevice struct { Customerdeviceid int `json:"customerdeviceid" gorm:"primaryKey;column:customerdeviceid"` Appcustomerid int `json:"appcustomerid" gorm:"column:appcustomerid;index;not null"` Token string `json:"token" gorm:"column:token;uniqueIndex;not null"` Platform string `json:"platform" gorm:"column:platform;size:16"` Appversion string `json:"appversion" gorm:"column:appversion;size:32"` Lastseenat time.Time `json:"lastseenat" gorm:"column:lastseenat;default:CURRENT_TIMESTAMP"` Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"` } func (CustomerDevice) TableName() string { return "customerdevices" }