feat: Implement Phase 8 Delivery and Rider Management
CI / test (push) Successful in 10m26s

- Introduced new Delivery and Rider API documentation.
- Added new models for RiderProfile, Shipment, ShipmentItem, ShipmentAssignment, ShipmentEvent, and ShipmentProof.
- Developed controllers for admin and rider logistics, including shipment management and rider actions.
- Created services for handling shipment creation, assignment, and state transitions.
- Implemented validation schemas for shipment and rider operations.
- Added new routes for admin and rider logistics, including tracking endpoints.
- Established a state machine for shipment status transitions.
- Created migration scripts for new database tables and relationships.
- Added unit tests for shipment state transitions and validation security.
This commit is contained in:
Sathira Sri Sathara
2026-09-09 12:55:32 +05:30
parent cd2c1c6d08
commit 222483d194
24 changed files with 168 additions and 0 deletions
+1
View File
@@ -96,6 +96,7 @@ db.Promotion=require("./merchandising/promotion.model")(sequelize,DataTypes);db.
db.Cart=require("./shopping/cart.model")(sequelize,DataTypes);db.CartItem=require("./shopping/cartItem.model")(sequelize,DataTypes);db.WishlistItem=require("./shopping/wishlistItem.model")(sequelize,DataTypes);db.CheckoutSession=require("./shopping/checkoutSession.model")(sequelize,DataTypes);db.CheckoutItem=require("./shopping/checkoutItem.model")(sequelize,DataTypes);
db.ShippingZone=require("./shipping/shippingZone.model")(sequelize,DataTypes);db.ShippingZoneRegion=require("./shipping/shippingZoneRegion.model")(sequelize,DataTypes);db.ShippingMethod=require("./shipping/shippingMethod.model")(sequelize,DataTypes);db.ShippingRate=require("./shipping/shippingRate.model")(sequelize,DataTypes);
db.Order=require("./commerce/order.model")(sequelize,DataTypes);db.OrderItem=require("./commerce/orderItem.model")(sequelize,DataTypes);db.Payment=require("./commerce/payment.model")(sequelize,DataTypes);db.PaymentAttempt=require("./commerce/paymentAttempt.model")(sequelize,DataTypes);db.PaymentWebhookEvent=require("./commerce/paymentWebhookEvent.model")(sequelize,DataTypes);db.Invoice=require("./commerce/invoice.model")(sequelize,DataTypes);db.Refund=require("./commerce/refund.model")(sequelize,DataTypes);db.RefundItem=require("./commerce/refundItem.model")(sequelize,DataTypes);db.ReturnRequest=require("./commerce/returnRequest.model")(sequelize,DataTypes);db.ReturnItem=require("./commerce/returnItem.model")(sequelize,DataTypes);db.CouponRedemption=require("./commerce/couponRedemption.model")(sequelize,DataTypes);
db.RiderProfile=require("./logistics/riderProfile.model")(sequelize,DataTypes);db.Shipment=require("./logistics/shipment.model")(sequelize,DataTypes);db.ShipmentItem=require("./logistics/shipmentItem.model")(sequelize,DataTypes);db.ShipmentAssignment=require("./logistics/shipmentAssignment.model")(sequelize,DataTypes);db.ShipmentEvent=require("./logistics/shipmentEvent.model")(sequelize,DataTypes);db.ShipmentProof=require("./logistics/shipmentProof.model")(sequelize,DataTypes);
/* Associations */
Object.keys(db).forEach(model => {
@@ -0,0 +1 @@
module.exports=(s,D)=>{const M=s.define("RiderProfile",{id:{type:D.STRING,primaryKey:true},user_id:{type:D.STRING,allowNull:false,unique:true},employee_code:{type:D.STRING(60),allowNull:false,unique:true},phone_override:D.STRING(30),vehicle_type:D.STRING(60),vehicle_registration:D.STRING(60),status:{type:D.ENUM("ACTIVE","INACTIVE","SUSPENDED"),allowNull:false,defaultValue:"ACTIVE"},availability_status:{type:D.ENUM("AVAILABLE","BUSY","OFFLINE"),allowNull:false,defaultValue:"OFFLINE"},current_latitude:D.DECIMAL(10,7),current_longitude:D.DECIMAL(10,7),last_location_at:D.DATE,max_active_assignments:{type:D.INTEGER,allowNull:false,defaultValue:1}},{tableName:"rider_profiles",timestamps:true});M.associate=db=>M.belongsTo(db.User,{foreignKey:"user_id",as:"user"});return M;};
+1
View File
@@ -0,0 +1 @@
module.exports=(s,D)=>{const M=s.define("Shipment",{id:{type:D.STRING,primaryKey:true},shipment_number:{type:D.STRING(80),allowNull:false,unique:true},order_id:D.STRING,return_request_id:D.STRING,type:{type:D.ENUM("CUSTOMER_DELIVERY","RETURN_PICKUP"),allowNull:false},status:{type:D.ENUM("PENDING","READY_FOR_ASSIGNMENT","ASSIGNED","PICKUP_PENDING","PICKED_UP","IN_TRANSIT","OUT_FOR_DELIVERY","DELIVERED","DELIVERY_FAILED","RESCHEDULED","CANCELLED"),allowNull:false,defaultValue:"PENDING"},shipping_method_code:D.STRING(60),shipping_method_name:D.STRING(150),recipient_name:{type:D.STRING(150),allowNull:false},recipient_phone:{type:D.STRING(30),allowNull:false},address_snapshot:{type:D.JSON,allowNull:false},assigned_rider_id:D.STRING,assigned_at:D.DATE,picked_up_at:D.DATE,out_for_delivery_at:D.DATE,delivered_at:D.DATE,failed_at:D.DATE,cancelled_at:D.DATE,scheduled_date:D.DATE,time_window_start:D.STRING(20),time_window_end:D.STRING(20),delivery_attempt_count:{type:D.INTEGER,allowNull:false,defaultValue:0},notes:D.STRING(500)},{tableName:"shipments",timestamps:true});M.associate=db=>{M.hasMany(db.ShipmentItem,{foreignKey:"shipment_id",as:"items"});M.hasMany(db.ShipmentAssignment,{foreignKey:"shipment_id",as:"assignments"});M.hasMany(db.ShipmentEvent,{foreignKey:"shipment_id",as:"events"});M.hasMany(db.ShipmentProof,{foreignKey:"shipment_id",as:"proofs"});M.belongsTo(db.Order,{foreignKey:"order_id",as:"order"});};return M;};
@@ -0,0 +1 @@
module.exports=(s,D)=>s.define("ShipmentAssignment",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},rider_id:{type:D.STRING,allowNull:false},status:{type:D.ENUM("ASSIGNED","ACCEPTED","REJECTED","UNASSIGNED","COMPLETED"),allowNull:false},assigned_by:{type:D.STRING,allowNull:false},assigned_at:{type:D.DATE,allowNull:false},accepted_at:D.DATE,rejected_at:D.DATE,unassigned_at:D.DATE,completed_at:D.DATE,reason:D.STRING(500)},{tableName:"shipment_assignments",timestamps:true});
@@ -0,0 +1 @@
module.exports=(s,D)=>s.define("ShipmentEvent",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},event_id:{type:D.STRING(160),allowNull:false,unique:true},type:{type:D.STRING(80),allowNull:false},status:{type:D.STRING(40),allowNull:false},actor_user_id:D.STRING,latitude:D.DECIMAL(10,7),longitude:D.DECIMAL(10,7),note:D.STRING(500),occurred_at:{type:D.DATE,allowNull:false}},{tableName:"shipment_events",timestamps:true,updatedAt:false});
@@ -0,0 +1 @@
module.exports=(s,D)=>{const M=s.define("ShipmentItem",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},order_item_id:{type:D.STRING,allowNull:false},quantity:{type:D.INTEGER,allowNull:false}},{tableName:"shipment_items",timestamps:true,indexes:[{unique:true,fields:["shipment_id","order_item_id"]}]});M.associate=db=>M.belongsTo(db.Shipment,{foreignKey:"shipment_id",as:"shipment"});return M;};
@@ -0,0 +1 @@
module.exports=(s,D)=>s.define("ShipmentProof",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},event_id:{type:D.STRING(160),allowNull:false,unique:true},type:{type:D.ENUM("SIGNATURE","PHOTO","RECIPIENT_CONFIRMATION","RETURN_PICKUP_PHOTO"),allowNull:false},recipient_name:D.STRING(150),signature_upload_id:D.INTEGER,photo_upload_id:D.INTEGER,notes:D.STRING(500),captured_at:{type:D.DATE,allowNull:false},captured_by:{type:D.STRING,allowNull:false}},{tableName:"shipment_proofs",timestamps:true});