diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ca2b787 Binary files /dev/null and b/.DS_Store differ diff --git a/api b/api new file mode 100755 index 0000000..eb50238 Binary files /dev/null and b/api differ diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000..c0c1dfc Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/index.html b/docs/index.html index 0c0e422..8507d19 100644 --- a/docs/index.html +++ b/docs/index.html @@ -136,20 +136,37 @@ const E=[ {cat:"Master Data / Postal Code",m:"post",p:"/master-data/postal-codes/import",s:"Import postal codes from CSV",q:[],rb:"_MULTIPART_",rbe:"{\n \"success\": true,\n \"data\": {\n \"job_id\": \"job_abc123def456\",\n \"total_rows\": 100,\n \"imported\": 95,\n \"skipped\": 5,\n \"logs\": [\n {\n \"row\": 2,\n \"status\": \"skipped\",\n \"data\": \"Jawa Barat\",\n \"message\": \"duplicate\"\n }\n ],\n \"errors\": [],\n \"is_async\": false\n }\n}",rf:[]}, {cat:"Master Data / Postal Code",m:"post",p:"/master-data/postal-codes/import/stop",s:"Stop postal code import job",q:[],rb:"{\n \"job_id\": \"job_abc123def456\"\n}",rbe:"{\n \"success\": true,\n \"message\": \"Job stopped\"\n}",rf:["job_id"]}, {cat:"Master Data / Postal Code",m:"get",p:"/master-data/postal-codes/import/status/:jobId",s:"Check postal code import job status",q:[{n:"jobId",t:"string",req:true,d:"Async job ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"job\": {\n \"id\": \"job_abc123def456\",\n \"status\": \"completed\",\n \"total\": 100,\n \"imported\": 95,\n \"skipped\": 5\n },\n \"logs\": []\n }\n}",rf:[]}, -{cat:"Master Data / Cost Component",m:"get",p:"/master-data/cost-components/",s:"Get all cost components",q:[{n:"name",t:"string",req:false,d:"Filter by name"},{n:"code",t:"string",req:false,d:"Filter by code"},{n:"component_type",t:"integer",req:false,d:"Filter by type (0=Input, 1=Formula)"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n ]\n}",rf:[]}, +{cat:"Master Data / Cost Component",m:"get",p:"/master-data/cost-components/",s:"Get all cost components",q:[{n:"name",t:"string",req:false,d:"Filter by name"},{n:"code",t:"string",req:false,d:"Filter by code"},{n:"component_type",t:"integer",req:false,d:"Filter by type (0=Input, 1=Formula)"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"unit_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n ]\n}",rf:[]}, {cat:"Master Data / Cost Component",m:"post",p:"/master-data/cost-components/list",s:"Cost component list (DataTable)",q:[],rb:"_DT_",rbe:"{\n \"success\": true,\n \"data\": {\n \"draw\": 1,\n \"recordsTotal\": 34,\n \"recordsFiltered\": 34,\n \"data\": [\n { \"id\": \"01HXYZ123456789012345678\", \"name\": \"...\" }\n ]\n }\n}",rf:[]}, -{cat:"Master Data / Cost Component",m:"get",p:"/master-data/cost-components/:id",s:"Get cost component by ID",q:[{n:"id",t:"string",req:true,d:"Cost component ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:[]}, -{cat:"Master Data / Cost Component",m:"post",p:"/master-data/cost-components/",s:"Create cost component",q:[],rb:"{\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"value\": \"15000\",\n \"component_type\": 0\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name","code","value"]}, -{cat:"Master Data / Cost Component",m:"put",p:"/master-data/cost-components/:id",s:"Update cost component",q:[{n:"id",t:"string",req:true,d:"Cost component ID"}],rb:"{\n \"name\": \"Biaya Pengiriman Updated\",\n \"code\": \"SHIP\",\n \"value\": \"20000\",\n \"component_type\": 0\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman Updated\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"20000\",\n \"component_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name","code","value"]}, +{cat:"Master Data / Cost Component",m:"get",p:"/master-data/cost-components/:id",s:"Get cost component by ID",q:[{n:"id",t:"string",req:true,d:"Cost component ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"unit_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:[]}, +{cat:"Master Data / Cost Component",m:"post",p:"/master-data/cost-components/",s:"Create cost component",q:[],rb:"{\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"value\": \"15000\",\n \"component_type\": 0,\n \"unit_type\": 0\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"15000\",\n \"component_type\": 0,\n \"unit_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name","code","value"]}, +{cat:"Master Data / Cost Component",m:"put",p:"/master-data/cost-components/:id",s:"Update cost component",q:[{n:"id",t:"string",req:true,d:"Cost component ID"}],rb:"{\n \"name\": \"Biaya Pengiriman Updated\",\n \"code\": \"SHIP\",\n \"value\": \"20000\",\n \"component_type\": 0,\n \"unit_type\": 0\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Biaya Pengiriman Updated\",\n \"code\": \"SHIP\",\n \"inc\": 1,\n \"value\": \"20000\",\n \"component_type\": 0,\n \"unit_type\": 0,\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name","code","value"]}, {cat:"Master Data / Cost Component",m:"delete",p:"/master-data/cost-components/:id",s:"Delete cost component",q:[{n:"id",t:"string",req:true,d:"Cost component ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"cost component deleted\"\n}",rf:[]}, {cat:"Master Data / Cost Component",m:"post",p:"/master-data/cost-components/sortup",s:"Move cost component up in sort order",q:[],rb:"{\n \"id\": \"01HXYZ123456789012345678\"\n}",rbe:"{\n \"success\": true,\n \"message\": \"Sorting Up Success\"\n}",rf:["id"]}, {cat:"Master Data / Cost Component",m:"post",p:"/master-data/cost-components/sortdown",s:"Move cost component down in sort order",q:[],rb:"{\n \"id\": \"01HXYZ123456789012345678\"\n}",rbe:"{\n \"success\": true,\n \"message\": \"Sorting Down Success\"\n}",rf:["id"]}, -{cat:"Basic Pricing",m:"get",p:"/basic-pricings/",s:"Get all basic pricings",q:[{n:"name",t:"string",req:false,d:"Filter by name"},{n:"province_source_id",t:"string",req:false,d:"Source province ID"},{n:"city_source_id",t:"string",req:false,d:"Source city ID"},{n:"package_type_id",t:"string",req:false,d:"Package type ID"},{n:"moda_id",t:"string",req:false,d:"Transport mode ID"}],rb:null,rbe:"{\\n \"success\": true,\\n \"data\": [\\n { ... }\\n ]\\n}",rf:[]}, +{cat:"Basic Pricing",m:"get",p:"/basic-pricings/",s:"Get all basic pricings",q:[{n:"province_source_id",t:"string",req:false,d:"Source province ID"},{n:"city_source_id",t:"string",req:false,d:"Source city ID"},{n:"package_type_id",t:"string",req:false,d:"Package type ID"},{n:"moda_id",t:"string",req:false,d:"Transport mode ID"}],rb:null,rbe:"{\\n \"success\": true,\\n \"data\": [\\n { ... }\\n ]\\n}",rf:[]}, {cat:"Basic Pricing",m:"post",p:"/basic-pricings/list",s:"Basic pricing list (DataTable)",q:[],rb:"_DT_",rbe:"{\n \"success\": true,\n \"data\": {\n \"draw\": 1,\n \"recordsTotal\": 34,\n \"recordsFiltered\": 34,\n \"data\": [\n { \"id\": \"01HXYZ123456789012345678\", \"name\": \"...\" }\n ]\n }\n}",rf:[]}, -{cat:"Basic Pricing",m:"get",p:"/basic-pricings/:id",s:"Get basic pricing by ID",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Basic Price Bandung-Jakarta\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": 15000\n }\n ]\n }\n}",rf:[]}, -{cat:"Basic Pricing",m:"post",p:"/basic-pricings/",s:"Create basic pricing",q:[],rb:"{\n \"name\": \"Basic Price Bandung-Jakarta\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": 15000\n }\n ]\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Basic Price Bandung-Jakarta\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": 15000\n }\n ]\n }\n}",rf:["name"]}, -{cat:"Basic Pricing",m:"put",p:"/basic-pricings/:id",s:"Update basic pricing",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:"{\n \"name\": \"Basic Price Bandung-Jakarta\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": 15000\n }\n ]\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Basic Price Bandung-Jakarta\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": 15000\n }\n ]\n }\n}",rf:["name"]}, -{cat:"Basic Pricing",m:"delete",p:"/basic-pricings/:id",s:"Delete basic pricing",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"basic pricing deleted\"\n}",rf:[]} +{cat:"Basic Pricing",m:"get",p:"/basic-pricings/:id",s:"Get basic pricing by ID",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"lead_time\": \"2-3 days\",\n \"minimum_weight\": 1.0,\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n }\n}",rf:[]}, +{cat:"Basic Pricing",m:"post",p:"/basic-pricings/match",s:"Match basic pricing by ID",q:[],rb:"{\n \"id\": \"01HXYZ123456789012345678\"\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n }\n}",rf:["id"]}, +{cat:"Basic Pricing",m:"post",p:"/basic-pricings/",s:"Create basic pricing",q:[],rb:"{\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"lead_time\": \"2-3 days\",\n \"minimum_weight\": 1.0,\n \"components\": [\n {\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"lead_time\": \"2-3 days\",\n \"minimum_weight\": 1.0,\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n }\n}",rf:["name"]}, +{cat:"Basic Pricing",m:"put",p:"/basic-pricings/:id",s:"Update basic pricing",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:"{\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"lead_time\": \"2-3 days\",\n \"minimum_weight\": 1.0,\n \"components\": [\n {\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"province_source_id\": \"01HXYZ123456789012345678\",\n \"city_source_id\": \"01HXYZ123456789012345678\",\n \"district_source_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_source_id\": \"01HXYZ123456789012345678\",\n \"postal_code_source_id\": \"01HXYZ123456789012345678\",\n \"province_destination_id\": \"01HXYZ123456789012345678\",\n \"city_destination_id\": \"01HXYZ123456789012345678\",\n \"district_destination_id\": \"01HXYZ123456789012345678\",\n \"subdistrict_destination_id\": \"01HXYZ123456789012345678\",\n \"postal_code_destination_id\": \"01HXYZ123456789012345678\",\n \"package_type_id\": \"01HXYZ123456789012345678\",\n \"moda_id\": \"01HXYZ123456789012345678\",\n \"lead_time\": \"2-3 days\",\n \"minimum_weight\": 1.0,\n \"merchant_id\": \"01HXYZ123456789012345678\",\n \"components\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"cost_component_id\": \"01HXYZ123456789012345678\",\n \"value\": \"15000\"\n }\n ]\n }\n}",rf:["name"]}, +{cat:"Basic Pricing",m:"delete",p:"/basic-pricings/:id",s:"Delete basic pricing",q:[{n:"id",t:"string",req:true,d:"Basic pricing ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"basic pricing deleted\"\n}",rf:[]}, +{cat:"Master Data / Package Type",m:"get",p:"/master-data/package-types/",s:"Get all package types",q:[{n:"id",t:"string",req:false,d:"Filter by ID"},{n:"name",t:"string",req:false,d:"Filter by name"},{n:"merchant_id",t:"string",req:false,d:"Filter by merchant"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Reguler\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n ]\n}",rf:[]}, +{cat:"Master Data / Package Type",m:"post",p:"/master-data/package-types/list",s:"Package type list (DataTable)",q:[],rb:"_DT_",rbe:"{\n \"success\": true,\n \"data\": {\n \"draw\": 1,\n \"recordsTotal\": 10,\n \"recordsFiltered\": 10,\n \"data\": [\n { \"id\": \"01HXYZ123456789012345678\", \"name\": \"Reguler\" }\n ]\n }\n}",rf:[]}, +{cat:"Master Data / Package Type",m:"get",p:"/master-data/package-types/:id",s:"Get package type by ID",q:[{n:"id",t:"string",req:true,d:"Package type ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Reguler\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:[]}, +{cat:"Master Data / Package Type",m:"post",p:"/master-data/package-types/",s:"Create package type",q:[],rb:"{\n \"name\": \"Reguler\"\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Reguler\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name"]}, +{cat:"Master Data / Package Type",m:"put",p:"/master-data/package-types/:id",s:"Update package type",q:[{n:"id",t:"string",req:true,d:"Package type ID"}],rb:"{\n \"name\": \"Express\"\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Express\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name"]}, +{cat:"Master Data / Package Type",m:"delete",p:"/master-data/package-types/:id",s:"Delete package type",q:[{n:"id",t:"string",req:true,d:"Package type ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"package type deleted\"\n}",rf:[]}, +{cat:"Master Data / Moda",m:"get",p:"/master-data/modas/",s:"Get all modas",q:[{n:"id",t:"string",req:false,d:"Filter by ID"},{n:"name",t:"string",req:false,d:"Filter by name"},{n:"merchant_id",t:"string",req:false,d:"Filter by merchant"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Darat\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n ]\n}",rf:[]}, +{cat:"Master Data / Moda",m:"post",p:"/master-data/modas/list",s:"Moda list (DataTable)",q:[],rb:"_DT_",rbe:"{\n \"success\": true,\n \"data\": {\n \"draw\": 1,\n \"recordsTotal\": 5,\n \"recordsFiltered\": 5,\n \"data\": [\n { \"id\": \"01HXYZ123456789012345678\", \"name\": \"Darat\" }\n ]\n }\n}",rf:[]}, +{cat:"Master Data / Moda",m:"get",p:"/master-data/modas/:id",s:"Get moda by ID",q:[{n:"id",t:"string",req:true,d:"Moda ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Darat\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:[]}, +{cat:"Master Data / Moda",m:"post",p:"/master-data/modas/",s:"Create moda",q:[],rb:"{\n \"name\": \"Darat\"\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Darat\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name"]}, +{cat:"Master Data / Moda",m:"put",p:"/master-data/modas/:id",s:"Update moda",q:[{n:"id",t:"string",req:true,d:"Moda ID"}],rb:"{\n \"name\": \"Udara\"\n}",rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"name\": \"Udara\",\n \"merchant_id\": \"01HXYZ123456789012345678\"\n }\n}",rf:["name"]}, +{cat:"Master Data / Moda",m:"delete",p:"/master-data/modas/:id",s:"Delete moda",q:[{n:"id",t:"string",req:true,d:"Moda ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"moda deleted\"\n}",rf:[]}, +{cat:"Activity Log",m:"get",p:"/activity-logs/",s:"Get all activity logs",q:[{n:"table_name",t:"string",req:false,d:"Filter by table name"},{n:"record_id",t:"string",req:false,d:"Filter by record ID"},{n:"action",t:"string",req:false,d:"Filter by action (CREATE, UPDATE, DELETE, REVERT)"},{n:"user_id",t:"string",req:false,d:"Filter by user ID"},{n:"date_from",t:"string",req:false,d:"Filter from date"},{n:"date_to",t:"string",req:false,d:"Filter to date"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": [\n {\n \"id\": \"01HXYZ123456789012345678\",\n \"table_name\": \"province\",\n \"record_id\": \"01HXYZ123456789012345678\",\n \"action\": \"DELETE\",\n \"old_data\": \"{...}\",\n \"new_data\": null,\n \"cascade_data\": \"[{...}]\",\n \"user_id\": \"01HXYZ123456789012345678\",\n \"user_name\": \"Admin Utama\",\n \"created_on\": \"2026-09-05 10:30:00\",\n \"cascade_count\": 1102\n }\n ]\n}",rf:[]}, +{cat:"Activity Log",m:"post",p:"/activity-logs/list",s:"Activity log list (DataTable)",q:[],rb:"_DT_",rbe:"{\n \"success\": true,\n \"data\": {\n \"draw\": 1,\n \"recordsTotal\": 42,\n \"recordsFiltered\": 42,\n \"data\": []\n }\n}",rf:[]}, +{cat:"Activity Log",m:"get",p:"/activity-logs/:id",s:"Get activity log by ID",q:[{n:"id",t:"string",req:true,d:"Activity log ID"}],rb:null,rbe:"{\n \"success\": true,\n \"data\": {\n \"id\": \"01HXYZ123456789012345678\",\n \"table_name\": \"province\",\n \"record_id\": \"01HXYZ123456789012345678\",\n \"action\": \"DELETE\",\n \"old_data\": \"{...}\",\n \"cascade_data\": \"[{...}]\",\n \"user_id\": \"01HXYZ123456789012345678\",\n \"user_name\": \"Admin Utama\",\n \"created_on\": \"2026-09-05 10:30:00\",\n \"cascade_count\": 1102\n }\n}",rf:[]}, +{cat:"Activity Log",m:"post",p:"/activity-logs/:id/revert",s:"Revert activity log action",q:[{n:"id",t:"string",req:true,d:"Activity log ID"}],rb:null,rbe:"{\n \"success\": true,\n \"message\": \"action reverted successfully\"\n}",rf:["id"]} ]; function fmtMethod(m){return m.toUpperCase()} function methodColor(m){return{get:"var(--get)",post:"var(--post)",put:"var(--put)",delete:"var(--delete)"}[m]||"var(--text2)"} diff --git a/internal/database/database.go b/internal/database/database.go index 6ea600e..4da7059 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -54,7 +54,10 @@ func (d *Database) Connect() { ) d.AppLogger.Info("Connect To Master DB") gormLogger := logger.NewGormZapLogger(d.Logger, 5*time.Second) - dbMaster, err := gorm.Open(postgres.Open(dsnMaster), &gorm.Config{ + dbMaster, err := gorm.Open(postgres.New(postgres.Config{ + DSN: dsnMaster, + PreferSimpleProtocol: true, + }), &gorm.Config{ Logger: gormLogger, }) if err != nil { @@ -76,7 +79,10 @@ func (d *Database) Connect() { ssl_slave, ) d.AppLogger.Info("Connect To Slave DB") - dbSlave, err := gorm.Open(postgres.Open(dsnSlave), &gorm.Config{ + dbSlave, err := gorm.Open(postgres.New(postgres.Config{ + DSN: dsnSlave, + PreferSimpleProtocol: true, + }), &gorm.Config{ Logger: gormLogger, }) if err != nil { diff --git a/internal/domain/activity_log.go b/internal/domain/activity_log.go new file mode 100644 index 0000000..c766a4f --- /dev/null +++ b/internal/domain/activity_log.go @@ -0,0 +1,23 @@ +package domain + +import "time" + +const TableNameActivityLogConst = "activity_log" + +type ActivityLog struct { + ID string `gorm:"column:id;type:character(26);primaryKey;default:generate_ulid()" json:"id"` + TargetTable string `gorm:"column:table_name;type:character varying(100);not null" json:"table_name"` + RecordID string `gorm:"column:record_id;type:character(26);not null" json:"record_id"` + Action string `gorm:"column:action;type:character varying(20);not null" json:"action"` + OldData *string `gorm:"column:old_data;type:jsonb" json:"old_data"` + NewData *string `gorm:"column:new_data;type:jsonb" json:"new_data"` + CascadeData *string `gorm:"column:cascade_data;type:jsonb" json:"cascade_data"` + UserID string `gorm:"column:user_id;type:character(26);not null" json:"user_id"` + UserName string `gorm:"column:user_name;type:character varying(255);not null" json:"user_name"` + MerchantID *string `gorm:"column:merchant_id;type:character(26)" json:"merchant_id"` + CreatedOn *time.Time `gorm:"column:created_on;type:timestamp without time zone;default:now()" json:"created_on"` +} + +func (*ActivityLog) TableName() string { + return TableNameActivityLogConst +} diff --git a/internal/domain/basic_pricing_component.go b/internal/domain/basic_pricing_component.go index 666f7f4..fb00b8c 100644 --- a/internal/domain/basic_pricing_component.go +++ b/internal/domain/basic_pricing_component.go @@ -10,7 +10,7 @@ const TableNameBasicPricingComponent = "basic_pricing_component" type BasicPricingComponent struct { ID string `gorm:"column:id;type:character(26);primaryKey;default:generate_ulid()" json:"id"` CostComponentID *string `gorm:"column:cost_component_id;type:character(26);index:basic_pricing_component_cost_component_id_idx,priority:1" json:"cost_component_id"` - Value *float32 `gorm:"column:value;type:real" json:"value"` + Value *string `gorm:"column:value;type:character varying(255)" json:"value"` BasicPricingID *string `gorm:"column:basic_pricing_id;type:character(26);index:basic_pricing_component_basic_pricing_id_idx,priority:1" json:"basic_pricing_id"` BasicPricing_Relation BasicPricing `gorm:"foreignKey:basic_pricing_id;references:id" json:"basic_pricing_relation"` CostComponent_Relation CostComponent `gorm:"foreignKey:cost_component_id;references:id" json:"cost_component_relation"` diff --git a/internal/domain/postal_code.go b/internal/domain/postal_code.go index 656d1fb..3006a3a 100644 --- a/internal/domain/postal_code.go +++ b/internal/domain/postal_code.go @@ -21,7 +21,7 @@ type PostalCode struct { CreatedBy *string `gorm:"column:created_by;type:character(26);index:postal_code_created_by_idx,priority:1" json:"created_by"` CreatedOn *time.Time `gorm:"column:created_on;type:timestamp without time zone" json:"created_on"` UpdatedBy *string `gorm:"column:updated_by;type:character(26)" json:"updated_by"` - UpdatedOn *time.Time `gorm:"column:updated_on;type:timestamp without time zone" json:"updated_on"` + UpdatedOn *string `gorm:"column:updated_on;type:character varying" json:"updated_on"` MerchantID *string `gorm:"column:merchant_id;type:character(26)" json:"merchant_id"` BasicPricing_ByPostalCodeDestination_Relation []BasicPricing `gorm:"foreignKey:postal_code_destination_id;references:id" json:"basic_pricing_by_postal_code_destination_relation"` BasicPricing_ByPostalCodeSource_Relation []BasicPricing `gorm:"foreignKey:postal_code_source_id;references:id" json:"basic_pricing_by_postal_code_source_relation"` diff --git a/internal/handlers/dto/activity_log.go b/internal/handlers/dto/activity_log.go new file mode 100644 index 0000000..58e6c0c --- /dev/null +++ b/internal/handlers/dto/activity_log.go @@ -0,0 +1,27 @@ +package dto + +type ActivityLogFilterRequest struct { + ID string `json:"id" form:"id"` + TableName string `json:"table_name" form:"table_name"` + RecordID string `json:"record_id" form:"record_id"` + Action string `json:"action" form:"action"` + UserID string `json:"user_id" form:"user_id"` + MerchantID string `json:"merchant_id" form:"merchant_id"` + DateFrom string `json:"date_from" form:"date_from"` + DateTo string `json:"date_to" form:"date_to"` +} + +type ActivityLogResponse struct { + ID string `json:"id"` + TableName string `json:"table_name"` + RecordID string `json:"record_id"` + Action string `json:"action"` + OldData *string `json:"old_data"` + NewData *string `json:"new_data"` + CascadeData *string `json:"cascade_data"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + MerchantID *string `json:"merchant_id"` + CreatedOn *string `json:"created_on"` + CascadeCount int `json:"cascade_count,omitempty"` +} diff --git a/internal/handlers/dto/basic_pricing.go b/internal/handlers/dto/basic_pricing.go index f384f10..de6292c 100644 --- a/internal/handlers/dto/basic_pricing.go +++ b/internal/handlers/dto/basic_pricing.go @@ -1,67 +1,71 @@ package dto type BasicPricingComponentRequest struct { - CostComponentID string `json:"cost_component_id" binding:"required"` - Value float32 `json:"value"` + CostComponentID string `json:"cost_component_id" binding:"required"` + Value string `json:"value"` } type BasicPricingComponentResponse struct { - ID string `json:"id"` - CostComponentID *string `json:"cost_component_id"` - Value *float32 `json:"value"` + ID string `json:"id"` + CostComponentID *string `json:"cost_component_id"` + Value *string `json:"value"` } type CreateBasicPricingRequest struct { - ProvinceSourceID string `json:"province_source_id" binding:"required"` - CitySourceID string `json:"city_source_id" binding:"required"` - DistrictSourceID string `json:"district_source_id" binding:"required"` - SubdistrictSourceID string `json:"subdistrict_source_id" binding:"required"` - PostalCodeSourceID string `json:"postal_code_source_id" binding:"required"` - ProvinceDestinationID string `json:"province_destination_id" binding:"required"` - CityDestinationID string `json:"city_destination_id" binding:"required"` - DistrictDestinationID string `json:"district_destination_id" binding:"required"` - SubdistrictDestinationID string `json:"subdistrict_destination_id" binding:"required"` - PostalCodeDestinationID string `json:"postal_code_destination_id" binding:"required"` - PackageTypeID string `json:"package_type_id" binding:"required"` - ModaID string `json:"moda_id" binding:"required"` - LeadTime string `json:"lead_time"` - MinimumWeight float32 `json:"minimum_weight"` - Components []BasicPricingComponentRequest `json:"components" binding:"required,dive"` + ProvinceSourceID string `json:"province_source_id" binding:"required"` + CitySourceID string `json:"city_source_id" binding:"required"` + DistrictSourceID string `json:"district_source_id" binding:"required"` + SubdistrictSourceID string `json:"subdistrict_source_id" binding:"required"` + PostalCodeSourceID string `json:"postal_code_source_id" binding:"required"` + ProvinceDestinationID string `json:"province_destination_id" binding:"required"` + CityDestinationID string `json:"city_destination_id" binding:"required"` + DistrictDestinationID string `json:"district_destination_id" binding:"required"` + SubdistrictDestinationID string `json:"subdistrict_destination_id" binding:"required"` + PostalCodeDestinationID string `json:"postal_code_destination_id" binding:"required"` + PackageTypeID string `json:"package_type_id" binding:"required"` + ModaID string `json:"moda_id" binding:"required"` + LeadTime string `json:"lead_time" binding:"required"` + MinimumWeight float32 `json:"minimum_weight" binding:"required"` + Components []BasicPricingComponentRequest `json:"components" binding:"required,dive"` } type CreateBasicPricingResponse struct { - ID string `json:"id"` - ProvinceSourceID *string `json:"province_source_id"` - CitySourceID *string `json:"city_source_id"` - DistrictSourceID *string `json:"district_source_id"` - SubdistrictSourceID *string `json:"subdistrict_source_id"` - PostalCodeSourceID *string `json:"postal_code_source_id"` - ProvinceDestinationID *string `json:"province_destination_id"` - CityDestinationID *string `json:"city_destination_id"` - DistrictDestinationID *string `json:"district_destination_id"` - SubdistrictDestinationID *string `json:"subdistrict_destination_id"` - PostalCodeDestinationID *string `json:"postal_code_destination_id"` - PackageTypeID *string `json:"package_type_id"` - ModaID *string `json:"moda_id"` - LeadTime *string `json:"lead_time"` - MinimumWeight *float32 `json:"minimum_weight"` - MerchantID *string `json:"merchant_id"` - Components []BasicPricingComponentResponse `json:"components"` + ID string `json:"id"` + ProvinceSourceID *string `json:"province_source_id"` + CitySourceID *string `json:"city_source_id"` + DistrictSourceID *string `json:"district_source_id"` + SubdistrictSourceID *string `json:"subdistrict_source_id"` + PostalCodeSourceID *string `json:"postal_code_source_id"` + ProvinceDestinationID *string `json:"province_destination_id"` + CityDestinationID *string `json:"city_destination_id"` + DistrictDestinationID *string `json:"district_destination_id"` + SubdistrictDestinationID *string `json:"subdistrict_destination_id"` + PostalCodeDestinationID *string `json:"postal_code_destination_id"` + PackageTypeID *string `json:"package_type_id"` + ModaID *string `json:"moda_id"` + LeadTime *string `json:"lead_time"` + MinimumWeight *float32 `json:"minimum_weight"` + MerchantID *string `json:"merchant_id"` + Components []BasicPricingComponentResponse `json:"components"` } type BasicPricingFilterAllRequest struct { - ID string `json:"id" form:"id"` - ProvinceSourceID string `json:"province_source_id" form:"province_source_id"` - CitySourceID string `json:"city_source_id" form:"city_source_id"` - DistrictSourceID string `json:"district_source_id" form:"district_source_id"` - SubdistrictSourceID string `json:"subdistrict_source_id" form:"subdistrict_source_id"` - PostalCodeSourceID string `json:"postal_code_source_id" form:"postal_code_source_id"` - ProvinceDestinationID string `json:"province_destination_id" form:"province_destination_id"` - CityDestinationID string `json:"city_destination_id" form:"city_destination_id"` - DistrictDestinationID string `json:"district_destination_id" form:"district_destination_id"` - SubdistrictDestinationID string `json:"subdistrict_destination_id" form:"subdistrict_destination_id"` - PostalCodeDestinationID string `json:"postal_code_destination_id" form:"postal_code_destination_id"` - PackageTypeID string `json:"package_type_id" form:"package_type_id"` - ModaID string `json:"moda_id" form:"moda_id"` - MerchantID string `json:"merchant_id" form:"merchant_id"` + ID string `json:"id" form:"id"` + ProvinceSourceID string `json:"province_source_id" form:"province_source_id"` + CitySourceID string `json:"city_source_id" form:"city_source_id"` + DistrictSourceID string `json:"district_source_id" form:"district_source_id"` + SubdistrictSourceID string `json:"subdistrict_source_id" form:"subdistrict_source_id"` + PostalCodeSourceID string `json:"postal_code_source_id" form:"postal_code_source_id"` + ProvinceDestinationID string `json:"province_destination_id" form:"province_destination_id"` + CityDestinationID string `json:"city_destination_id" form:"city_destination_id"` + DistrictDestinationID string `json:"district_destination_id" form:"district_destination_id"` + SubdistrictDestinationID string `json:"subdistrict_destination_id" form:"subdistrict_destination_id"` + PostalCodeDestinationID string `json:"postal_code_destination_id" form:"postal_code_destination_id"` + PackageTypeID string `json:"package_type_id" form:"package_type_id"` + ModaID string `json:"moda_id" form:"moda_id"` + MerchantID string `json:"merchant_id" form:"merchant_id"` +} + +type BasicPricingMatchRequest struct { + ID string `json:"id" form:"id" binding:"required"` } diff --git a/internal/handlers/dto/moda.go b/internal/handlers/dto/moda.go new file mode 100644 index 0000000..f01e99a --- /dev/null +++ b/internal/handlers/dto/moda.go @@ -0,0 +1,21 @@ +package dto + +type CreateModaRequest struct { + Name string `json:"name" binding:"required,min=2"` +} + +type CreateModaResponse struct { + ID string `json:"id"` + Name *string `json:"name"` + MerchantID *string `json:"merchant_id"` +} + +type ModaFilterAllRequest struct { + ID string `json:"id" form:"id"` + Name string `json:"name" form:"name"` + MerchantID string `json:"merchant_id" form:"merchant_id"` +} + +type UpdateModaRequest struct { + Name string `json:"name" binding:"required,min=2"` +} diff --git a/internal/handlers/dto/package_type.go b/internal/handlers/dto/package_type.go new file mode 100644 index 0000000..87d37ec --- /dev/null +++ b/internal/handlers/dto/package_type.go @@ -0,0 +1,21 @@ +package dto + +type CreatePackageTypeRequest struct { + Name string `json:"name" binding:"required,min=2"` +} + +type CreatePackageTypeResponse struct { + ID string `json:"id"` + Name *string `json:"name"` + MerchantID *string `json:"merchant_id"` +} + +type PackageTypeFilterAllRequest struct { + ID string `json:"id" form:"id"` + Name string `json:"name" form:"name"` + MerchantID string `json:"merchant_id" form:"merchant_id"` +} + +type UpdatePackageTypeRequest struct { + Name string `json:"name" binding:"required,min=2"` +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 34fef78..05d1137 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,18 +1,24 @@ package handlers import ( + "cargo-erp-backend/internal/handlers/router/activity_log" "cargo-erp-backend/internal/handlers/router/auth" "cargo-erp-backend/internal/handlers/router/basic_pricing" "cargo-erp-backend/internal/handlers/router/city" "cargo-erp-backend/internal/handlers/router/cost_component" "cargo-erp-backend/internal/handlers/router/district" + "cargo-erp-backend/internal/handlers/router/moda" + "cargo-erp-backend/internal/handlers/router/package_type" "cargo-erp-backend/internal/handlers/router/postal_code" "cargo-erp-backend/internal/handlers/router/province" "cargo-erp-backend/internal/handlers/router/subdistrict" "cargo-erp-backend/internal/middleware" "cargo-erp-backend/pkg/helpers" + "cargo-erp-backend/internal/usecases" "fmt" "net/http" + "os" + "path/filepath" "time" "github.com/gin-gonic/gin" @@ -108,6 +114,13 @@ func (h *Handlers) Run() { "message": "pong", }) }) + + // serve API docs (no auth) + docsPath := filepath.Join(".", "..", "..", "docs") + if _, err := os.Stat(docsPath); err == nil { + r.Static("/docs", docsPath) + } + //auth auth.NewAuthHandler(h.Helper, r).Router() @@ -122,6 +135,8 @@ func (h *Handlers) Run() { subdistrict.NewSubdistrictHandler(h.Helper, masterdata).Router() postal_code.NewPostalCodeHandler(h.Helper, masterdata).Router() cost_component.NewCostComponentHandler(h.Helper, masterdata).Router() + package_type.NewPackageTypeHandler(h.Helper, masterdata).Router() + moda.NewModaHandler(h.Helper, masterdata).Router() } //basic-pricing @@ -129,6 +144,19 @@ func (h *Handlers) Run() { basicPricingGroup.Use(middleware.NewAuthMiddleware(h.Helper).Check()) basic_pricing.NewBasicPricingHandler(h.Helper, basicPricingGroup).Router() + //activity-logs + activityLogGroup := r.Group("activity-logs") + activityLogGroup.Use(middleware.NewAuthMiddleware(h.Helper).Check()) + activity_log.NewActivityLogHandler(h.Helper, activityLogGroup).Router() + + // load cascade rules from DB + db := h.Helper.GetDB("master") + if err := usecases.LoadCascadeRules(db); err != nil { + h.Helper.Log().Error("failed to load cascade rules", zap.Error(err)) + } else { + h.Helper.Log().Info("Cascade rules loaded from database") + } + h.Helper.Log().Info("Starting Apps") r.Run(fmt.Sprintf("%v:%v", host, port)) } diff --git a/internal/handlers/router/activity_log/activity_log.go b/internal/handlers/router/activity_log/activity_log.go new file mode 100644 index 0000000..228f4d5 --- /dev/null +++ b/internal/handlers/router/activity_log/activity_log.go @@ -0,0 +1,119 @@ +package activity_log + +import ( + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/internal/usecases" + "cargo-erp-backend/pkg/helpers" + "cargo-erp-backend/pkg/response" + "net/http" + + "github.com/gin-gonic/gin" +) + +type ActivityLogHandlerInterface interface { + Router() +} + +type ActivityLogHandler struct { + Helper helpers.HelperInterface + Group *gin.RouterGroup +} + +func NewActivityLogHandler(helper helpers.HelperInterface, g *gin.RouterGroup) ActivityLogHandlerInterface { + return &ActivityLogHandler{Helper: helper, Group: g} +} + +func (h *ActivityLogHandler) Router() { + r := h.Group + { + r.GET("/", h.GetAll) + r.POST("/list", h.GetList) + r.GET("/:id", h.GetByID) + r.POST("/:id/revert", h.Revert) + } +} + +func (h *ActivityLogHandler) GetAll(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + var req dto.ActivityLogFilterRequest + if err := c.ShouldBindQuery(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + u := usecases.NewActivityLogUsecase(h.Helper, *userid) + logs, _, err := u.GetAll(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, logs) +} + +func (h *ActivityLogHandler) GetList(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + var req dto.DataTableRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Request Format request invalid") + return + } + u := usecases.NewActivityLogUsecase(h.Helper, *userid) + res, err := u.GetList(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, res) +} + +func (h *ActivityLogHandler) GetByID(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + id := c.Param("id") + + u := usecases.NewActivityLogUsecase(h.Helper, *userid) + log, err := u.GetByID(id) + if err != nil { + response.Error(c, http.StatusNotFound, "activity log not found") + return + } + + response.Success(c, log) +} + +func (h *ActivityLogHandler) Revert(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + roleKey := "" + if userid.UsersRole_Relation != nil { + roleKey = userid.UsersRole_Relation.RoleKey + } + if roleKey != "SPM" { + response.Error(c, http.StatusForbidden, "only Super Admin can revert") + return + } + + id := c.Param("id") + u := usecases.NewActivityLogUsecase(h.Helper, *userid) + if err := u.Revert(id); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Success(c, map[string]interface{}{"message": "action reverted successfully"}) +} diff --git a/internal/handlers/router/basic_pricing/basic_pricing.go b/internal/handlers/router/basic_pricing/basic_pricing.go index f00fd8f..6fa6564 100644 --- a/internal/handlers/router/basic_pricing/basic_pricing.go +++ b/internal/handlers/router/basic_pricing/basic_pricing.go @@ -29,6 +29,7 @@ func (h *BasicPricingHandler) Router() { r.GET("/", h.GetAll) r.POST("/list", h.GetList) r.GET("/:id", h.GetByID) + r.POST("/match", h.GetMatch) r.POST("/", h.Create) r.PUT("/:id", h.Update) r.DELETE("/:id", h.Delete) @@ -160,3 +161,24 @@ func (h *BasicPricingHandler) Delete(c *gin.Context) { response.Success(c, map[string]interface{}{"message": "basic pricing deleted"}) } + +func (h *BasicPricingHandler) GetMatch(c *gin.Context) { + var req dto.BasicPricingMatchRequest + if err := c.BindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + u := usecases.NewBasicPricingUsecase(h.Helper, *userid) + pricing := u.GetMatch(req.ID) + if pricing == nil { + response.Error(c, http.StatusNotFound, "basic pricing not found") + return + } + response.Success(c, pricing) +} diff --git a/internal/handlers/router/moda/moda.go b/internal/handlers/router/moda/moda.go new file mode 100644 index 0000000..e6f20c0 --- /dev/null +++ b/internal/handlers/router/moda/moda.go @@ -0,0 +1,172 @@ +package moda + +import ( + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/internal/usecases" + "cargo-erp-backend/pkg/helpers" + "cargo-erp-backend/pkg/response" + "net/http" + + "github.com/gin-gonic/gin" +) + +type ModaHandlerInterface interface { + Router() +} + +type ModaHandler struct { + Helper helpers.HelperInterface + Group *gin.RouterGroup +} + +func NewModaHandler(helper helpers.HelperInterface, g *gin.RouterGroup) ModaHandlerInterface { + return &ModaHandler{Helper: helper, Group: g} +} + +func (h *ModaHandler) Router() { + r := h.Group.Group("modas") + { + r.GET("/", h.GetAll) + r.POST("/list", h.GetList) + r.GET("/:id", h.GetByID) + r.POST("/", h.Create) + r.PUT("/:id", h.Update) + r.DELETE("/:id", h.Delete) + } +} + +func (h *ModaHandler) GetAll(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + var req dto.ModaFilterAllRequest + if err := c.ShouldBindQuery(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + u := usecases.NewModaUsecase(h.Helper, *userid) + items, _, err := u.GetAll(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO = make([]dto.CreateModaResponse, 0) + for _, v := range items { + var DTO dto.CreateModaResponse + h.Helper.MapStructToStruct(v, "json", &DTO, "json") + responseDTO = append(responseDTO, DTO) + } + response.Success(c, responseDTO) +} + +func (h *ModaHandler) GetList(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + var req dto.DataTableRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Request Format request invalid") + return + } + u := usecases.NewModaUsecase(h.Helper, *userid) + res, err := u.GetList(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, res) + return +} + +func (h *ModaHandler) GetByID(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + id := c.Param("id") + + u := usecases.NewModaUsecase(h.Helper, *userid) + item, err := u.GetByID(id) + if err != nil { + response.Error(c, http.StatusNotFound, "moda not found") + return + } + + response.Success(c, item) +} + +func (h *ModaHandler) Create(c *gin.Context) { + var req dto.CreateModaRequest + if err := c.BindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + var merchantid *string = nil + if userid.UsersRole_Relation == nil || userid.UsersRole_Relation.RoleKey != "SPM" { + merchantid = userid.MerchantID + } + createdBy := userid.ID + + u := usecases.NewModaUsecase(h.Helper, *userid) + item, err := u.Create(req.Name, createdBy, merchantid) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO dto.CreateModaResponse + h.Helper.MapStructToStruct(item, "json", &responseDTO, "json") + response.Created(c, responseDTO) +} + +func (h *ModaHandler) Update(c *gin.Context) { + id := c.Param("id") + + var req dto.UpdateModaRequest + if err := c.BindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + updatedBy := userid.ID + + u := usecases.NewModaUsecase(h.Helper, *userid) + item, err := u.Update(id, req.Name, updatedBy) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO dto.CreateModaResponse + h.Helper.MapStructToStruct(item, "json", &responseDTO, "json") + response.Success(c, responseDTO) +} + +func (h *ModaHandler) Delete(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + id := c.Param("id") + u := usecases.NewModaUsecase(h.Helper, *userid) + if err := u.Delete(id); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Success(c, map[string]interface{}{"message": "moda deleted"}) +} diff --git a/internal/handlers/router/package_type/package_type.go b/internal/handlers/router/package_type/package_type.go new file mode 100644 index 0000000..40488ce --- /dev/null +++ b/internal/handlers/router/package_type/package_type.go @@ -0,0 +1,172 @@ +package package_type + +import ( + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/internal/usecases" + "cargo-erp-backend/pkg/helpers" + "cargo-erp-backend/pkg/response" + "net/http" + + "github.com/gin-gonic/gin" +) + +type PackageTypeHandlerInterface interface { + Router() +} + +type PackageTypeHandler struct { + Helper helpers.HelperInterface + Group *gin.RouterGroup +} + +func NewPackageTypeHandler(helper helpers.HelperInterface, g *gin.RouterGroup) PackageTypeHandlerInterface { + return &PackageTypeHandler{Helper: helper, Group: g} +} + +func (h *PackageTypeHandler) Router() { + r := h.Group.Group("package-types") + { + r.GET("/", h.GetAll) + r.POST("/list", h.GetList) + r.GET("/:id", h.GetByID) + r.POST("/", h.Create) + r.PUT("/:id", h.Update) + r.DELETE("/:id", h.Delete) + } +} + +func (h *PackageTypeHandler) GetAll(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + var req dto.PackageTypeFilterAllRequest + if err := c.ShouldBindQuery(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + items, _, err := u.GetAll(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO = make([]dto.CreatePackageTypeResponse, 0) + for _, v := range items { + var DTO dto.CreatePackageTypeResponse + h.Helper.MapStructToStruct(v, "json", &DTO, "json") + responseDTO = append(responseDTO, DTO) + } + response.Success(c, responseDTO) +} + +func (h *PackageTypeHandler) GetList(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + + var req dto.DataTableRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Request Format request invalid") + return + } + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + res, err := u.GetList(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, res) + return +} + +func (h *PackageTypeHandler) GetByID(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + id := c.Param("id") + + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + item, err := u.GetByID(id) + if err != nil { + response.Error(c, http.StatusNotFound, "package type not found") + return + } + + response.Success(c, item) +} + +func (h *PackageTypeHandler) Create(c *gin.Context) { + var req dto.CreatePackageTypeRequest + if err := c.BindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + var merchantid *string = nil + if userid.UsersRole_Relation == nil || userid.UsersRole_Relation.RoleKey != "SPM" { + merchantid = userid.MerchantID + } + createdBy := userid.ID + + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + item, err := u.Create(req.Name, createdBy, merchantid) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO dto.CreatePackageTypeResponse + h.Helper.MapStructToStruct(item, "json", &responseDTO, "json") + response.Created(c, responseDTO) +} + +func (h *PackageTypeHandler) Update(c *gin.Context) { + id := c.Param("id") + + var req dto.UpdatePackageTypeRequest + if err := c.BindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + updatedBy := userid.ID + + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + item, err := u.Update(id, req.Name, updatedBy) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + var responseDTO dto.CreatePackageTypeResponse + h.Helper.MapStructToStruct(item, "json", &responseDTO, "json") + response.Success(c, responseDTO) +} + +func (h *PackageTypeHandler) Delete(c *gin.Context) { + userid, err := h.Helper.GetAuthInfo(c.GetString("auth")) + if err != nil { + response.Error(c, http.StatusUnauthorized, err.Error()) + return + } + id := c.Param("id") + u := usecases.NewPackageTypeUsecase(h.Helper, *userid) + if err := u.Delete(id); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + + response.Success(c, map[string]interface{}{"message": "package type deleted"}) +} diff --git a/internal/importers/postal_code/importer.go b/internal/importers/postal_code/importer.go index 88999d3..8fb5595 100644 --- a/internal/importers/postal_code/importer.go +++ b/internal/importers/postal_code/importer.go @@ -97,6 +97,7 @@ func (h *PostalCodeImportHandler) ValidateRow(row []string, rowNumber int) (impo h.lastSubdistrictName = subdistrictName now := time.Now() + nowStr := now.Format("2006-01-02 15:04:05.999999-07:00") pc := &domain.PostalCode{ PostalCode: &postalCode, ProvinceID: &province.ID, @@ -107,7 +108,7 @@ func (h *PostalCodeImportHandler) ValidateRow(row []string, rowNumber int) (impo CreatedBy: &h.UserData.ID, CreatedOn: &now, UpdatedBy: &h.UserData.ID, - UpdatedOn: &now, + UpdatedOn: &nowStr, } return pc, nil diff --git a/internal/usecases/activity_log.go b/internal/usecases/activity_log.go new file mode 100644 index 0000000..475addf --- /dev/null +++ b/internal/usecases/activity_log.go @@ -0,0 +1,500 @@ +package usecases + +import ( + "cargo-erp-backend/internal/domain" + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/pkg/datatable" + "cargo-erp-backend/pkg/helpers" + "encoding/json" + "fmt" + "strings" + "sync" + "time" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type CascadeRule struct { + Table string + FKColumn string + DeleteRule string +} + +type CascadeGroup struct { + Table string `json:"table"` + FKColumn string `json:"fk_column"` + DeleteRule string `json:"delete_rule"` + Records []map[string]interface{} `json:"records"` +} + +var ( + CascadeMap map[string][]CascadeRule + CascadeMapMu sync.RWMutex +) + +type ActivityLogUsecaseInterface interface { + GetAll(filter dto.ActivityLogFilterRequest) ([]dto.ActivityLogResponse, int64, error) + GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) + GetByID(id string) (dto.ActivityLogResponse, error) + Revert(logID string) error + LogCreate(tableName string, record interface{}, merchantID *string) + LogUpdate(tableName string, oldData, newData interface{}, merchantID *string) + LogDelete(tableName string, recordID string, oldData interface{}, merchantID *string) +} + +type ActivityLogUsecase struct { + Helper helpers.HelperInterface + AuthUser domain.User +} + +func NewActivityLogUsecase(helper helpers.HelperInterface, authUser domain.User) ActivityLogUsecaseInterface { + return &ActivityLogUsecase{Helper: helper, AuthUser: authUser} +} + +func LoadCascadeRules(db *gorm.DB) error { + rows, err := db.Raw(` + SELECT + tc.table_name AS source_table, + kcu.column_name AS source_column, + ccu.table_name AS target_table, + ccu.column_name AS target_column, + rc.delete_rule + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = ccu.table_schema + JOIN information_schema.referential_constraints rc + ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'public' + ORDER BY tc.table_name + `).Rows() + if err != nil { + return fmt.Errorf("failed to query FK constraints: %w", err) + } + defer rows.Close() + + newMap := make(map[string][]CascadeRule) + for rows.Next() { + var fk ForeignKeyInfo + if err := rows.Scan(&fk.SourceTable, &fk.SourceColumn, &fk.TargetTable, &fk.TargetColumn, &fk.DeleteRule); err != nil { + continue + } + if _, ok := newMap[fk.TargetTable]; !ok { + newMap[fk.TargetTable] = make([]CascadeRule, 0) + } + newMap[fk.TargetTable] = append(newMap[fk.TargetTable], CascadeRule{ + Table: fk.SourceTable, + FKColumn: fk.SourceColumn, + DeleteRule: fk.DeleteRule, + }) + } + + CascadeMapMu.Lock() + CascadeMap = newMap + CascadeMapMu.Unlock() + return nil +} + +type ForeignKeyInfo struct { + SourceTable string + SourceColumn string + TargetTable string + TargetColumn string + DeleteRule string +} + +func CaptureCascade(tx *gorm.DB, tableName string, recordID string) ([]CascadeGroup, error) { + groups := make([]CascadeGroup, 0) + visited := make(map[string]bool) + + CascadeMapMu.RLock() + defer CascadeMapMu.RUnlock() + + var walk func(table string, ids []string) + walk = func(table string, ids []string) { + if len(ids) == 0 { + return + } + rules, ok := CascadeMap[table] + if !ok { + return + } + for _, rule := range rules { + key := rule.Table + ":" + strings.Join(ids, ",") + if visited[key] { + continue + } + visited[key] = true + + var records []map[string]interface{} + if err := tx.Table(rule.Table).Where(rule.FKColumn+" IN ?", ids).Find(&records).Error; err != nil { + continue + } + if len(records) > 0 { + groups = append(groups, CascadeGroup{ + Table: rule.Table, + FKColumn: rule.FKColumn, + DeleteRule: rule.DeleteRule, + Records: records, + }) + childIDs := extractIDs(records, "id") + walk(rule.Table, childIDs) + } + } + } + + walk(tableName, []string{recordID}) + return groups, nil +} + +func extractIDs(records []map[string]interface{}, key string) []string { + ids := make([]string, 0, len(records)) + for _, r := range records { + if v, ok := r[key]; ok && v != nil { + switch id := v.(type) { + case string: + ids = append(ids, id) + case float64: + ids = append(ids, fmt.Sprintf("%.0f", id)) + } + } + } + return ids +} + +func toJSON(v interface{}) *string { + if v == nil { + return nil + } + b, err := json.Marshal(v) + if err != nil { + return nil + } + s := string(b) + return &s +} + +func (u *ActivityLogUsecase) LogCreate(tableName string, record interface{}, merchantID *string) { + db := u.Helper.GetDB("master") + log := domain.ActivityLog{ + TargetTable: tableName, + RecordID: extractRecordID(record), + Action: "CREATE", + NewData: toJSON(record), + UserID: u.AuthUser.ID, + UserName: u.getDisplayName(), + MerchantID: merchantID, + CreatedOn: timePtr(time.Now()), + } + db.Create(&log) +} + +func (u *ActivityLogUsecase) LogUpdate(tableName string, oldData, newData interface{}, merchantID *string) { + db := u.Helper.GetDB("master") + log := domain.ActivityLog{ + TargetTable: tableName, + RecordID: extractRecordID(oldData), + Action: "UPDATE", + OldData: toJSON(oldData), + NewData: toJSON(newData), + UserID: u.AuthUser.ID, + UserName: u.getDisplayName(), + MerchantID: merchantID, + CreatedOn: timePtr(time.Now()), + } + db.Create(&log) +} + +func (u *ActivityLogUsecase) LogDelete(tableName string, recordID string, oldData interface{}, merchantID *string) { + db := u.Helper.GetDB("master") + + var cascadeData []CascadeGroup + err := db.Transaction(func(tx *gorm.DB) error { + var err error + cascadeData, err = CaptureCascade(tx, tableName, recordID) + return err + }) + if err != nil { + u.Helper.Log().Error("failed to capture cascade", zap.Error(err)) + cascadeData = []CascadeGroup{} + } + + log := domain.ActivityLog{ + TargetTable: tableName, + RecordID: recordID, + Action: "DELETE", + OldData: toJSON(oldData), + CascadeData: toJSON(cascadeData), + UserID: u.AuthUser.ID, + UserName: u.getDisplayName(), + MerchantID: merchantID, + CreatedOn: timePtr(time.Now()), + } + db.Create(&log) +} + +func (u *ActivityLogUsecase) GetAll(filter dto.ActivityLogFilterRequest) ([]dto.ActivityLogResponse, int64, error) { + var logs []domain.ActivityLog + var total int64 + + db := u.Helper.GetDB("slave") + tx := db.Model(&logs) + + if filter.TableName != "" { + tx = tx.Where("table_name = ?", filter.TableName) + } + if filter.RecordID != "" { + tx = tx.Where("record_id = ?", filter.RecordID) + } + if filter.Action != "" { + tx = tx.Where("action = ?", filter.Action) + } + if filter.UserID != "" { + tx = tx.Where("user_id = ?", filter.UserID) + } + if filter.MerchantID != "" { + tx = tx.Where("merchant_id = ?", filter.MerchantID) + } + if filter.DateFrom != "" { + tx = tx.Where("created_on >= ?", filter.DateFrom) + } + if filter.DateTo != "" { + tx = tx.Where("created_on <= ?", filter.DateTo) + } + + tx.Count(&total) + tx.Order("created_on DESC") + if err := tx.Find(&logs).Error; err != nil { + return nil, 0, err + } + + responses := make([]dto.ActivityLogResponse, 0, len(logs)) + for _, l := range logs { + responses = append(responses, toActivityLogResponse(l)) + } + return responses, total, nil +} + +func (u *ActivityLogUsecase) GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) { + db := u.Helper.GetDB("slave") + tx := db.Table("activity_log") + dt := datatable.NewDatatable(tx, req) + coldef := make([]dto.DataTableColDef, 0) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.table_name", Alias: "table_name"}) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.record_id", Alias: "record_id"}) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.action", Alias: "action"}) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.user_id", Alias: "user_id"}) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.user_name", Alias: "user_name"}) + coldef = append(coldef, dto.DataTableColDef{Field: "activity_log.id", Alias: "id"}) + return dt.Render(coldef), nil +} + +func (u *ActivityLogUsecase) GetByID(id string) (dto.ActivityLogResponse, error) { + var log domain.ActivityLog + db := u.Helper.GetDB("slave") + if err := db.Where("id = ?", id).First(&log).Error; err != nil { + return dto.ActivityLogResponse{}, err + } + return toActivityLogResponse(log), nil +} + +func (u *ActivityLogUsecase) Revert(logID string) error { + db := u.Helper.GetDB("master") + + var log domain.ActivityLog + if err := db.Where("id = ?", logID).First(&log).Error; err != nil { + return fmt.Errorf("activity log not found") + } + + switch log.Action { + case "CREATE": + return u.revertCreate(db, log) + case "UPDATE": + return u.revertUpdate(db, log) + case "DELETE": + return u.revertDelete(db, log) + default: + return fmt.Errorf("unsupported action: %s", log.Action) + } +} + +func (u *ActivityLogUsecase) revertCreate(db *gorm.DB, log domain.ActivityLog) error { + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Exec("DELETE FROM "+log.TargetTable+" WHERE id = ?", log.RecordID).Error; err != nil { + return err + } + u.logRevert("CREATE", log) + return nil + }) +} + +func (u *ActivityLogUsecase) revertUpdate(db *gorm.DB, log domain.ActivityLog) error { + return db.Transaction(func(tx *gorm.DB) error { + if log.OldData == nil { + return fmt.Errorf("no old data to revert") + } + + var oldRecord map[string]interface{} + if err := json.Unmarshal([]byte(*log.OldData), &oldRecord); err != nil { + return fmt.Errorf("failed to parse old data: %w", err) + } + + cleaned := cleanRecordMap(oldRecord) + delete(cleaned, "created_on") + delete(cleaned, "created_by") + + if err := tx.Table(log.TargetTable).Where("id = ?", log.RecordID).Updates(cleaned).Error; err != nil { + return err + } + + u.logRevert("UPDATE", log) + return nil + }) +} + +func (u *ActivityLogUsecase) revertDelete(db *gorm.DB, log domain.ActivityLog) error { + return db.Transaction(func(tx *gorm.DB) error { + var cascadeGroups []CascadeGroup + if log.CascadeData != nil { + if err := json.Unmarshal([]byte(*log.CascadeData), &cascadeGroups); err != nil { + return fmt.Errorf("failed to parse cascade data: %w", err) + } + } + + for i := len(cascadeGroups) - 1; i >= 0; i-- { + group := cascadeGroups[i] + for _, record := range group.Records { + cleaned := cleanRecordMap(record) + tx.Table(group.Table).Create(cleaned) + } + } + + if log.OldData != nil { + var mainRecord map[string]interface{} + if err := json.Unmarshal([]byte(*log.OldData), &mainRecord); err == nil { + cleaned := cleanRecordMap(mainRecord) + tx.Table(log.TargetTable).Create(cleaned) + } + } + + u.logRevert("DELETE", log) + return nil + }) +} + +func (u *ActivityLogUsecase) logRevert(originalAction string, originalLog domain.ActivityLog) { + db := u.Helper.GetDB("master") + revertLog := domain.ActivityLog{ + TargetTable: originalLog.TargetTable, + RecordID: originalLog.RecordID, + Action: "REVERT", + OldData: toJSON(fmt.Sprintf("Reverted %s action (log_id: %s)", originalAction, originalLog.ID)), + UserID: u.AuthUser.ID, + UserName: u.getDisplayName(), + MerchantID: originalLog.MerchantID, + CreatedOn: timePtr(time.Now()), + } + db.Create(&revertLog) +} + +func (u *ActivityLogUsecase) getDisplayName() string { + if u.AuthUser.NickName != nil && *u.AuthUser.NickName != "" { + return *u.AuthUser.NickName + } + if u.AuthUser.Email != nil { + return *u.AuthUser.Email + } + return u.AuthUser.ID +} + +func extractRecordID(record interface{}) string { + switch v := record.(type) { + case map[string]interface{}: + if id, ok := v["id"].(string); ok { + return id + } + case domain.Province: + return v.ID + case domain.City: + return v.ID + case domain.District: + return v.ID + case domain.Subdistrict: + return v.ID + case domain.PostalCode: + return v.ID + case domain.CostComponent: + return v.ID + case domain.PackageType: + return v.ID + case domain.Moda: + return v.ID + case domain.BasicPricing: + return v.ID + } + + b, err := json.Marshal(record) + if err != nil { + return "" + } + var m map[string]interface{} + if err := json.Unmarshal(b, &m); err != nil { + return "" + } + if id, ok := m["id"].(string); ok { + return id + } + return "" +} + +func timePtr(t time.Time) *time.Time { + return &t +} + +func toActivityLogResponse(log domain.ActivityLog) dto.ActivityLogResponse { + resp := dto.ActivityLogResponse{ + ID: log.ID, + TableName: log.TargetTable, + RecordID: log.RecordID, + Action: log.Action, + OldData: log.OldData, + NewData: log.NewData, + CascadeData: log.CascadeData, + UserID: log.UserID, + UserName: log.UserName, + MerchantID: log.MerchantID, + } + if log.CreatedOn != nil { + s := log.CreatedOn.Format("2006-01-02 15:04:05") + resp.CreatedOn = &s + } + if log.CascadeData != nil { + var groups []CascadeGroup + if err := json.Unmarshal([]byte(*log.CascadeData), &groups); err == nil { + count := 0 + for _, g := range groups { + count += len(g.Records) + } + resp.CascadeCount = count + } + } + return resp +} + +func cleanRecordMap(record map[string]interface{}) map[string]interface{} { + cleaned := make(map[string]interface{}) + for k, v := range record { + if v == nil { + continue + } + switch v.(type) { + case map[string]interface{}, []interface{}: + continue + } + cleaned[k] = v + } + return cleaned +} diff --git a/internal/usecases/auth.go b/internal/usecases/auth.go index b1be26b..07334b8 100644 --- a/internal/usecases/auth.go +++ b/internal/usecases/auth.go @@ -44,17 +44,20 @@ func (u *AuthUsecase) Login(email string, password string) (token string, err er if err != nil { return "", err } - // key := fmt.Sprintf("session_login:%v:*", v.ID) iter := u.Helper.GetRedis("master").Scan(ctx, 0, key, 1000).Iterator() for iter.Next(ctx) { keys := iter.Val() keys_ex := strings.Split(keys, ":") - //change to del if using single login if keys_ex[1] == v.ID { - u.Helper.GetRedis("master").Expire(ctx, keys, time.Duration(t)*time.Second) - return keys_ex[2], nil + cachedToken := keys_ex[2] + _, verifyErr := secretJwt.VerifyToken(cachedToken) + if verifyErr == nil { + u.Helper.GetRedis("master").Expire(ctx, keys, time.Duration(t)*time.Second) + return cachedToken, nil + } + u.Helper.GetRedis("master").Del(ctx, keys) } } diff --git a/internal/usecases/basic_pricing.go b/internal/usecases/basic_pricing.go index 2f5b118..877910d 100644 --- a/internal/usecases/basic_pricing.go +++ b/internal/usecases/basic_pricing.go @@ -19,6 +19,7 @@ type BasicPricingUsecaseInterface interface { Create(req dto.CreateBasicPricingRequest, createdBy string, merchantid *string) (dto.CreateBasicPricingResponse, error) Update(id string, req dto.CreateBasicPricingRequest, updatedBy string) (dto.CreateBasicPricingResponse, error) Delete(id string) error + GetMatch(id string) *dto.CreateBasicPricingResponse } type BasicPricingUsecase struct { @@ -77,7 +78,29 @@ func (u *BasicPricingUsecase) toResponse(pricing domain.BasicPricing, components } return resp } +func (u *BasicPricingUsecase) GetMatch(id string) *dto.CreateBasicPricingResponse { + var pricings domain.BasicPricing + db := u.Helper.GetDB("slave") + tx := db.Model(&pricings).Where("basic_pricing.id = ?", id) + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + tx = tx.Where("basic_pricing.merchant_id = ?", u.AuthUser.MerchantID) + } + + if err := tx.First(&pricings).Error; err != nil { + return nil + } + + pricingIDs := make([]string, 0) + pricingIDs = append(pricingIDs, pricings.ID) + componentsMap, err := u.loadComponents(pricingIDs) + if err != nil { + return nil + } + + responses := u.toResponse(pricings, componentsMap[pricings.ID]) + return &responses +} func (u *BasicPricingUsecase) GetAll(filter dto.BasicPricingFilterAllRequest) ([]dto.CreateBasicPricingResponse, int64, error) { var pricings []domain.BasicPricing var total int64 @@ -85,7 +108,7 @@ func (u *BasicPricingUsecase) GetAll(filter dto.BasicPricingFilterAllRequest) ([ db := u.Helper.GetDB("slave") tx := db.Model(&pricings) if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - tx = tx.Where(db.Where("merchant_id IS NULL").Or("merchant_id=?", u.AuthUser.MerchantID)) + tx = tx.Where("merchant_id=?", u.AuthUser.MerchantID) } v := reflect.ValueOf(filter) t := reflect.TypeOf(filter) @@ -129,7 +152,7 @@ func (u *BasicPricingUsecase) GetList(req dto.DataTableRequest) (dto.DataTableRe Joins("LEFT JOIN city AS cd ON basic_pricing.city_destination_id=cd.id"). Joins("LEFT JOIN merchant ON basic_pricing.merchant_id=merchant.id") if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - tx = tx.Where("basic_pricing.merchant_id IS NULL OR basic_pricing.merchant_id=?", u.AuthUser.MerchantID) + tx = tx.Where("basic_pricing.merchant_id=?", u.AuthUser.MerchantID) } dt := datatable.NewDatatable(tx, req) coldef := make([]dto.DataTableColDef, 0) @@ -209,12 +232,15 @@ func (u *BasicPricingUsecase) Create(req dto.CreateBasicPricingRequest, createdB if err != nil { return dto.CreateBasicPricingResponse{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("basic_pricing", pricing, merchantid) return u.toResponse(pricing, savedComponents), nil } func (u *BasicPricingUsecase) Update(id string, req dto.CreateBasicPricingRequest, updatedBy string) (dto.CreateBasicPricingResponse, error) { db := u.Helper.GetDB("master") var pricing domain.BasicPricing + var oldPricing domain.BasicPricing var savedComponents []domain.BasicPricingComponent err := db.Transaction(func(tx *gorm.DB) error { @@ -225,6 +251,7 @@ func (u *BasicPricingUsecase) Update(id string, req dto.CreateBasicPricingReques if err := q.Where("id = ?", id).First(&pricing).Error; err != nil { return err } + oldPricing = pricing now := time.Now() pricing.ProvinceSourceID = strPtr(req.ProvinceSourceID) pricing.CitySourceID = strPtr(req.CitySourceID) @@ -267,6 +294,12 @@ func (u *BasicPricingUsecase) Update(id string, req dto.CreateBasicPricingReques if err != nil { return dto.CreateBasicPricingResponse{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("basic_pricing", oldPricing, pricing, merchantID) return u.toResponse(pricing, savedComponents), nil } @@ -281,6 +314,12 @@ func (u *BasicPricingUsecase) Delete(id string) error { if err := q.Where("id = ?", id).First(&pricing).Error; err != nil { return err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("basic_pricing", id, pricing, merchantID) if err := tx.Where("basic_pricing_id = ?", id).Delete(&domain.BasicPricingComponent{}).Error; err != nil { return err } diff --git a/internal/usecases/city.go b/internal/usecases/city.go index 3a6c7c1..3f1ddf9 100644 --- a/internal/usecases/city.go +++ b/internal/usecases/city.go @@ -125,11 +125,14 @@ func (u *CityUsecase) Create(name string, provinceID string, port string, create if err != nil { return domain.City{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("city", city, merchantid) return city, nil } func (u *CityUsecase) Update(id string, name string, provinceID string, port string, updatedBy string) (domain.City, error) { var city domain.City + var oldCity domain.City db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -139,6 +142,7 @@ func (u *CityUsecase) Update(id string, name string, provinceID string, port str if err := q.Where("id = ?", id).First(&city).Error; err != nil { return err } + oldCity = city now := time.Now() city.Name = &name city.ProvinceID = &provinceID @@ -150,14 +154,31 @@ func (u *CityUsecase) Update(id string, name string, provinceID string, port str if err != nil { return domain.City{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("city", oldCity, city, merchantID) return city, nil } func (u *CityUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + var city domain.City + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&city).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("city", id, city, merchantID) if err := db.Delete(&domain.City{}, "id = ?", id).Error; err != nil { return err } diff --git a/internal/usecases/cost_component.go b/internal/usecases/cost_component.go index 2e27261..7e98040 100644 --- a/internal/usecases/cost_component.go +++ b/internal/usecases/cost_component.go @@ -148,11 +148,14 @@ func (u *CostComponentUsecase) Create(name string, code string, value string, co if err != nil { return domain.CostComponent{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("cost_component", component, merchantid) return component, nil } func (u *CostComponentUsecase) Update(id string, name string, code string, value string, componentType int32, unitType int32, updatedBy string) (domain.CostComponent, error) { var component domain.CostComponent + var oldComponent domain.CostComponent db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -162,6 +165,7 @@ func (u *CostComponentUsecase) Update(id string, name string, code string, value if err := q.Where("id = ?", id).First(&component).Error; err != nil { return err } + oldComponent = component now := time.Now() component.Name = &name component.Code = &code @@ -175,14 +179,31 @@ func (u *CostComponentUsecase) Update(id string, name string, code string, value if err != nil { return domain.CostComponent{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("cost_component", oldComponent, component, merchantID) return component, nil } func (u *CostComponentUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + var component domain.CostComponent + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&component).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("cost_component", id, component, merchantID) if err := db.Delete(&domain.CostComponent{}, "id = ?", id).Error; err != nil { return err } diff --git a/internal/usecases/district.go b/internal/usecases/district.go index 66f1006..4fab222 100644 --- a/internal/usecases/district.go +++ b/internal/usecases/district.go @@ -134,11 +134,14 @@ func (u *DistrictUsecase) Create(name string, provinceID string, cityID string, if err != nil { return domain.District{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("district", district, merchantid) return district, nil } func (u *DistrictUsecase) Update(id string, name string, provinceID string, cityID string, updatedBy string) (domain.District, error) { var district domain.District + var oldDistrict domain.District db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -148,6 +151,7 @@ func (u *DistrictUsecase) Update(id string, name string, provinceID string, city if err := q.Where("id = ?", id).First(&district).Error; err != nil { return err } + oldDistrict = district now := time.Now() district.Name = &name district.ProvinceID = &provinceID @@ -159,14 +163,31 @@ func (u *DistrictUsecase) Update(id string, name string, provinceID string, city if err != nil { return domain.District{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("district", oldDistrict, district, merchantID) return district, nil } func (u *DistrictUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + var district domain.District + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&district).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("district", id, district, merchantID) if err := db.Delete(&domain.District{}, "id = ?", id).Error; err != nil { return err } diff --git a/internal/usecases/moda.go b/internal/usecases/moda.go new file mode 100644 index 0000000..fe5d69b --- /dev/null +++ b/internal/usecases/moda.go @@ -0,0 +1,175 @@ +package usecases + +import ( + "cargo-erp-backend/internal/domain" + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/pkg/datatable" + "cargo-erp-backend/pkg/helpers" + "fmt" + "reflect" + "time" + + "gorm.io/gorm" +) + +type ModaUsecaseInterface interface { + GetAll(filter dto.ModaFilterAllRequest) ([]domain.Moda, int64, error) + GetList(dto.DataTableRequest) (dto.DataTableResponse, error) + GetByID(id string) (domain.Moda, error) + Create(name string, createdBy string, merchantid *string) (domain.Moda, error) + Update(id string, name string, updatedBy string) (domain.Moda, error) + Delete(id string) error +} + +type ModaUsecase struct { + Helper helpers.HelperInterface + AuthUser domain.User +} + +func NewModaUsecase(helper helpers.HelperInterface, authUser domain.User) ModaUsecaseInterface { + return &ModaUsecase{Helper: helper, AuthUser: authUser} +} + +func (u *ModaUsecase) GetAll(filter dto.ModaFilterAllRequest) ([]domain.Moda, int64, error) { + var items []domain.Moda + var total int64 + + db := u.Helper.GetDB("slave") + tx := db.Model(&items) + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + tx = tx.Where(db.Where("merchant_id IS NULL").Or("merchant_id=?", u.AuthUser.MerchantID)) + } + v := reflect.ValueOf(filter) + t := reflect.TypeOf(filter) + + for i := 0; i < v.NumField(); i++ { + if v.Field(i).String() != "" { + tagKey := t.Field(i).Tag.Get("form") + tx = tx.Where(fmt.Sprintf("%v=?", tagKey), v.Field(i)) + } + } + tx.Model(&domain.Moda{}).Count(&total) + tx.Order("name ASC") + if err := tx.Find(&items).Error; err != nil { + return nil, 0, err + } + return items, total, nil +} + +func (u *ModaUsecase) GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) { + var response dto.DataTableResponse + db := u.Helper.GetDB("slave") + tx := db.Table("moda"). + Joins("LEFT JOIN merchant ON moda.merchant_id=merchant.id") + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + tx = tx.Where("moda.merchant_id IS NULL OR moda.merchant_id=?", u.AuthUser.MerchantID) + } + dt := datatable.NewDatatable(tx, req) + coldef := make([]dto.DataTableColDef, 0) + coldef = append(coldef, dto.DataTableColDef{ + Field: "moda.name", + Alias: "name", + }) + coldef = append(coldef, dto.DataTableColDef{ + Field: "merchant.id", + Alias: "merchant_id", + }) + coldef = append(coldef, dto.DataTableColDef{ + Field: "moda.id", + Alias: "id", + }) + + response = dt.Render(coldef) + return response, nil +} + +func (u *ModaUsecase) GetByID(id string) (domain.Moda, error) { + var item domain.Moda + db := u.Helper.GetDB("slave") + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + db = db.Where("merchant_id IS NULL OR merchant_id=?", u.AuthUser.MerchantID) + } + if err := db.Where("id = ?", id).First(&item).Error; err != nil { + return domain.Moda{}, err + } + return item, nil +} + +func (u *ModaUsecase) Create(name string, createdBy string, merchantid *string) (domain.Moda, error) { + now := time.Now() + + db := u.Helper.GetDB("master") + + var item domain.Moda + err := db.Transaction(func(tx *gorm.DB) error { + item = domain.Moda{ + Name: &name, + MerchantID: merchantid, + CreatedBy: &createdBy, + CreatedOn: &now, + UpdatedBy: &createdBy, + UpdatedOn: &now, + } + + return tx.Create(&item).Error + }) + if err != nil { + return domain.Moda{}, err + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("moda", item, merchantid) + return item, nil +} + +func (u *ModaUsecase) Update(id string, name string, updatedBy string) (domain.Moda, error) { + var item domain.Moda + var oldItem domain.Moda + db := u.Helper.GetDB("master") + err := db.Transaction(func(tx *gorm.DB) error { + q := tx + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + q = tx.Where("merchant_id=?", u.AuthUser.MerchantID) + } + if err := q.Where("id = ?", id).First(&item).Error; err != nil { + return err + } + oldItem = item + now := time.Now() + item.Name = &name + item.UpdatedBy = &updatedBy + item.UpdatedOn = &now + return tx.Save(&item).Error + }) + if err != nil { + return domain.Moda{}, err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("moda", oldItem, item, merchantID) + return item, nil +} + +func (u *ModaUsecase) Delete(id string) error { + db := u.Helper.GetDB("master") + var item domain.Moda + q := db + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) + } + if err := q.Where("id = ?", id).First(&item).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("moda", id, item, merchantID) + if err := db.Delete(&domain.Moda{}, "id = ?", id).Error; err != nil { + return err + } + return nil +} diff --git a/internal/usecases/package_type.go b/internal/usecases/package_type.go new file mode 100644 index 0000000..f03df40 --- /dev/null +++ b/internal/usecases/package_type.go @@ -0,0 +1,175 @@ +package usecases + +import ( + "cargo-erp-backend/internal/domain" + "cargo-erp-backend/internal/handlers/dto" + "cargo-erp-backend/pkg/datatable" + "cargo-erp-backend/pkg/helpers" + "fmt" + "reflect" + "time" + + "gorm.io/gorm" +) + +type PackageTypeUsecaseInterface interface { + GetAll(filter dto.PackageTypeFilterAllRequest) ([]domain.PackageType, int64, error) + GetList(dto.DataTableRequest) (dto.DataTableResponse, error) + GetByID(id string) (domain.PackageType, error) + Create(name string, createdBy string, merchantid *string) (domain.PackageType, error) + Update(id string, name string, updatedBy string) (domain.PackageType, error) + Delete(id string) error +} + +type PackageTypeUsecase struct { + Helper helpers.HelperInterface + AuthUser domain.User +} + +func NewPackageTypeUsecase(helper helpers.HelperInterface, authUser domain.User) PackageTypeUsecaseInterface { + return &PackageTypeUsecase{Helper: helper, AuthUser: authUser} +} + +func (u *PackageTypeUsecase) GetAll(filter dto.PackageTypeFilterAllRequest) ([]domain.PackageType, int64, error) { + var items []domain.PackageType + var total int64 + + db := u.Helper.GetDB("slave") + tx := db.Model(&items) + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + tx = tx.Where(db.Where("merchant_id IS NULL").Or("merchant_id=?", u.AuthUser.MerchantID)) + } + v := reflect.ValueOf(filter) + t := reflect.TypeOf(filter) + + for i := 0; i < v.NumField(); i++ { + if v.Field(i).String() != "" { + tagKey := t.Field(i).Tag.Get("form") + tx = tx.Where(fmt.Sprintf("%v=?", tagKey), v.Field(i)) + } + } + tx.Model(&domain.PackageType{}).Count(&total) + tx.Order("name ASC") + if err := tx.Find(&items).Error; err != nil { + return nil, 0, err + } + return items, total, nil +} + +func (u *PackageTypeUsecase) GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) { + var response dto.DataTableResponse + db := u.Helper.GetDB("slave") + tx := db.Table("package_type"). + Joins("LEFT JOIN merchant ON package_type.merchant_id=merchant.id") + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + tx = tx.Where("package_type.merchant_id IS NULL OR package_type.merchant_id=?", u.AuthUser.MerchantID) + } + dt := datatable.NewDatatable(tx, req) + coldef := make([]dto.DataTableColDef, 0) + coldef = append(coldef, dto.DataTableColDef{ + Field: "package_type.name", + Alias: "name", + }) + coldef = append(coldef, dto.DataTableColDef{ + Field: "merchant.id", + Alias: "merchant_id", + }) + coldef = append(coldef, dto.DataTableColDef{ + Field: "package_type.id", + Alias: "id", + }) + + response = dt.Render(coldef) + return response, nil +} + +func (u *PackageTypeUsecase) GetByID(id string) (domain.PackageType, error) { + var item domain.PackageType + db := u.Helper.GetDB("slave") + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + db = db.Where("merchant_id IS NULL OR merchant_id=?", u.AuthUser.MerchantID) + } + if err := db.Where("id = ?", id).First(&item).Error; err != nil { + return domain.PackageType{}, err + } + return item, nil +} + +func (u *PackageTypeUsecase) Create(name string, createdBy string, merchantid *string) (domain.PackageType, error) { + now := time.Now() + + db := u.Helper.GetDB("master") + + var item domain.PackageType + err := db.Transaction(func(tx *gorm.DB) error { + item = domain.PackageType{ + Name: &name, + MerchantID: merchantid, + CreatedBy: &createdBy, + CreatedOn: &now, + UpdatedBy: &createdBy, + UpdatedOn: &now, + } + + return tx.Create(&item).Error + }) + if err != nil { + return domain.PackageType{}, err + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("package_type", item, merchantid) + return item, nil +} + +func (u *PackageTypeUsecase) Update(id string, name string, updatedBy string) (domain.PackageType, error) { + var item domain.PackageType + var oldItem domain.PackageType + db := u.Helper.GetDB("master") + err := db.Transaction(func(tx *gorm.DB) error { + q := tx + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + q = tx.Where("merchant_id=?", u.AuthUser.MerchantID) + } + if err := q.Where("id = ?", id).First(&item).Error; err != nil { + return err + } + oldItem = item + now := time.Now() + item.Name = &name + item.UpdatedBy = &updatedBy + item.UpdatedOn = &now + return tx.Save(&item).Error + }) + if err != nil { + return domain.PackageType{}, err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("package_type", oldItem, item, merchantID) + return item, nil +} + +func (u *PackageTypeUsecase) Delete(id string) error { + db := u.Helper.GetDB("master") + var item domain.PackageType + q := db + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) + } + if err := q.Where("id = ?", id).First(&item).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("package_type", id, item, merchantID) + if err := db.Delete(&domain.PackageType{}, "id = ?", id).Error; err != nil { + return err + } + return nil +} diff --git a/internal/usecases/postal_code.go b/internal/usecases/postal_code.go index df9ffc2..e65c505 100644 --- a/internal/usecases/postal_code.go +++ b/internal/usecases/postal_code.go @@ -147,18 +147,21 @@ func (u *PostalCodeUsecase) Create(postalCode string, provinceID string, cityID CreatedBy: &createdBy, CreatedOn: &now, UpdatedBy: &createdBy, - UpdatedOn: &now, + UpdatedOn: strPtr(now.Format("2006-01-02 15:04:05.999999-07:00")), } return tx.Create(&pc).Error }) if err != nil { return domain.PostalCode{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("postal_code", pc, merchantid) return pc, nil } func (u *PostalCodeUsecase) Update(id string, postalCode string, provinceID string, cityID string, districtID string, subdistrictID string, updatedBy string) (domain.PostalCode, error) { var pc domain.PostalCode + var oldPC domain.PostalCode db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -168,6 +171,7 @@ func (u *PostalCodeUsecase) Update(id string, postalCode string, provinceID stri if err := q.Where("id = ?", id).First(&pc).Error; err != nil { return err } + oldPC = pc now := time.Now() pc.PostalCode = &postalCode pc.ProvinceID = &provinceID @@ -175,20 +179,37 @@ func (u *PostalCodeUsecase) Update(id string, postalCode string, provinceID stri pc.DistrictID = &districtID pc.SubdistrictID = &subdistrictID pc.UpdatedBy = &updatedBy - pc.UpdatedOn = &now + pc.UpdatedOn = strPtr(now.Format("2006-01-02 15:04:05.999999-07:00")) return tx.Save(&pc).Error }) if err != nil { return domain.PostalCode{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("postal_code", oldPC, pc, merchantID) return pc, nil } func (u *PostalCodeUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + var pc domain.PostalCode + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&pc).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("postal_code", id, pc, merchantID) if err := db.Delete(&domain.PostalCode{}, "id = ?", id).Error; err != nil { return err } diff --git a/internal/usecases/province.go b/internal/usecases/province.go index 778ecf2..75e33cc 100644 --- a/internal/usecases/province.go +++ b/internal/usecases/province.go @@ -121,11 +121,14 @@ func (u *ProvinceUsecase) Create(name string, createdBy string, merchantid *stri if err != nil { return domain.Province{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("province", province, merchantid) return province, nil } func (u *ProvinceUsecase) Update(id string, name string, updatedBy string) (domain.Province, error) { var province domain.Province + var oldProvince domain.Province db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -135,6 +138,7 @@ func (u *ProvinceUsecase) Update(id string, name string, updatedBy string) (doma if err := q.Where("id = ?", id).First(&province).Error; err != nil { return err } + oldProvince = province now := time.Now() province.Name = &name province.UpdatedBy = &updatedBy @@ -144,14 +148,34 @@ func (u *ProvinceUsecase) Update(id string, name string, updatedBy string) (doma if err != nil { return domain.Province{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("province", oldProvince, province, merchantID) return province, nil } func (u *ProvinceUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + + var province domain.Province + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&province).Error; err != nil { + return err + } + + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("province", id, province, merchantID) + if err := db.Delete(&domain.Province{}, "id = ?", id).Error; err != nil { return err } diff --git a/internal/usecases/subdistrict.go b/internal/usecases/subdistrict.go index b18e479..76b484a 100644 --- a/internal/usecases/subdistrict.go +++ b/internal/usecases/subdistrict.go @@ -144,11 +144,14 @@ func (u *SubdistrictUsecase) Create(name string, provinceID string, cityID strin if err != nil { return domain.Subdistrict{}, err } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogCreate("subdistrict", subdistrict, merchantid) return subdistrict, nil } func (u *SubdistrictUsecase) Update(id string, name string, provinceID string, cityID string, districtID string, updatedBy string) (domain.Subdistrict, error) { var subdistrict domain.Subdistrict + var oldSubdistrict domain.Subdistrict db := u.Helper.GetDB("master") err := db.Transaction(func(tx *gorm.DB) error { q := tx @@ -158,6 +161,7 @@ func (u *SubdistrictUsecase) Update(id string, name string, provinceID string, c if err := q.Where("id = ?", id).First(&subdistrict).Error; err != nil { return err } + oldSubdistrict = subdistrict now := time.Now() subdistrict.Name = &name subdistrict.ProvinceID = &provinceID @@ -170,14 +174,31 @@ func (u *SubdistrictUsecase) Update(id string, name string, provinceID string, c if err != nil { return domain.Subdistrict{}, err } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogUpdate("subdistrict", oldSubdistrict, subdistrict, merchantID) return subdistrict, nil } func (u *SubdistrictUsecase) Delete(id string) error { db := u.Helper.GetDB("master") + var subdistrict domain.Subdistrict + q := db if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { - db = db.Where("merchant_id=?", u.AuthUser.MerchantID) + q = db.Where("merchant_id=?", u.AuthUser.MerchantID) } + if err := q.Where("id = ?", id).First(&subdistrict).Error; err != nil { + return err + } + var merchantID *string + if u.AuthUser.UsersRole_Relation == nil || u.AuthUser.UsersRole_Relation.RoleKey != "SPM" { + merchantID = u.AuthUser.MerchantID + } + logUsecase := NewActivityLogUsecase(u.Helper, u.AuthUser) + logUsecase.LogDelete("subdistrict", id, subdistrict, merchantID) if err := db.Delete(&domain.Subdistrict{}, "id = ?", id).Error; err != nil { return err } diff --git a/migrations/003_activity_log.sql b/migrations/003_activity_log.sql new file mode 100644 index 0000000..bad9ab8 --- /dev/null +++ b/migrations/003_activity_log.sql @@ -0,0 +1,22 @@ +-- Activity Log table for user activity tracking and revert capability +-- Run this migration before deploying the new code + +CREATE TABLE IF NOT EXISTS activity_log ( + id character(26) PRIMARY KEY DEFAULT generate_ulid(), + table_name character varying(100) NOT NULL, + record_id character(26) NOT NULL, + action character varying(20) NOT NULL, + old_data jsonb, + new_data jsonb, + cascade_data jsonb, + user_id character(26) NOT NULL, + user_name character varying(255) NOT NULL, + merchant_id character(26), + created_on timestamp without time zone DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_activity_log_table_name ON activity_log(table_name); +CREATE INDEX IF NOT EXISTS idx_activity_log_record_id ON activity_log(record_id); +CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON activity_log(user_id); +CREATE INDEX IF NOT EXISTS idx_activity_log_created_on ON activity_log(created_on); +CREATE INDEX IF NOT EXISTS idx_activity_log_action ON activity_log(action); diff --git a/storage/logs/app.log b/storage/logs/app.log index cda180a..976e791 100644 --- a/storage/logs/app.log +++ b/storage/logs/app.log @@ -1280,3 +1280,8 @@ {"level":"info","ts":"2026-08-25T16:16:54.090+0700","caller":"database/database.go:63","msg":"DB Master Connected"} {"level":"info","ts":"2026-08-25T16:16:54.090+0700","caller":"database/database.go:76","msg":"Connect To Slave DB"} {"level":"info","ts":"2026-08-25T16:16:54.250+0700","caller":"database/database.go:86","msg":"DB Connection Done"} +{"level":"info","ts":"2026-09-04T21:21:20.045+0700","caller":"database/database.go:55","msg":"Connect To Master DB"} +{"level":"info","ts":"2026-09-04T21:21:20.135+0700","caller":"database/database.go:68","msg":"DB Master Connected"} +{"level":"info","ts":"2026-09-04T21:21:20.135+0700","caller":"database/database.go:81","msg":"Connect To Slave DB"} +{"level":"info","ts":"2026-09-04T21:21:20.232+0700","caller":"database/database.go:94","msg":"DB Connection Done"} +{"level":"info","ts":"2026-09-04T21:21:20.362+0700","caller":"handlers/handlers.go:136","msg":"Starting Apps"} diff --git a/storage/logs/db.log b/storage/logs/db.log index 8c497cb..101514f 100644 --- a/storage/logs/db.log +++ b/storage/logs/db.log @@ -15418,3 +15418,9 @@ {"level":"info","ts":"2026-08-25T16:17:15.380+0700","caller":"logger/database.go:40","msg":"generate query file: /Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/cmd/api/internal/domain/query_utils/subdistrict.gen.go"} {"level":"info","ts":"2026-08-25T16:17:15.392+0700","caller":"logger/database.go:40","msg":"generate query file: /Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/cmd/api/internal/domain/query_utils/gen.go"} {"level":"info","ts":"2026-08-25T16:17:15.392+0700","caller":"logger/database.go:40","msg":"Generate code done."} +{"level":"info","ts":"2026-09-04T21:21:20.232+0700","caller":"database/redis.go:34","msg":"Parse config master"} +{"level":"info","ts":"2026-09-04T21:21:20.234+0700","caller":"database/redis.go:49","msg":"Connecting to Redis master"} +{"level":"info","ts":"2026-09-04T21:21:20.293+0700","caller":"database/redis.go:62","msg":"Connected"} +{"level":"info","ts":"2026-09-04T21:21:20.293+0700","caller":"database/redis.go:34","msg":"Parse config slave"} +{"level":"info","ts":"2026-09-04T21:21:20.293+0700","caller":"database/redis.go:49","msg":"Connecting to Redis slave"} +{"level":"info","ts":"2026-09-04T21:21:20.361+0700","caller":"database/redis.go:62","msg":"Connected"}