fix: close price-tamper, premature rider-free, and IST/txn gaps in merged cx/handover work

Reviewed the 10 merged customer-app/base-handover commits and fixed the
defects found:

- HIGH (money): CreateCxBooking let the request body's `estimate` set the
  billed price with no server-side check; it flows into Estimatedprice →
  ridercharges (miler pay + tenant bill) with no weight re-price, so
  {min:1,max:1} settled a delivery at ₹1. Now the client estimate is honoured
  only when it matches the server quote within 15%, else the server quote
  stands.
- MED: base handover freed the rider and closed the booking-level assignment
  after the FIRST parcel of a multi-destination pickup, dropping the remaining
  stops and crediting one leg. Now finalized only when no consignment of the
  booking is still in the rider's hands.
- MED: inwardedat/completedat were written with time.Now() (UTC) instead of
  DBNow() (IST), skewing them ~5h30 vs createdat and the earnings/reconcile
  windows. Fixed in the handover, inbound-scan, reconcile and pickup-complete
  paths.
- MED: B2C customers got two "miler assigned" pushes on auto-assign (two token
  stores) and none on manual assign. Reconciled to one cxstage.Notify on both
  paths.
- LOW: ReconcileHubInbound now runs in a transaction and checks its audit
  inserts (was returning 200 with a silently-missing history row); CxLogout no
  longer reports signedOut when the token revoke fails; a rider-named handover
  base far from their reported position is rejected instead of silently
  rerouting the parcel to another city.

go build, go vet and go test ./... all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRaFH5hMRqmUQvVPQsyjZD
This commit is contained in:
2026-09-16 12:16:50 +05:30
parent 049bb85359
commit ba2cd2299c
7 changed files with 185 additions and 58 deletions

View File

@@ -504,9 +504,14 @@ func CxLogout(c *fiber.Ctx) error {
now := time.Now()
if strings.TrimSpace(req.RefreshToken) != "" {
db.DB.Model(&models.CustomerRefreshToken{}).
// A failed revoke must not answer signedOut — the 60-day refresh token
// would stay valid while the customer believes they logged out.
if err := db.DB.Model(&models.CustomerRefreshToken{}).
Where("tokenhash = ? AND appcustomerid = ?", hashToken(req.RefreshToken), customerID).
Update("revokedat", now)
Update("revokedat", now).Error; err != nil {
utils.Error("CxLogout: could not revoke the presented token", "customer_id", customerID, "error", err)
return utils.CxInternal(c)
}
} else {
// No token supplied — sign out everywhere rather than leave a session
// the customer believes they ended.
@@ -514,8 +519,12 @@ func CxLogout(c *fiber.Ctx) error {
}
if strings.TrimSpace(req.DeviceToken) != "" {
db.DB.Where("appcustomerid = ? AND token = ?", customerID, req.DeviceToken).
Delete(&models.CustomerDevice{})
if err := db.DB.Where("appcustomerid = ? AND token = ?", customerID, req.DeviceToken).
Delete(&models.CustomerDevice{}).Error; err != nil {
// The session is already revoked above; a stuck device row only means a
// stray push, so log and still report signed out rather than fail.
utils.Warn("CxLogout: could not remove device token", "customer_id", customerID, "error", err)
}
}
return utils.CxOK(c, fiber.Map{"signedOut": true})