feat: Implement Phase 6 Shopping and Checkout
- Introduced authenticated shopping state and atomic checkout process without creating orders or payments. - Reused existing components from previous phases including identities, addresses, catalogues, pricing, and inventory. - Developed cart architecture to support one active cart per user with unique items per variant. - Implemented dynamic cart pricing with various precedence rules for promotions and coupons. - Created a wishlist feature that exposes only visible active products without reserving inventory. - Integrated inventory validation to ensure availability of items before adding to cart. - Developed a checkout architecture that locks the cart, revalidates pricing and shipping, and reserves inventory. - Added shipping zones, methods, and rates with configurable options for international shipping and duty/tax boundaries. - Implemented checkout snapshots to capture immutable checkout details. - Introduced idempotency for checkout requests to prevent duplicate processing. - Added functionality for coupon integration and business checkout validation. - Established permissions for managing shipping zones, methods, and rates. - Created comprehensive unit tests covering various aspects of the shopping and checkout processes. - Added cron job for handling checkout expiry reconciliation. - Created migration script to set up new database tables and constraints for carts, checkout sessions, and shipping.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
const db=require("../../models"),service=require("../../services/shopping/cart.service"),{logActivity}=require("../../services/activity.service");const audit=(req,type,id)=>logActivity({user:req.user,type,module:"Shopping",description:type,targetType:"CART",targetId:id,requestId:req.id});exports.get=async(req,res,next)=>{try{res.json({success:true,data:await service.project(req.user.id)});}catch(e){next(e);}};exports.add=async(req,res,next)=>{try{const x=await service.add(req.user.id,req.body.variantId,req.body.quantity);await audit(req,"CART_ITEM_ADDED",x.id);res.status(201).json({success:true,data:await service.project(req.user.id)});}catch(e){next(e);}};exports.update=async(req,res,next)=>{try{const x=await service.mutate(req.user.id,req.params.itemId,req.body.quantity);await audit(req,"CART_ITEM_UPDATED",x.id);res.json({success:true,data:await service.project(req.user.id)});}catch(e){next(e);}};exports.remove=async(req,res,next)=>{try{await service.mutate(req.user.id,req.params.itemId,0);await audit(req,"CART_ITEM_REMOVED",req.params.itemId);res.status(204).end();}catch(e){next(e);}};exports.clear=async(req,res,next)=>{try{const cart=await service.activeCart(req.user.id);await db.CartItem.destroy({where:{cart_id:cart.id}});await cart.increment("version");await audit(req,"CART_CLEARED",cart.id);res.status(204).end();}catch(e){next(e);}};exports.coupon=async(req,res,next)=>{try{const cart=await service.activeCart(req.user.id);await cart.update({coupon_code:req.body.code});res.json({success:true,data:await service.project(req.user.id)});}catch(e){next(e);}};exports.removeCoupon=async(req,res,next)=>{try{const cart=await service.activeCart(req.user.id);await cart.update({coupon_code:null});res.status(204).end();}catch(e){next(e);}};
|
||||
@@ -0,0 +1 @@
|
||||
const service=require("../../services/shopping/checkout.service"),{logActivity}=require("../../services/activity.service");exports.create=async(req,res,next)=>{try{const result=await service.create({userId:req.user.id,...req.body,idempotencyKey:req.get("Idempotency-Key"),requestId:req.id});if(!result.idempotent)await logActivity({user:req.user,type:"CHECKOUT_CREATED",module:"Checkout",targetId:result.checkout.id,requestId:req.id});res.status(result.idempotent?200:201).json({success:true,data:result.checkout});}catch(e){next(e);}};exports.get=async(req,res,next)=>{try{res.json({success:true,data:await service.getOwned(req.params.id,req.user.id)});}catch(e){next(e);}};exports.active=async(req,res,next)=>{try{const db=require("../../models"),row=await db.CheckoutSession.findOne({where:{user_id:req.user.id,status:["PENDING","READY"]},include:[{model:db.CheckoutItem,as:"items"}],order:[["createdAt","DESC"]]});res.json({success:true,data:row});}catch(e){next(e);}};exports.cancel=async(req,res,next)=>{try{const row=await service.close({id:req.params.id,userId:req.user.id,status:"CANCELLED",requestId:req.id});await logActivity({user:req.user,type:"CHECKOUT_CANCELLED",module:"Checkout",targetId:row.id,requestId:req.id});res.json({success:true,data:row});}catch(e){next(e);}};
|
||||
@@ -0,0 +1 @@
|
||||
const crypto=require("crypto"),db=require("../../models"),{logActivity}=require("../../services/activity.service");exports.list=async(req,res,next)=>{try{res.json({success:true,data:await db.WishlistItem.findAll({where:{user_id:req.user.id},include:[{model:db.Product,as:"product",where:{status:"ACTIVE",visibility:"PUBLIC"}}]})});}catch(e){next(e);}};exports.add=async(req,res,next)=>{try{const product=await db.Product.findOne({where:{id:req.body.productId,status:"ACTIVE",visibility:"PUBLIC"}});if(!product)throw Object.assign(new Error("Product not found"),{status:404,code:"NOT_FOUND"});const[row,created]=await db.WishlistItem.findOrCreate({where:{user_id:req.user.id,product_id:product.id},defaults:{id:crypto.randomUUID()}});if(created)await logActivity({user:req.user,type:"WISHLIST_ITEM_ADDED",module:"Shopping",targetId:row.id});res.status(created?201:200).json({success:true,data:row});}catch(e){next(e);}};exports.remove=async(req,res,next)=>{try{const n=await db.WishlistItem.destroy({where:{user_id:req.user.id,product_id:req.params.productId}});if(!n)throw Object.assign(new Error("Wishlist item not found"),{status:404,code:"NOT_FOUND"});await logActivity({user:req.user,type:"WISHLIST_ITEM_REMOVED",module:"Shopping",targetId:req.params.productId});res.status(204).end();}catch(e){next(e);}};
|
||||
Reference in New Issue
Block a user