backend requirements onthe xustomer app
This commit is contained in:
241
models/customer_app.go
Normal file
241
models/customer_app.go
Normal file
@@ -0,0 +1,241 @@
|
||||
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" }
|
||||
Reference in New Issue
Block a user