diff --git a/git.cakeli.workers.dev-Ravig786-challenge2016.postman_collection.json b/git.cakeli.workers.dev-Ravig786-challenge2016.postman_collection.json new file mode 100644 index 000000000..6b7c8e7ff --- /dev/null +++ b/git.cakeli.workers.dev-Ravig786-challenge2016.postman_collection.json @@ -0,0 +1,272 @@ +{ + "info": { + "_postman_id": "b8ebc29a-cd0e-40a9-860d-d63616489238", + "name": "github.com/Ravig786/challenge2016", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "15563037" + }, + "item": [ + { + "name": "Region Data", + "item": [ + { + "name": "Get Countries", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/countries", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "countries" + ] + } + }, + "response": [] + }, + { + "name": "Get States", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/countries/:country_code/states", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "countries", + ":country_code", + "states" + ], + "variable": [ + { + "key": "country_code", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "Get Cities", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/countries/:country_code/states/:state_code/cities", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "countries", + ":country_code", + "states", + ":state_code", + "cities" + ], + "variable": [ + { + "key": "country_code", + "value": null + }, + { + "key": "state_code", + "value": null + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Distributor", + "item": [ + { + "name": "1.Add Distributor", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\":\"ravi\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:5000/distributor", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributor" + ] + } + }, + "response": [] + }, + { + "name": "2.Add Permission to Distributor", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"region\":\"IN\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:5000/distributor/:name/:action", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributor", + ":name", + ":action" + ], + "variable": [ + { + "key": "name", + "value": "ravi" + }, + { + "key": "action", + "value": "include" + } + ] + } + }, + "response": [] + }, + { + "name": "3.Add Permission to Distributor Copy", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:5000/distributor/:name/:action", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributor", + ":name", + ":action" + ], + "variable": [ + { + "key": "name", + "value": "ravi" + }, + { + "key": "action", + "value": "EXCLUDE" + } + ] + } + }, + "response": [] + }, + { + "name": "4.Check Distribution Location", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/distributor/:name/can-distribute", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributor", + ":name", + "can-distribute" + ], + "variable": [ + { + "key": "name", + "value": null + } + ] + } + }, + "response": [] + }, + { + "name": "5. Get Distributors", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/distributors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributors" + ] + } + }, + "response": [] + }, + { + "name": "6.Add Sub Distributor", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\":\"ravi-child\",\n \"parent\":\"ravi\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:5000/distributor", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "distributor" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d199e4c15 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/Ravig786/challenge2016 + +go 1.23.4 + +require github.com/gin-gonic/gin v1.10.0 + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..7f08abb20 --- /dev/null +++ b/go.sum @@ -0,0 +1,89 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 000000000..529d3cbbe --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,102 @@ +package handlers + +import ( + "net/http" + + "github.com/Ravig786/challenge2016/services" + "github.com/gin-gonic/gin" +) + +func CreateDistributorHandler(c *gin.Context) { + var payload struct { + Name string `json:"name"` + Parent string `json:"parent,omitempty"` + } + + if err := c.ShouldBindJSON(&payload); err != nil || payload.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + err := services.CreateDistributor(payload.Name, payload.Parent) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "Distributor created", "name": payload.Name, "parent": payload.Parent}) +} + +func AddPermission(c *gin.Context) { + name := c.Param("name") + action := c.Param("action") + + var payload struct { + Region string `json:"region"` + } + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON payload"}) + return + } + + isInclude := action == "include" + err := services.AddPermission(name, payload.Region, isInclude) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Permission updated", "distributor": name, "action": action, "region": payload.Region}) +} + +func CheckDistribution(c *gin.Context) { + name := c.Param("name") + location := c.Query("location") + + if location == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Location query parameter is required"}) + return + } + + allowed, err := services.CanDistribute(name, location) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"distributor": name, "location": location, "allowed": allowed}) +} + +func GetAllDistributors(c *gin.Context) { + distributors := services.GetAllDistributors() + c.JSON(http.StatusOK, gin.H{"distributors": distributors}) +} + +func GetAllCountriesHandler(c *gin.Context) { + countries := services.GetAllCountries() + c.JSON(http.StatusOK, gin.H{"countries": countries}) +} + +func GetStatesByCountryHandler(c *gin.Context) { + countryCode := c.Param("country_code") + + states, err := services.GetStatesByCountry(countryCode) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"country_code": countryCode, "states": states}) +} + +func GetCitiesByStateHandler(c *gin.Context) { + countryCode := c.Param("country_code") + stateCode := c.Param("state_code") + + cities, err := services.GetCitiesByState(countryCode, stateCode) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"country_code": countryCode, "state_code": stateCode, "cities": cities}) +} diff --git a/main.go b/main.go new file mode 100644 index 000000000..6aa11f4ab --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/Ravig786/challenge2016/models" + "github.com/Ravig786/challenge2016/router" + "github.com/gin-gonic/gin" +) + +func main() { + fmt.Println("Initializing data...") + + models.InitRegionData() + models.InitDistributorRegistry() + + err := models.LoadRegionDataFromCSV("cities.csv") + if err != nil { + fmt.Println("Error loading region data:", err) + } + // models.PrintGlobalRegionData() + + fmt.Println("Data initialized successfully!") + + r := gin.Default() + router.SetupRoutes(r) + r.Run(":5000") +} diff --git a/models/distributor.go b/models/distributor.go new file mode 100644 index 000000000..8626f9c76 --- /dev/null +++ b/models/distributor.go @@ -0,0 +1,22 @@ +package models + +import "sync" + +type DistributorPermissions struct { + Includes map[string]struct{} + Excludes map[string]struct{} + Parent string +} + +type DistributorRegistry struct { + sync.RWMutex + Distributors map[string]*DistributorPermissions +} + +var Registry *DistributorRegistry + +func InitDistributorRegistry() { + Registry = &DistributorRegistry{ + Distributors: make(map[string]*DistributorPermissions), + } +} diff --git a/models/region.go b/models/region.go new file mode 100644 index 000000000..bcd47a0d6 --- /dev/null +++ b/models/region.go @@ -0,0 +1,105 @@ +package models + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" +) + +type City struct { + CityCode string `json:"city_code"` + CityName string `json:"city_name"` +} + +type State struct { + StateCode string `json:"state_code"` + StateName string `json:"state_name"` + Cities []City `json:"city_list"` +} + +type Country struct { + CountryName string `json:"country_name"` + CountryCode string `json:"country_code"` + States []State `json:"state_list"` +} + +type RegionData struct { + Data map[string]*Country +} + +var GlobalRegionData *RegionData + +func InitRegionData() { + GlobalRegionData = &RegionData{ + Data: make(map[string]*Country), + } +} + +func LoadRegionDataFromCSV(filePath string) error { + + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open region data file: %v", err) + } + defer file.Close() + + reader := csv.NewReader(file) + _, _ = reader.Read() + + for { + record, err := reader.Read() + if err != nil { + break + } + + cityCode, stateCode, countryCode := record[0], record[1], record[2] + cityName, stateName, countryName := record[3], record[4], record[5] + + if _, exists := GlobalRegionData.Data[countryCode]; !exists { + GlobalRegionData.Data[countryCode] = &Country{ + CountryCode: countryCode, + CountryName: countryName, + States: []State{}, + } + } + + country := GlobalRegionData.Data[countryCode] + + stateIndex := -1 + for i, state := range country.States { + if state.StateCode == stateCode { + stateIndex = i + break + } + } + if stateIndex == -1 { + + country.States = append(country.States, State{ + StateCode: stateCode, + StateName: stateName, + Cities: []City{}, + }) + stateIndex = len(country.States) - 1 + } + + country.States[stateIndex].Cities = append(country.States[stateIndex].Cities, City{ + CityCode: cityCode, + CityName: cityName, + }) + } + + fmt.Println("Region data loaded successfully!") + return nil +} + +func PrintGlobalRegionData() { + data, err := json.MarshalIndent(GlobalRegionData.Data, "", " ") + if err != nil { + fmt.Println("error converting GlobalRegionData to JSON:", err) + return + } + + fmt.Println("globalRegionData (In-Memory Store):") + fmt.Println(string(data)) +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 000000000..0e12c3be5 --- /dev/null +++ b/router/router.go @@ -0,0 +1,27 @@ +package router + +import ( + "github.com/Ravig786/challenge2016/handlers" + "github.com/gin-gonic/gin" +) + +func SetupRoutes(r *gin.Engine) { + + // store distributor details + r.POST("/distributor", handlers.CreateDistributorHandler) + + // give permission to distributor for location + r.POST("/distributor/:name/:action", handlers.AddPermission) + + // check location for distributor + r.GET("/distributor/:name/can-distribute", handlers.CheckDistribution) + + // get distributors + r.GET("/distributors", handlers.GetAllDistributors) + + // get countries, states, cities + r.GET("/countries", handlers.GetAllCountriesHandler) + r.GET("/countries/:country_code/states", handlers.GetStatesByCountryHandler) + r.GET("/countries/:country_code/states/:state_code/cities", handlers.GetCitiesByStateHandler) + +} diff --git a/services/service.go b/services/service.go new file mode 100644 index 000000000..fb6d4e052 --- /dev/null +++ b/services/service.go @@ -0,0 +1,307 @@ +package services + +import ( + "fmt" + "sort" + + "github.com/Ravig786/challenge2016/models" + "github.com/Ravig786/challenge2016/utils" +) + +func CreateDistributor(name, parent string) error { + models.Registry.Lock() + defer models.Registry.Unlock() + + if _, exists := models.Registry.Distributors[name]; exists { + return fmt.Errorf("distributor %s already exists", name) + } + + if parent == "" { + models.Registry.Distributors[name] = &models.DistributorPermissions{ + Includes: make(map[string]struct{}), + Excludes: make(map[string]struct{}), + Parent: "", + } + fmt.Printf("distributor %s created successfully\n", name) + return nil + } + + parentDistributor, parentExists := models.Registry.Distributors[parent] + if !parentExists { + return fmt.Errorf("parent distributor %s not found", parent) + } + + subDistributor := &models.DistributorPermissions{ + Includes: make(map[string]struct{}), + Excludes: make(map[string]struct{}), + Parent: parent, + } + + for region := range parentDistributor.Includes { + subDistributor.Includes[region] = struct{}{} + } + for region := range parentDistributor.Excludes { + subDistributor.Excludes[region] = struct{}{} + } + + models.Registry.Distributors[name] = subDistributor + + fmt.Printf("✅ Sub-distributor %s created under %s with inherited permissions\n", name, parent) + return nil +} + +func AddPermission(name, region string, isInclude bool) error { + models.Registry.Lock() + defer models.Registry.Unlock() + + distributor, exists := models.Registry.Distributors[name] + if !exists { + return fmt.Errorf("distributor %s not found", name) + } + + normalizedRegion := utils.NormalizeRegion(region) + + fmt.Printf("Adding %s: %s for distributor: %s\n", map[bool]string{true: "INCLUDE", false: "EXCLUDE"}[isInclude], normalizedRegion, name) + + if !isRegionValid(normalizedRegion) { + return fmt.Errorf("invalid region: %s. Region does not exist in memory", region) + } + + if isInclude { + distributor.Includes[normalizedRegion] = struct{}{} + } else { + distributor.Excludes[normalizedRegion] = struct{}{} + } + + fmt.Printf("Updated Distributor Data: %+v\n", distributor) + return nil +} + +func CanDistribute(name, location string) (bool, error) { + models.Registry.RLock() + defer models.Registry.RUnlock() + + _, exists := models.Registry.Distributors[name] + if !exists { + return false, fmt.Errorf("distributor %s not found", name) + } + + normalizedLocation := utils.NormalizeRegion(location) + + fmt.Printf("🔍 Checking if %s can distribute in: %s\n", name, normalizedLocation) + + if !isRegionValid(normalizedLocation) { + fmt.Printf("⚠️ Invalid location check: %s does not exist in GlobalRegionData\n", location) + return false, nil + } + + locationParts := utils.SplitRegion(normalizedLocation) + locationDepth := len(locationParts) + + for d := name; d != ""; d = models.Registry.Distributors[d].Parent { + permissions, exists := models.Registry.Distributors[d] + if !exists { + return false, nil + } + + if locationDepth == 3 { + stateRegion := fmt.Sprintf("%s-%s", locationParts[1], locationParts[2]) + countryRegion := locationParts[2] + + if _, found := permissions.Excludes[countryRegion]; found { + fmt.Printf("Access Denied! Country %s is excluded for %s\n", countryRegion, name) + return false, nil + } + + if _, found := permissions.Excludes[stateRegion]; found { + fmt.Printf("Access Denied! State %s is excluded for %s\n", stateRegion, name) + return false, nil + } + + if _, found := permissions.Excludes[normalizedLocation]; found { + fmt.Printf("Access Denied! City %s is explicitly excluded for %s\n", normalizedLocation, name) + return false, nil + } + } + + if locationDepth == 2 { + countryRegion := locationParts[1] + + if _, found := permissions.Excludes[countryRegion]; found { + fmt.Printf("Access Denied! Country %s is excluded for %s\n", countryRegion, name) + return false, nil + } + + if _, found := permissions.Excludes[normalizedLocation]; found { + fmt.Printf("Access Denied! State %s is explicitly excluded for %s\n", normalizedLocation, name) + return false, nil + } + } + + if locationDepth == 1 { + if _, found := permissions.Excludes[normalizedLocation]; found { + fmt.Printf("Access Denied! Country %s is explicitly excluded for %s\n", normalizedLocation, name) + return false, nil + } + } + + if locationDepth == 3 { + stateRegion := fmt.Sprintf("%s-%s", locationParts[1], locationParts[2]) + countryRegion := locationParts[2] + + if _, found := permissions.Includes[countryRegion]; found { + fmt.Printf("Access Allowed! Country-level access granted for %s\n", normalizedLocation) + return true, nil + } + + if _, found := permissions.Includes[stateRegion]; found { + fmt.Printf("Access Allowed! State-level access granted for %s\n", normalizedLocation) + return true, nil + } + + if _, found := permissions.Includes[normalizedLocation]; found { + fmt.Printf("Access Allowed! City %s is explicitly included for %s\n", normalizedLocation, name) + return true, nil + } + } + + if locationDepth == 2 { + countryRegion := locationParts[1] + + if _, found := permissions.Includes[countryRegion]; found { + fmt.Printf("Access Allowed! Country-level access granted for %s\n", normalizedLocation) + return true, nil + } + + if _, found := permissions.Includes[normalizedLocation]; found { + fmt.Printf("Access Allowed! State %s is explicitly included for %s\n", normalizedLocation, name) + return true, nil + } + } + + if locationDepth == 1 { + if _, found := permissions.Includes[normalizedLocation]; found { + fmt.Printf("✅ Access Allowed! Country %s is explicitly included for %s\n", normalizedLocation, name) + return true, nil + } + } + } + + fmt.Printf("Access Denied! No INCLUDE rule found for %s\n", normalizedLocation) + return false, nil +} + +func GetAllDistributors() []string { + var distributors []string + for name := range models.Registry.Distributors { + distributors = append(distributors, name) + } + return distributors +} + +type countryInfo struct { + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` +} + +func GetAllCountries() []countryInfo { + countries := []countryInfo{} + + for countryCode, country := range models.GlobalRegionData.Data { + countries = append(countries, countryInfo{ + CountryCode: countryCode, + CountryName: country.CountryName, + }) + } + + sort.Slice(countries, func(i, j int) bool { + return countries[i].CountryName < countries[j].CountryName + }) + + return countries +} + +func GetStatesByCountry(countryCode string) ([]map[string]string, error) { + + fmt.Println(models.GlobalRegionData.Data) + + if country, exists := models.GlobalRegionData.Data[countryCode]; exists { + var states []map[string]string + for _, state := range country.States { + states = append(states, map[string]string{ + "state_code": state.StateCode, + "state_name": state.StateName, + }) + } + + sort.Slice(states, func(i, j int) bool { + return states[i]["state_name"] < states[j]["state_name"] + }) + return states, nil + } + + return nil, fmt.Errorf(" Country code %s not found", countryCode) +} + +/* +func GetStatesByCountry(countryCode string) ([]models.State, error) { + models.GlobalRegionData.RLock() + defer models.GlobalRegionData.RUnlock() + + if country, exists := models.GlobalRegionData.Data[countryCode]; exists { + return country.States, nil + } + + return nil, fmt.Errorf("country code %s not found", countryCode) +} +*/ + +func GetCitiesByState(countryCode, stateCode string) ([]models.City, error) { + + if country, exists := models.GlobalRegionData.Data[countryCode]; exists { + for _, state := range country.States { + if state.StateCode == stateCode { + sort.Slice(state.Cities, func(i, j int) bool { + return state.Cities[i].CityName < state.Cities[j].CityName + }) + return state.Cities, nil + } + } + return nil, fmt.Errorf("state code %s not found in country %s", stateCode, countryCode) + } + + return nil, fmt.Errorf("country code %s not found", countryCode) +} + +func isRegionValid(region string) bool { + + parts := utils.SplitRegion(region) + length := len(parts) + + switch length { + case 1: + _, exists := models.GlobalRegionData.Data[parts[0]] + return exists + case 2: + if country, exists := models.GlobalRegionData.Data[parts[1]]; exists { + for _, state := range country.States { + if state.StateCode == parts[0] { + return true + } + } + } + case 3: + if country, exists := models.GlobalRegionData.Data[parts[2]]; exists { + for _, state := range country.States { + if state.StateCode == parts[1] { + for _, city := range state.Cities { + if city.CityCode == parts[0] { + return true + } + } + } + } + } + } + return false +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 000000000..c7811fec2 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,11 @@ +package utils + +import "strings" + +func NormalizeRegion(region string) string { + return strings.ToUpper(region) +} + +func SplitRegion(region string) []string { + return strings.Split(strings.ToUpper(region), "-") +}