feat: Implement return and refund functionality in commerce module
CI / test (push) Successful in 10m27s

- Added return request handling in return.controller.js
- Implemented webhook handling for PayHere payments in webhook.controller.js
- Created models for coupon redemptions, invoices, orders, order items, payments, payment attempts, payment webhook events, refunds, return items, and return requests.
- Developed services for order management, payment processing, refunds, and returns.
- Introduced validation schemas for payment and return requests.
- Created migration scripts for new database tables related to orders, payments, refunds, and returns.
- Added unit tests for order state transitions and PayHere provider functionality.
This commit is contained in:
Sathira Sri Sathara
2026-09-09 12:39:57 +05:30
parent ad9287b804
commit cd2c1c6d08
38 changed files with 192 additions and 4 deletions
@@ -0,0 +1,4 @@
"use strict";module.exports={async up(q,S){const ID={type:S.STRING,primaryKey:true,allowNull:false},REQ={type:S.STRING,allowNull:false},STR={type:S.STRING},DATE={type:S.DATE},DEC={type:S.DECIMAL(15,2)},INT={type:S.INTEGER,allowNull:false,defaultValue:0},TS={createdAt:{type:S.DATE,allowNull:false},updatedAt:{type:S.DATE,allowNull:false}};await q.createTable("orders",{id:ID,order_number:{type:S.STRING(80),allowNull:false,unique:true},user_id:REQ,business_customer_id:STR,checkout_session_id:{type:S.STRING,allowNull:false,unique:true},status:REQ,payment_status:REQ,fulfillment_status:REQ,currency:{type:S.STRING(3),allowNull:false},subtotal:{...DEC,allowNull:false},discount_total:{...DEC,allowNull:false},shipping_amount:{...DEC,allowNull:false},tax_amount:{...DEC,allowNull:false},duty_amount:DEC,grand_total:{...DEC,allowNull:false},shipping_address_snapshot:{type:S.JSON,allowNull:false},billing_address_snapshot:{type:S.JSON,allowNull:false},shipping_method_snapshot:{type:S.JSON,allowNull:false},coupon_code:STR,business_partner_snapshot:{type:S.JSON},placed_at:{type:S.DATE,allowNull:false},cancelled_at:DATE,completed_at:DATE,...TS});await q.addIndex("orders",["user_id","createdAt"]);await q.addIndex("orders",["status"]);await q.createTable("order_items",{id:ID,order_id:REQ,product_id:REQ,variant_id:REQ,reservation_key:{type:S.STRING(160),allowNull:false},sku:REQ,product_name:REQ,variant_description:STR,quantity:{type:S.INTEGER,allowNull:false},unit_price:{...DEC,allowNull:false},discount_amount:{...DEC,allowNull:false},line_total:{...DEC,allowNull:false},currency:{type:S.STRING(3),allowNull:false},returned_quantity:INT,refunded_quantity:INT,metadata:{type:S.JSON},createdAt:{type:S.DATE,allowNull:false}});
await q.createTable("payments",{id:ID,order_id:REQ,payment_reference:{type:S.STRING(100),allowNull:false,unique:true},provider:REQ,method:REQ,status:REQ,currency:{type:S.STRING(3),allowNull:false},amount:{...DEC,allowNull:false},provider_transaction_id:STR,provider_customer_reference:STR,failure_code:STR,failure_message:{type:S.STRING(500)},authorized_at:DATE,paid_at:DATE,failed_at:DATE,cancelled_at:DATE,...TS});await q.addIndex("payments",["order_id","status"]);await q.createTable("payment_attempts",{id:ID,payment_id:REQ,attempt_number:{type:S.INTEGER,allowNull:false},provider_request_id:{type:S.STRING,allowNull:false,unique:true},status:REQ,amount:{...DEC,allowNull:false},provider_response_code:STR,provider_transaction_id:STR,started_at:{type:S.DATE,allowNull:false},completed_at:DATE,metadata:{type:S.JSON},...TS});await q.createTable("payment_webhook_events",{id:ID,provider:REQ,provider_event_id:{type:S.STRING(160),allowNull:false},event_type:REQ,payload_hash:{type:S.STRING(64),allowNull:false},processing_status:REQ,received_at:{type:S.DATE,allowNull:false},processed_at:DATE,error_code:STR,...TS});await q.addConstraint("payment_webhook_events",{fields:["provider","provider_event_id"],type:"unique",name:"uq_payment_webhook_provider_event"});
await q.createTable("invoices",{id:ID,invoice_number:{type:S.STRING(80),allowNull:false,unique:true},order_id:{type:S.STRING,allowNull:false,unique:true},status:REQ,currency:{type:S.STRING(3),allowNull:false},subtotal:{...DEC,allowNull:false},discount_total:{...DEC,allowNull:false},shipping_amount:{...DEC,allowNull:false},tax_amount:{...DEC,allowNull:false},duty_amount:DEC,grand_total:{...DEC,allowNull:false},issued_at:{type:S.DATE,allowNull:false},paid_at:DATE,document_upload_id:{type:S.INTEGER},...TS});await q.createTable("refunds",{id:ID,refund_number:{type:S.STRING(80),allowNull:false,unique:true},operation_key:{type:S.STRING(160),allowNull:false,unique:true},order_id:REQ,payment_id:REQ,status:REQ,amount:{...DEC,allowNull:false},currency:{type:S.STRING(3),allowNull:false},reason_code:REQ,reason_text:{type:S.STRING(500)},provider_refund_id:STR,requested_by:REQ,approved_by:STR,requested_at:{type:S.DATE,allowNull:false},processed_at:DATE,...TS});await q.addIndex("refunds",["order_id","status"]);await q.createTable("refund_items",{id:ID,refund_id:REQ,order_item_id:REQ,quantity:{type:S.INTEGER,allowNull:false},amount:{...DEC,allowNull:false},...TS});
await q.createTable("return_requests",{id:ID,return_number:{type:S.STRING(80),allowNull:false,unique:true},order_id:REQ,user_id:REQ,status:REQ,reason_code:REQ,reason_text:{type:S.STRING(500)},requested_at:{type:S.DATE,allowNull:false},approved_at:DATE,received_at:DATE,completed_at:DATE,...TS});await q.addIndex("return_requests",["order_id","status"]);await q.createTable("return_items",{id:ID,return_request_id:REQ,order_item_id:REQ,quantity:{type:S.INTEGER,allowNull:false},resolution:REQ,condition:REQ,refund_amount:DEC,...TS});await q.createTable("coupon_redemptions",{id:ID,coupon_id:REQ,user_id:REQ,order_id:REQ,redeemed_at:{type:S.DATE,allowNull:false},...TS});await q.addConstraint("coupon_redemptions",{fields:["coupon_id","order_id"],type:"unique",name:"uq_coupon_order_redemption"});},async down(){throw new Error("Phase 7 migration is forward-only; restore from a verified backup instead");}};