feat: implement business pricing service and money utilities

- Added `businessPricing.service.js` to handle business customer pricing logic.
- Introduced `money.js` for money parsing, formatting, and calculations.
- Created `pricing.service.js` to manage variant quoting and promotion application.
- Updated validation schemas in `catalogue.schemas.js` and `inventoryMerchandising.schemas.js` for new pricing and inventory features.
- Implemented cron job for inventory reservation expiry in `inventoryReservationExpiry.cron.js`.
- Created database migrations for catalogue and inventory structures.
- Added unit tests for catalogue localization, validation, and inventory merchandising functionalities.
This commit is contained in:
Sathira Sri Sathara
2026-09-03 19:25:13 +05:30
parent fd4e324571
commit 5643d89236
75 changed files with 663 additions and 1 deletions
@@ -0,0 +1,21 @@
"use strict";
module.exports={async up(q,S){await q.sequelize.transaction(async transaction=>{const ts={createdAt:{type:S.DATE,allowNull:false},updatedAt:{type:S.DATE,allowNull:false}};const table=(name,fields)=>q.createTable(name,{...fields,...ts},{transaction});
await table("brands",{id:{type:S.STRING,primaryKey:true},name:{type:S.STRING(160),allowNull:false},slug:{type:S.STRING(180),allowNull:false,unique:true},description:S.TEXT,logo_upload_id:S.INTEGER,website:S.STRING,status:{type:S.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"INACTIVE"},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("brands",["status","sort_order"],{name:"brands_public_idx",transaction});
await table("categories",{id:{type:S.STRING,primaryKey:true},parent_id:{type:S.STRING,allowNull:true,references:{model:"categories",key:"id"},onDelete:"RESTRICT"},code:{type:S.STRING(80),allowNull:false,unique:true},slug:{type:S.STRING(180),allowNull:false,unique:true},status:{type:S.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"INACTIVE"},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0},image_upload_id:S.INTEGER});await q.addIndex("categories",["parent_id","status","sort_order"],{name:"categories_tree_idx",transaction});
await table("category_translations",{id:{type:S.STRING,primaryKey:true},category_id:{type:S.STRING,allowNull:false,references:{model:"categories",key:"id"},onDelete:"CASCADE"},locale:{type:S.ENUM("en","si","ta"),allowNull:false},name:{type:S.STRING(180),allowNull:false},description:S.TEXT,meta_title:S.STRING(180),meta_description:S.STRING(320)});await q.addIndex("category_translations",["category_id","locale"],{unique:true,name:"category_translation_locale_unique",transaction});
await table("size_guides",{id:{type:S.STRING,primaryKey:true},code:{type:S.STRING(80),allowNull:false,unique:true},name:{type:S.STRING(160),allowNull:false},locale:{type:S.ENUM("en","si","ta"),allowNull:false,defaultValue:"en"},data:{type:S.JSON,allowNull:false},category_id:{type:S.STRING,allowNull:true,references:{model:"categories",key:"id"},onDelete:"SET NULL"},status:{type:S.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"ACTIVE"}});
await table("products",{id:{type:S.STRING,primaryKey:true},brand_id:{type:S.STRING,allowNull:false,references:{model:"brands",key:"id"},onDelete:"RESTRICT"},default_category_id:{type:S.STRING,allowNull:true,references:{model:"categories",key:"id"},onDelete:"SET NULL"},slug:{type:S.STRING(200),allowNull:false,unique:true},product_code:{type:S.STRING(100),allowNull:false,unique:true},status:{type:S.ENUM("DRAFT","ACTIVE","INACTIVE","ARCHIVED"),allowNull:false,defaultValue:"DRAFT"},visibility:{type:S.ENUM("PUBLIC","HIDDEN"),allowNull:false,defaultValue:"HIDDEN"},product_type:{type:S.STRING(80),allowNull:false,defaultValue:"STANDARD"},featured:{type:S.BOOLEAN,allowNull:false,defaultValue:false},new_arrival_until:S.DATE,published_at:S.DATE,created_by:{type:S.STRING,allowNull:false,references:{model:"users",key:"id"}},updated_by:S.STRING,size_guide_id:{type:S.STRING,allowNull:true,references:{model:"size_guides",key:"id"},onDelete:"SET NULL"}});await q.addIndex("products",["status","visibility","published_at"],{name:"products_public_idx",transaction});await q.addIndex("products",["brand_id","featured","new_arrival_until"],{name:"products_merchandising_idx",transaction});
await table("product_translations",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},locale:{type:S.ENUM("en","si","ta"),allowNull:false},name:{type:S.STRING(220),allowNull:false},short_description:S.STRING(500),description:S.TEXT,care_instructions:S.TEXT,materials:S.TEXT,origin:S.STRING(120),meta_title:S.STRING(180),meta_description:S.STRING(320)});await q.addIndex("product_translations",["product_id","locale"],{unique:true,name:"product_translation_locale_unique",transaction});await q.addIndex("product_translations",["name"],{name:"product_translation_name_idx",transaction});
await table("product_categories",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},category_id:{type:S.STRING,allowNull:false,references:{model:"categories",key:"id"},onDelete:"RESTRICT"}});await q.addIndex("product_categories",["product_id","category_id"],{unique:true,name:"product_category_unique",transaction});await q.addIndex("product_categories",["category_id","product_id"],{name:"product_category_lookup_idx",transaction});
await table("product_variants",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},sku:{type:S.STRING(100),allowNull:false,unique:true},barcode:{type:S.STRING(100),allowNull:true,unique:true},status:{type:S.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"ACTIVE"},base_price:{type:S.DECIMAL(15,2),allowNull:false},compare_at_price:S.DECIMAL(15,2),currency:{type:S.STRING(3),allowNull:false,defaultValue:"LKR"},weight:S.DECIMAL(10,3),sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("product_variants",["product_id","status","base_price"],{name:"product_variant_public_idx",transaction});
await table("product_options",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},name:{type:S.STRING(100),allowNull:false},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("product_options",["product_id","name"],{unique:true,name:"product_option_name_unique",transaction});
await table("product_option_values",{id:{type:S.STRING,primaryKey:true},option_id:{type:S.STRING,allowNull:false,references:{model:"product_options",key:"id"},onDelete:"CASCADE"},value:{type:S.STRING(100),allowNull:false},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("product_option_values",["option_id","value"],{unique:true,name:"product_option_value_unique",transaction});
await table("variant_option_values",{id:{type:S.STRING,primaryKey:true},variant_id:{type:S.STRING,allowNull:false,references:{model:"product_variants",key:"id"},onDelete:"CASCADE"},option_value_id:{type:S.STRING,allowNull:false,references:{model:"product_option_values",key:"id"},onDelete:"CASCADE"}});await q.addIndex("variant_option_values",["variant_id","option_value_id"],{unique:true,name:"variant_option_value_unique",transaction});
await table("product_attributes",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},name:{type:S.STRING(100),allowNull:false},value:{type:S.STRING(500),allowNull:false},locale:{type:S.ENUM("en","si","ta"),allowNull:false,defaultValue:"en"},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("product_attributes",["product_id","locale"],{name:"product_attribute_lookup_idx",transaction});
await table("product_media",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},variant_id:{type:S.STRING,allowNull:true,references:{model:"product_variants",key:"id"},onDelete:"CASCADE"},upload_id:{type:S.INTEGER,allowNull:false,references:{model:"uploads",key:"id"},onDelete:"RESTRICT"},type:{type:S.ENUM("IMAGE"),allowNull:false,defaultValue:"IMAGE"},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0},alt_text:S.STRING(250),is_primary:{type:S.BOOLEAN,allowNull:false,defaultValue:false}});await q.addIndex("product_media",["product_id","upload_id"],{unique:true,name:"product_media_upload_unique",transaction});await q.addIndex("product_media",["product_id","is_primary","sort_order"],{name:"product_media_gallery_idx",transaction});
await table("collections",{id:{type:S.STRING,primaryKey:true},slug:{type:S.STRING(180),allowNull:false,unique:true},type:{type:S.STRING(80),allowNull:false},status:{type:S.ENUM("DRAFT","ACTIVE","INACTIVE"),allowNull:false,defaultValue:"DRAFT"},starts_at:S.DATE,ends_at:S.DATE,hero_upload_id:S.INTEGER,sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("collections",["status","starts_at","ends_at"],{name:"collections_publish_window_idx",transaction});
await table("collection_translations",{id:{type:S.STRING,primaryKey:true},collection_id:{type:S.STRING,allowNull:false,references:{model:"collections",key:"id"},onDelete:"CASCADE"},locale:{type:S.ENUM("en","si","ta"),allowNull:false},name:{type:S.STRING(180),allowNull:false},description:S.TEXT,headline:S.STRING(250),subheadline:S.STRING(300)});await q.addIndex("collection_translations",["collection_id","locale"],{unique:true,name:"collection_translation_locale_unique",transaction});
await table("collection_products",{id:{type:S.STRING,primaryKey:true},collection_id:{type:S.STRING,allowNull:false,references:{model:"collections",key:"id"},onDelete:"CASCADE"},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0},featured:{type:S.BOOLEAN,allowNull:false,defaultValue:false}});await q.addIndex("collection_products",["collection_id","product_id"],{unique:true,name:"collection_product_unique",transaction});await q.addIndex("collection_products",["collection_id","sort_order"],{name:"collection_product_order_idx",transaction});
await table("product_relations",{id:{type:S.STRING,primaryKey:true},source_product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},target_product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},relation_type:{type:S.ENUM("RELATED","SIMILAR","COMPLETE_THE_LOOK"),allowNull:false},sort_order:{type:S.INTEGER,allowNull:false,defaultValue:0}});await q.addIndex("product_relations",["source_product_id","target_product_id","relation_type"],{unique:true,name:"product_relation_unique",transaction});await q.sequelize.query("ALTER TABLE product_relations ADD CONSTRAINT product_relation_not_self CHECK (source_product_id <> target_product_id)",{transaction});
await table("product_reviews",{id:{type:S.STRING,primaryKey:true},product_id:{type:S.STRING,allowNull:false,references:{model:"products",key:"id"},onDelete:"CASCADE"},user_id:{type:S.STRING,allowNull:false,references:{model:"users",key:"id"},onDelete:"RESTRICT"},rating:{type:S.INTEGER,allowNull:false},title:{type:S.STRING(160),allowNull:false},body:{type:S.TEXT,allowNull:false},status:{type:S.ENUM("PENDING","APPROVED","REJECTED"),allowNull:false,defaultValue:"PENDING"},verified_purchase:{type:S.BOOLEAN,allowNull:false,defaultValue:false},moderated_by:S.STRING,moderated_at:S.DATE});await q.addIndex("product_reviews",["user_id","product_id"],{unique:true,name:"product_review_user_unique",transaction});await q.addIndex("product_reviews",["product_id","status","createdAt"],{name:"product_review_public_idx",transaction});await q.sequelize.query("ALTER TABLE product_reviews ADD CONSTRAINT product_review_rating_check CHECK (rating BETWEEN 1 AND 5)",{transaction});
});},async down(){throw new Error("Phase 4 migration is forward-only; restore from backup for rollback");}};