backend requirements onthe xustomer app
This commit is contained in:
251
controllers/cxPickupFanout.go
Normal file
251
controllers/cxPickupFanout.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Pickup fan-out: one visit becomes N orders.
|
||||
//
|
||||
// This is the change §1 of the customer contract turns on. A pickup booking
|
||||
// used to convert into exactly one consignment, because a booking carried
|
||||
// exactly one delivery address in its own columns. A customer-app booking
|
||||
// carries 1..N destinations, and each of those has to become its own
|
||||
// consignment with its own tracking number and its own journey — three parcels
|
||||
// collected in one visit for Chennai, Kochi and Bengaluru travel three separate
|
||||
// routes the moment they leave the door.
|
||||
//
|
||||
// A booking with no destination rows — every console-created express booking,
|
||||
// and every row written before this table existed — produces exactly one leg
|
||||
// built from the flat delivery columns, which is byte-for-byte the behaviour
|
||||
// that was there before. Single-destination is not a special case in either
|
||||
// direction: it is one leg, through the same code.
|
||||
|
||||
// cxPickupLeg is one journey to create at pickup completion.
|
||||
type cxPickupLeg struct {
|
||||
// Destination is nil for a booking that has no destination rows.
|
||||
Destination *models.BookingDestination
|
||||
|
||||
DeliveryLatitude float64
|
||||
DeliveryLongitude float64
|
||||
DeliveryPincode string
|
||||
CodAmount float64
|
||||
|
||||
// Parcels are the packages going to this destination, already weighed by
|
||||
// the miler.
|
||||
Parcels []models.BookingParcel
|
||||
}
|
||||
|
||||
// cxPickupLegs splits a booking into the journeys its parcels are about to
|
||||
// take.
|
||||
func cxPickupLegs(tx *gorm.DB, booking *models.PickupBooking) ([]cxPickupLeg, error) {
|
||||
var destinations []models.BookingDestination
|
||||
if err := tx.Where("bookingid = ?", booking.Bookingid).
|
||||
Order("seq ASC").Find(&destinations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parcels []models.BookingParcel
|
||||
if err := tx.Where("bookingid = ?", booking.Bookingid).Find(&parcels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(destinations) == 0 {
|
||||
// The pre-existing shape: one consignment from the booking's own
|
||||
// delivery columns, carrying every parcel on the booking.
|
||||
return []cxPickupLeg{{
|
||||
DeliveryLatitude: booking.Deliverylatitude,
|
||||
DeliveryLongitude: booking.Deliverylongitude,
|
||||
DeliveryPincode: booking.Deliverypincode,
|
||||
Parcels: parcels,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
byDestination := map[int][]models.BookingParcel{}
|
||||
unassigned := []models.BookingParcel{}
|
||||
for _, p := range parcels {
|
||||
if p.Bookingdestinationid == nil {
|
||||
unassigned = append(unassigned, p)
|
||||
continue
|
||||
}
|
||||
byDestination[*p.Bookingdestinationid] = append(byDestination[*p.Bookingdestinationid], p)
|
||||
}
|
||||
|
||||
legs := make([]cxPickupLeg, 0, len(destinations))
|
||||
for i := range destinations {
|
||||
d := destinations[i]
|
||||
|
||||
lat, lng := cxDestinationPoint(&d)
|
||||
leg := cxPickupLeg{
|
||||
Destination: &destinations[i],
|
||||
DeliveryLatitude: lat,
|
||||
DeliveryLongitude: lng,
|
||||
DeliveryPincode: d.Pincode,
|
||||
CodAmount: d.Codamount,
|
||||
Parcels: byDestination[d.Bookingdestinationid],
|
||||
}
|
||||
// Parcels the miler added at the door without naming a destination go
|
||||
// with the first one. Attributing them to nothing would drop them out
|
||||
// of every weight total, and the first destination is the one the rider
|
||||
// was shown.
|
||||
if i == 0 {
|
||||
leg.Parcels = append(leg.Parcels, unassigned...)
|
||||
}
|
||||
legs = append(legs, leg)
|
||||
}
|
||||
return legs, nil
|
||||
}
|
||||
|
||||
// cxDestinationPoint gives a destination the best coordinates it has: the
|
||||
// customer's own map pin if they dropped one, otherwise the district centre.
|
||||
// Only state and district are required at booking time, so the centre is
|
||||
// frequently all there is — and routing skips stops sitting at 0,0.
|
||||
func cxDestinationPoint(d *models.BookingDestination) (lat, lng float64) {
|
||||
if d.Pinlatitude != nil && d.Pinlongitude != nil &&
|
||||
(*d.Pinlatitude != 0 || *d.Pinlongitude != 0) {
|
||||
return *d.Pinlatitude, *d.Pinlongitude
|
||||
}
|
||||
var district models.ServiceableDistrict
|
||||
if err := db.DB.Select("centrelatitude, centrelongitude").
|
||||
Where("districtcode = ?", d.Districtcode).First(&district).Error; err == nil {
|
||||
return district.Centrelatitude, district.Centrelongitude
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// cxLegWeights totals a leg's packages the way the consignment records them.
|
||||
// A leg whose packages were never weighed falls back to the same 0.5 kg
|
||||
// placeholder the single-consignment path already used, so an unweighed pickup
|
||||
// still produces a chargeable consignment rather than a zero-weight one.
|
||||
func cxLegWeights(leg cxPickupLeg) (deadWeight, chargeable, maxL, maxW, maxH float64) {
|
||||
for _, p := range leg.Parcels {
|
||||
vol := calculateVolumetricWeight(p.Length, p.Width, p.Height)
|
||||
deadWeight += p.Weight
|
||||
chargeable += math.Max(p.Weight, vol)
|
||||
if p.Length > maxL {
|
||||
maxL = p.Length
|
||||
}
|
||||
if p.Width > maxW {
|
||||
maxW = p.Width
|
||||
}
|
||||
if p.Height > maxH {
|
||||
maxH = p.Height
|
||||
}
|
||||
}
|
||||
if len(leg.Parcels) == 0 || chargeable == 0 {
|
||||
deadWeight, chargeable = 0.5, 0.5
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// cxLinkLegToOrder records that a destination has become an order: its tracking
|
||||
// number, its consignment, and the delivery date the customer is promised.
|
||||
func cxLinkLegToOrder(tx *gorm.DB, leg cxPickupLeg, consignmentID int, trackingNo string, expected *time.Time, at time.Time) error {
|
||||
if leg.Destination == nil {
|
||||
return nil
|
||||
}
|
||||
updates := map[string]interface{}{
|
||||
"consignmentid": consignmentID,
|
||||
"trackingno": trackingNo,
|
||||
"stage": constants.CxStageOrderCreated,
|
||||
"updatedat": at,
|
||||
}
|
||||
if expected != nil {
|
||||
updates["expecteddeliveryat"] = *expected
|
||||
}
|
||||
return tx.Model(&models.BookingDestination{}).
|
||||
Where("bookingdestinationid = ?", leg.Destination.Bookingdestinationid).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
// cxRecordVerification stores what the miler measured at the door: the weight
|
||||
// the price settles on, when, and who took the reading. It is the evidence half
|
||||
// of the receipt, and it is per destination because each order is weighed
|
||||
// separately.
|
||||
func cxRecordVerification(tx *gorm.DB, destinationID int, weightKg float64, byUserID int, at time.Time) error {
|
||||
return tx.Model(&models.BookingDestination{}).
|
||||
Where("bookingdestinationid = ?", destinationID).
|
||||
Updates(map[string]interface{}{
|
||||
"verifiedweightkg": weightKg,
|
||||
"verifiedat": at,
|
||||
"verifiedbyuserid": byUserID,
|
||||
"updatedat": at,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// cxDestinationForConsignment finds which order a consignment belongs to.
|
||||
//
|
||||
// The delivery handlers used to reach the booking with
|
||||
// `WHERE consignmentid = ?` on pickupbookings, which held exactly one
|
||||
// consignment id. With a fan-out that column only names the FIRST order, so
|
||||
// that lookup silently found nothing for destinations 2..N — no customer
|
||||
// notification, no stage advance, on every multi-destination booking. The join
|
||||
// goes through bookingdestinations now, which is the table that actually knows.
|
||||
func cxDestinationForConsignment(consignmentID int) (*models.BookingDestination, *models.PickupBooking, bool) {
|
||||
var dest models.BookingDestination
|
||||
if err := db.DB.Where("consignmentid = ?", consignmentID).First(&dest).Error; err == nil {
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, dest.Bookingid).Error; err == nil {
|
||||
return &dest, &booking, true
|
||||
}
|
||||
return &dest, nil, false
|
||||
}
|
||||
|
||||
// No destination row: a console-created booking, or one written before the
|
||||
// fan-out existed. The legacy link still answers for those.
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Where("consignmentid = ?", consignmentID).First(&booking).Error; err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
return nil, &booking, true
|
||||
}
|
||||
|
||||
// cxDestinationIDFor returns the destination id to attribute a per-order stage
|
||||
// to, or nil when the booking has no destination rows.
|
||||
func cxDestinationIDFor(dest *models.BookingDestination) *int {
|
||||
if dest == nil {
|
||||
return nil
|
||||
}
|
||||
id := dest.Bookingdestinationid
|
||||
return &id
|
||||
}
|
||||
|
||||
// cxEstimatedDelivery is the promise shown to the customer, derived from the
|
||||
// serving district's own promise text rather than a single platform-wide SLA —
|
||||
// "Next-day delivery" into Chennai and "2-day delivery" into a district two
|
||||
// states away are different promises and must not resolve to the same date.
|
||||
func cxEstimatedDelivery(leg cxPickupLeg, from time.Time) *time.Time {
|
||||
days := 2
|
||||
if leg.Destination != nil {
|
||||
var district models.ServiceableDistrict
|
||||
if err := db.DB.Select("promise").
|
||||
Where("districtcode = ?", leg.Destination.Districtcode).
|
||||
First(&district).Error; err == nil {
|
||||
switch district.Promise {
|
||||
case "Same-day delivery":
|
||||
days = 0
|
||||
case "Next-day delivery":
|
||||
days = 1
|
||||
case "2-day delivery":
|
||||
days = 2
|
||||
case "3-day delivery":
|
||||
days = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delivered by end of the promised day, expressed in the wall clock this
|
||||
// database stores.
|
||||
target := from.AddDate(0, 0, days)
|
||||
expected := time.Date(target.Year(), target.Month(), target.Day(), 20, 0, 0, 0, time.UTC)
|
||||
if utils.EpochMillis(expected) < utils.EpochMillis(from) {
|
||||
expected = from
|
||||
}
|
||||
return &expected
|
||||
}
|
||||
Reference in New Issue
Block a user