diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index e94dd7d..0000000 --- a/.drone.yml +++ /dev/null @@ -1,67 +0,0 @@ -kind: pipeline -type: docker -name: default - -steps: -- name: static_check - image: golang:latest - commands: - - go install honnef.co/go/tools/cmd/staticcheck@latest - - cd src && staticcheck ./... - volumes: - - name: gopath - path: /go - -- name: lint - image: golang:latest - commands: - - go install golang.org/x/lint/golint@latest - - golint ./src/... - volumes: - - name: gopath - path: /go - -- name: analyze - image: golang:latest - commands: - - cd src && go vet ./... - volumes: - - name: gopath - path: /go - -- name: publish_image - image: plugins/docker - environment: - DOCKER_USERNAME: - from_secret: registry_username - DOCKER_PASSWORD: - from_secret: registry_password - commands: - - sleep 5 - - ./deploy/image-build.sh - - ./deploy/image-push.sh - volumes: - - name: docker-sock - path: /var/run - when: - branch: - - main - -services: -- name: docker - image: docker:dind - privileged: true - volumes: - - name: docker-sock - path: /var/run - - name: etc_hosts - path: /etc/hosts - -volumes: -- name: gopath - temp: {} -- name: docker-sock - temp: {} -- name: etc_hosts - host: - path: /etc/hosts diff --git a/.gitignore b/.gitignore index 17da0d3..4c7d531 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ deploy/.env !deploy/.env.dist -deploy/.env.* +!deploy/.env.docker +deploy/.env.local deploy/server deploy/worker diff --git a/.woodpecker/workflow.yaml b/.woodpecker/workflow.yaml new file mode 100644 index 0000000..18909ed --- /dev/null +++ b/.woodpecker/workflow.yaml @@ -0,0 +1,134 @@ +variables: + - &golang_image 'docker.io/golang:1.24' + - &buildx_plugin 'docker.io/woodpeckerci/plugin-docker-buildx:6.0.3' + - &platforms_release 'linux/amd64' + - &build_args 'CI_COMMIT_SHA=${CI_COMMIT_SHA},CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH},CI_COMMIT_TAG=${CI_COMMIT_TAG}' + - publish_logins: &publish_logins + - registry: git.ego.freeddns.org + username: + from_secret: docker_username + password: + from_secret: docker_password + +when: + - event: [pull_request, tag] + - event: push + # branch: ${CI_REPO_DEFAULT_BRANCH} + branch: develop + +steps: + # - name: analyze + # image: golang:1.24 + # commands: + # - echo "Analyze" + # - cd src && go vet ./... + # - name: static check + # image: golang:1.24 + # commands: + # - echo "Static Check" + # - go install honnef.co/go/tools/cmd/staticcheck@latest + # - cd src && staticcheck ./... + # - name: lint + # image: golang:1.24 + # commands: + # - echo "Lint" + # - go install golang.org/x/lint/golint@latest + # - golint ./src/... + - name: vendor + image: *golang_image + pull: true + commands: + - cd src/ + - go mod vendor + when: + - branch: + - develop + - name: build-image + image: woodpeckerci/plugin-docker-buildx + depends_on: + - vendor + commands: + - docker build --rm -t "egommerce-builder:catalog" -f deploy/docker/Dockerfile.builder . + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # settings: + # repo: egommerce-builder + # dockerfile: deploy/docker/Dockerfile.builder + # tag: catalog + # logins: *publish_logins + - name: push-image + image: woodpeckerci/plugin-docker-buildx + depends_on: + - build-image + settings: + repo: git.ego.freeddns.org/egommerce/catalog-svc + dockerfile: deploy/docker/Dockerfile.target + build_args: + BUILDER_IMAGE: "egommerce-builder:catalog" + SVC_NAME: "catalog-svc" + SVC_VER: "1.0" + tag: dev + logins: *publish_logins +# steps: +# - name: static_check +# image: golang:latest +# commands: +# - go install honnef.co/go/tools/cmd/staticcheck@latest +# - cd src && staticcheck ./... +# volumes: +# - name: gopath +# path: /go + +# - name: lint +# image: golang:latest +# commands: +# - go install golang.org/x/lint/golint@latest +# - golint ./src/... +# volumes: +# - name: gopath +# path: /go + +# - name: analyze +# image: golang:latest +# commands: +# - cd src && go vet ./... +# volumes: +# - name: gopath +# path: /go + +# - name: publish_image +# image: plugins/docker +# environment: +# DOCKER_USERNAME: +# from_secret: registry_username +# DOCKER_PASSWORD: +# from_secret: registry_password +# commands: +# - sleep 5 +# - ./deploy/image-build.sh +# - ./deploy/image-push.sh +# volumes: +# - name: docker-sock +# path: /var/run +# when: +# branch: +# - main + +# services: +# - name: docker +# image: docker:dind +# privileged: true +# volumes: +# - name: docker-sock +# path: /var/run +# - name: etc_hosts +# path: /etc/hosts + +# volumes: +# - name: gopath +# temp: {} +# - name: docker-sock +# temp: {} +# - name: etc_hosts +# host: +# path: /etc/hosts diff --git a/Makefile b/Makefile index a7c4191..d683874 100644 --- a/Makefile +++ b/Makefile @@ -2,16 +2,16 @@ DEPLOY_DIR := ./deploy SRC_DIR := ./src build-image-dev: - - sh ${DEPLOY_DIR}/image-build.sh dev + - sh ${DEPLOY_DIR}/scripts/image-build.sh dev build-image-prod: - - sh ${DEPLOY_DIR}/image-build.sh + - sh ${DEPLOY_DIR}/scripts/image-build.sh push-image-dev: - - sh ${DEPLOY_DIR}/image-push.sh dev + - sh ${DEPLOY_DIR}/scripts/image-push.sh dev push-image-prod: - - sh ${DEPLOY_DIR}/image-push.sh + - sh ${DEPLOY_DIR}/scripts/image-push.sh build-local-scheduler: - go build -C ${SRC_DIR} -o ../deploy/scheduler cmd/scheduler/main.go diff --git a/deploy/.env.dist b/deploy/.env.dist new file mode 100644 index 0000000..f433d02 --- /dev/null +++ b/deploy/.env.dist @@ -0,0 +1,12 @@ +SERVER_ADDR=:443 + +APP_NAME=catalog-svc +APP_DOMAIN=catalog.service.ego.io + +API_DATABASE_URL=postgres://postgres:12345678@db-postgres:5432/egommerce +API_CACHE_ADDR=api-cache:6379 +API_CACHE_USERNAME=default +API_CACHE_PASSWORD=12345678 +API_MONGODB_URL=mongodb://mongodb:12345678@mongo-db:27017 +API_EVENTBUS_URL=amqp://guest:guest@api-eventbus:5672 + diff --git a/deploy/.env.docker b/deploy/.env.docker new file mode 100644 index 0000000..432e7cd --- /dev/null +++ b/deploy/.env.docker @@ -0,0 +1,13 @@ +SERVER_ADDR=:443 + +APP_NAME=catalog-svc +APP_DOMAIN=catalog.service.ego.io + +API_LOGGER_ADDR=api-logger:24224 +API_DATABASE_URL=postgres://postgres:12345678@db-postgres:5432/egommerce +API_CACHE_ADDR=api-cache:6379 +API_CACHE_USERNAME=default +API_CACHE_PASSWORD=12345678 +API_MONGODB_URL=mongodb://mongodb:12345678@mongo-db:27017 +API_EVENTBUS_URL=amqp://guest:guest@api-eventbus:5672 + diff --git a/Dockerfile.builder b/deploy/docker/Dockerfile.builder similarity index 100% rename from Dockerfile.builder rename to deploy/docker/Dockerfile.builder diff --git a/Dockerfile.target b/deploy/docker/Dockerfile.target similarity index 100% rename from Dockerfile.target rename to deploy/docker/Dockerfile.target diff --git a/deploy/docker/Dockerfile.woodpecker b/deploy/docker/Dockerfile.woodpecker new file mode 100644 index 0000000..a7b3221 --- /dev/null +++ b/deploy/docker/Dockerfile.woodpecker @@ -0,0 +1,5 @@ +FROM docker.io/golang:1.25-alpine AS golang_image +FROM docker.io/alpine:3.22 + +COPY --from=golang_image /usr/local/go /usr/local/go + diff --git a/deploy/image-build.sh b/deploy/scripts/image-build.sh similarity index 76% rename from deploy/image-build.sh rename to deploy/scripts/image-build.sh index 10230a7..b927cb6 100755 --- a/deploy/image-build.sh +++ b/deploy/scripts/image-build.sh @@ -12,7 +12,7 @@ TARGET=${1:-latest} [ ! -d "src/vendor" ] && sh -c "cd src; go mod vendor" echo "Building target $IMAGE_PREFIX images..." -docker build --rm -t "$BUILDER_IMAGE" -f Dockerfile.builder . +docker build --rm -t "$BUILDER_IMAGE" -f deploy/docker/Dockerfile.builder . if [ $TARGET = "latest" ] then @@ -24,7 +24,7 @@ then --build-arg BUILD_TIME \ --rm --cache-from $SERVER_IMAGE:$TARGET \ -t $SERVER_IMAGE:$TARGET \ - -f Dockerfile.target . > /dev/null 2>&1 && echo "Successfully tagged $SERVER_IMAGE:$TARGET" + -f deploy/docker/Dockerfile.target . > /dev/null 2>&1 && echo "Successfully tagged $SERVER_IMAGE:$TARGET" # WORKER - TODO REMOVE IN FUTURE - cause we copy worker binary to the server image # docker build \ @@ -35,7 +35,7 @@ then # --build-arg BUILD_TIME \ # --rm --cache-from $WORKER_IMAGE:$TARGET \ # -t $WORKER_IMAGE:$TARGET \ - # -f Dockerfile.target . >/dev/null 2>&1 && echo "Successfully tagged $WORKER_IMAGE:$TARGET" + # -f deploy/docker/Dockerfile.target . >/dev/null 2>&1 && echo "Successfully tagged $WORKER_IMAGE:$TARGET" else # DEV docker build \ @@ -44,7 +44,7 @@ else --build-arg BUILDER_IMAGE=$BUILDER_IMAGE \ --build-arg BUILD_TIME \ --rm --no-cache -t $SERVER_IMAGE:$TARGET \ - -f Dockerfile.target . > /dev/null 2>&1 && echo "Successfully tagged $SERVER_IMAGE:$TARGET" + -f deploy/docker/Dockerfile.target . > /dev/null 2>&1 && echo "Successfully tagged $SERVER_IMAGE:$TARGET" # WORKER # docker build \ @@ -55,7 +55,7 @@ else # --build-arg BUILD_TIME \ # --rm --no-cache \ # -t $WORKER_IMAGE:$TARGET \ - # -f Dockerfile.target . >/dev/null 2>&1 && echo "Successfully tagged $WORKER_IMAGE:$TARGET" + # -f deploy/docker/Dockerfile.target . >/dev/null 2>&1 && echo "Successfully tagged $WORKER_IMAGE:$TARGET" fi echo "Done." diff --git a/deploy/image-push.sh b/deploy/scripts/image-push.sh similarity index 100% rename from deploy/image-push.sh rename to deploy/scripts/image-push.sh diff --git a/src/app/app.go b/src/app/app.go index ab0c1fb..d2cc619 100644 --- a/src/app/app.go +++ b/src/app/app.go @@ -36,5 +36,5 @@ func (app *App) Shutdown() { } func (app *App) RegisterPlugin(plugin Plugin) { - app.worker.addPlugin(plugin.name, plugin.fn) + app.worker.addPlugin(plugin.name, plugin.connect) } diff --git a/src/app/interface.go b/src/app/interface.go index a5382d3..bf6bc03 100644 --- a/src/app/interface.go +++ b/src/app/interface.go @@ -11,13 +11,6 @@ type ( Start() error OnShutdown() - addPlugin(name string, fn PluginFn) + addPlugin(name string, fn PluginConnectFn) } - - Plugin struct { - name string - fn PluginFn - } - - PluginFn func() any ) diff --git a/src/app/plugins.go b/src/app/plugins.go index 4c6dc72..43b19b3 100644 --- a/src/app/plugins.go +++ b/src/app/plugins.go @@ -3,15 +3,22 @@ package app import ( "context" "log" - "os" "time" redis "github.com/go-redis/redis/v8" "github.com/jackc/pgx/v5/pgxpool" - db "github.com/jackc/pgx/v5/pgxpool" amqp "github.com/rabbitmq/amqp091-go" ) +type ( + Plugin struct { + name string + connect PluginConnectFn + } + + PluginConnectFn func() any // returns connection handle +) + type PluginManager struct { plugins map[string]any } @@ -22,68 +29,144 @@ func NewPluginManager() *PluginManager { } } -func (pm *PluginManager) addPlugin(name string, fn PluginFn) { +func (pm *PluginManager) addPlugin(name string, fn PluginConnectFn) { pm.plugins[name] = fn() } -func (pm *PluginManager) getCache() *redis.Client { +func (pm *PluginManager) GetCache() *redis.Client { return (pm.plugins["cache"]).(*redis.Client) } -func (pm *PluginManager) getDatabase() *pgxpool.Pool { +func (pm *PluginManager) GetDatabase() *pgxpool.Pool { return (pm.plugins["database"]).(*pgxpool.Pool) } -func (pm *PluginManager) getEventbus() *amqp.Channel { + +func (pm *PluginManager) GetEventbus() *amqp.Channel { return (pm.plugins["eventbus"]).(*amqp.Channel) } func CachePlugin(cnf *Config) Plugin { + // plugin := &Plugin{ + // name: "cache", + // connectFn: func() any {}, + // afterConnFn: func() any {}, + // } + + connectFn := func() *redis.Client { + log.Println("establishing api-cache connection...") + + return redis.NewClient(&redis.Options{ + Addr: cnf.CacheAddr, + Username: cnf.CacheUsername, + Password: cnf.CachePassword, + DB: 0, // TODO + DialTimeout: 100 * time.Millisecond, // TODO + }) + } + + // checking if the connection is still alive and try to reconnect when it is not + go func(conn *redis.Client) { + tick := time.NewTicker(5 * time.Second) // is 5 seconds is not too much? + defer tick.Stop() + + for { + select { + case <-tick.C: + if err := conn.Ping(context.Background()).Err(); err != nil { + log.Println("lost connection with api-cache. Reconnecting...") + conn = connectFn() + } + } + } + }(connectFn()) + return Plugin{ name: "cache", - fn: func() any { - return redis.NewClient(&redis.Options{ - Addr: cnf.CacheAddr, - Username: cnf.CacheUsername, - Password: cnf.CachePassword, - DB: 0, // TODO - DialTimeout: 100 * time.Millisecond, // TODO - }) + connect: func() any { + return connectFn() }, } } func DatabasePlugin(cnf *Config) Plugin { + connectFn := func() *pgxpool.Pool { + log.Println("establishing db-postgres connection...") + + conn, err := pgxpool.New(context.Background(), cnf.DbURL) + if err != nil { + log.Printf("failed to connect to the database: %s. Err: %s\n", cnf.DbURL, err.Error()) + return nil + // os.Exit(1) + } + + return conn + } + + // checking if the connection is still alive and try to reconnect when it is not + go func(conn *pgxpool.Pool) { + tick := time.NewTicker(5 * time.Second) // is 5 seconds is not too much? + defer tick.Stop() + + for { + select { + case <-tick.C: + if err := conn.Ping(context.Background()); err != nil { + log.Println("lost connection with db-postgres. Reconnecting...") + conn = connectFn() + } + } + } + }(connectFn()) + return Plugin{ name: "database", - fn: func() any { - dbConn, err := db.New(context.Background(), cnf.DbURL) - if err != nil { - log.Fatalf("Failed to connect to the Database: %s. Err: %v\n", cnf.DbURL, err) - os.Exit(1) - } - - return dbConn + connect: func() any { + return connectFn() }, } } func EventbusPlugin(cnf *Config) Plugin { + connectFn := func() *amqp.Channel { + log.Println("establishing api-eventbus connection...") + + conn, err := amqp.Dial(cnf.EventbusURL) + if err != nil { + log.Fatalf("failed to connect to the eventbus: %s. Err: %v\n", cnf.EventbusURL, err.Error()) + + return nil + } + + chn, err := conn.Channel() + if err != nil { + log.Fatalf("failed to open new eventbus channel. Err: %v\n", err.Error()) + + return nil + } + + return chn + } + + // checking if the connection is still alive and try to reconnect when it is not + go func(chn *amqp.Channel) { + tick := time.NewTicker(5 * time.Second) // is 5 seconds is not too much? + defer tick.Stop() + + for { + select { + case <-tick.C: + if closed := chn.IsClosed(); closed { + log.Println("lost connection with api-eventbus. Reconnecting...") + chn = connectFn() + } + } + } + }(connectFn()) + return Plugin{ name: "eventbus", - fn: func() any { - conn, err := amqp.Dial(cnf.EventbusURL) - if err != nil { - log.Fatalf("Failed to connect to the Eventbus: %s. Err: %v\n", cnf.EventbusURL, err) - os.Exit(1) - } - - chn, err := conn.Channel() - if err != nil { - log.Fatalf("Failed to open new Eventbus channel. Err: %v\n", err) - os.Exit(1) - } - - return chn + connect: func() any { + return connectFn() }, } } diff --git a/src/app/scheduler.go b/src/app/scheduler.go index a64ba62..982849f 100644 --- a/src/app/scheduler.go +++ b/src/app/scheduler.go @@ -28,8 +28,8 @@ func (c *Scheduler) Start() error { func (c *Scheduler) OnShutdown() { log.Println("Scheduler is going down...") - c.getDatabase().Close() - c.getCache().Close() + c.GetDatabase().Close() + c.GetCache().Close() } // func (s *Server) addPlugin(name string, fn PluginFn) { diff --git a/src/app/server.go b/src/app/server.go index eb5262d..c076901 100644 --- a/src/app/server.go +++ b/src/app/server.go @@ -63,7 +63,7 @@ func (s *Server) Start() error { crt, err := tls.LoadX509KeyPair("certs/catalog-svc.crt", "certs/catalog-svc.key") if err != nil { - log.Fatal(err) + log.Fatal("failed to load certificates:", err) } tlsCnf := &tls.Config{Certificates: []tls.Certificate{crt}} @@ -76,29 +76,13 @@ func (s *Server) Start() error { func (s *Server) OnShutdown() { log.Printf("Server %s is going down...", s.ID) - s.getDatabase().Close() - s.getEventbus().Close() - s.getCache().Close() + s.GetDatabase().Close() + s.GetEventbus().Close() + s.GetCache().Close() s.Shutdown() } -// func (s *Server) addPlugin(name string, fn PluginFn) { -// s.plugins[name] = fn() -// } - -// func (s *Server) getCache() *redis.Client { -// return (s.plugins["cache"]).(*redis.Client) -// } - -// func (s *Server) getDatabase() *pgxpool.Pool { -// return (s.plugins["database"]).(*pgxpool.Pool) -// } - -// func (s *Server) getEventbus() *amqp.Channel { -// return (s.plugins["eventbus"]).(*amqp.Channel) -// } - // func GetRequestID(c *fiber.Ctx) (string, error) { // var hdr = new(HeaderRequestID) // if err := c.ReqHeaderParser(hdr); err != nil { @@ -112,11 +96,11 @@ func (s *Server) setupRouter() { s.Options("*", defaultCORS) s.Use(defaultCORS) - s.Get("/health", http.HealthHandlerFn(s.getDatabase(), s.getEventbus(), s.getCache())) + s.Get("/health", http.HealthHandlerFn(s.GetDatabase(), s.GetEventbus(), s.GetCache())) s.Group("/v1"). - Get("/product", http.ListProductsHandlerFn(s.getDatabase())). - Get("/product/:id", http.ShowProductHandlerFn(s.getDatabase())) + Get("/product", http.ListProductsHandlerFn(s.GetDatabase())). + Get("/product/:id", http.ShowProductHandlerFn(s.GetDatabase())) // Post("/product", http.AddProductHandlerFn(s.getDatabase(), s.getCache())) // Delete("/product", http.RemoveProductFromBasketHandlerFn(s.getDatabase(), s.getCache())) } diff --git a/src/app/worker.go b/src/app/worker.go index 4420cfb..a4aa49c 100644 --- a/src/app/worker.go +++ b/src/app/worker.go @@ -28,19 +28,28 @@ func NewWorker(c *Config) *Worker { } } -func (bus *Worker) Start() error { - bus.consumeCommands(bus.getEventbus()) +func (w *Worker) Start() error { + // fmt.Printf("eventbus: %#v", w.GetEventbus()) + chn := w.GetEventbus() + // chn, err := conn.Channel() + // if err != nil { + // log.Println("failed to open eventbus channel: ", err.Error()) + // } + + if !chn.IsClosed() { + w.consumeCommands(chn) + } return nil } -func (bus *Worker) OnShutdown() { - bus.getEventbus().Close() +func (w *Worker) OnShutdown() { + w.GetEventbus().Close() } -func (bus *Worker) consumeCommands(ch *amqp.Channel) error { +func (w *Worker) consumeCommands(ch *amqp.Channel) error { msgs, err := ch.Consume( - bus.queueName, + w.queueName, "", false, false, diff --git a/src/cmd/scheduler/main.go b/src/cmd/scheduler/main.go index 20cfa11..ccb4abe 100644 --- a/src/cmd/scheduler/main.go +++ b/src/cmd/scheduler/main.go @@ -25,7 +25,7 @@ func main() { <-while if err != nil { - log.Fatalf("Failed to run scheduler. Reason: %v\n", err) + log.Fatalf("failed to run scheduler. Reason: %v\n", err) os.Exit(1) } diff --git a/src/cmd/server/main.go b/src/cmd/server/main.go index 5620cf6..a2f008a 100644 --- a/src/cmd/server/main.go +++ b/src/cmd/server/main.go @@ -25,8 +25,7 @@ func main() { <-while if err != nil { - log.Fatalf("Failed to start server. Reason: %v\n", err) - os.Exit(1) + log.Fatalf("failed to start server. reason: %v\n", err) } os.Exit(0) diff --git a/src/cmd/worker/main.go b/src/cmd/worker/main.go index 82e0c47..25a8177 100644 --- a/src/cmd/worker/main.go +++ b/src/cmd/worker/main.go @@ -11,12 +11,11 @@ import ( func main() { if cnf.ErrLoadingEnvs != nil { - log.Fatalln("Error loading .env file.") + log.Fatalln("error loading .env file.") } cnf := app.NewConfig("catalog-worker") - bus := app.NewWorker(cnf) - a := app.NewApp(bus) + a := app.NewApp(app.NewWorker(cnf)) a.RegisterPlugin(app.CachePlugin(cnf)) a.RegisterPlugin(app.DatabasePlugin(cnf)) a.RegisterPlugin(app.EventbusPlugin(cnf)) @@ -26,7 +25,7 @@ func main() { <-while if err != nil { - log.Fatalf("Failed to start worker. Reason: %v\n", err) + log.Fatalf("failed to start worker. Reason: %v\n", err) os.Exit(1) } diff --git a/src/internal/http/health_handler.go b/src/internal/http/health_handler.go index 5b61b06..fedc4a9 100644 --- a/src/internal/http/health_handler.go +++ b/src/internal/http/health_handler.go @@ -2,7 +2,6 @@ package http import ( "context" - "net/http" redis "github.com/go-redis/redis/v8" "github.com/gofiber/fiber/v2" @@ -18,15 +17,15 @@ func HealthHandlerFn(db *pgxpool.Pool, bus *amqp.Channel, cache *redis.Client) f return func(c *fiber.Ctx) error { // Only 404 indicate service as not-healthy if err := db.Ping(context.Background()); err != nil { - return c.SendStatus(http.StatusNotFound) + return c.SendStatus(fiber.StatusNotFound) } if closed := bus.IsClosed(); closed { - return c.SendStatus(http.StatusNotFound) + return c.SendStatus(fiber.StatusNotFound) } if err := cache.Ping(context.Background()).Err(); err != nil { - return c.SendStatus(http.StatusNotFound) + return c.SendStatus(fiber.StatusNotFound) } return c.JSON(&HealthResponse{ diff --git a/src/internal/http/list_products_handler.go b/src/internal/http/list_products_handler.go index c35a55d..15c1af9 100644 --- a/src/internal/http/list_products_handler.go +++ b/src/internal/http/list_products_handler.go @@ -46,7 +46,7 @@ func ListProductsHandlerFn(db *pgxpool.Pool) fiber.Handler { // if res.ProductID == 0 { // return s.Error(c, fiber.StatusNotFound, fmt.Sprintf("Product #%d not exists", req.ProductID)) // } -// return s.Error(c, fiber.StatusBadRequest, "Failed to add product to the basket") +// return s.Error(c, fiber.StatusBadRequest, "failed to add product to the basket") // } // return c.JSON(res)