├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .run ├── Run Food Delivery App.run.xml └── Run Tests.run.xml ├── README.md ├── Taskfile.yml ├── cmd └── api │ ├── main.go │ └── middleware │ └── middleware.go ├── go.mod ├── go.sum ├── httpclient ├── FoodDelivery.http └── FoodDelivery.postman_collection.json ├── misc └── images │ ├── background.png │ ├── go_run_config.png │ └── httpclient.png └── pkg ├── abstract ├── announcements │ └── events.go ├── cart │ ├── cart.go │ └── order.go ├── delivery │ └── delivery.go ├── restaurant │ ├── menu.go │ └── restaurant.go ├── review │ └── review.go └── user │ └── user.go ├── database ├── database.go └── models │ ├── cart │ └── cart.go │ ├── delivery │ └── delivery.go │ ├── order │ └── order.go │ ├── restaurant │ └── restaurant.go │ ├── review │ └── review.go │ ├── user │ └── user.go │ └── utils │ └── timestamp.go ├── handler ├── annoucements │ ├── events.go │ ├── routes.go │ └── service.go ├── cart │ ├── cart.go │ ├── order.go │ ├── routes.go │ └── service.go ├── delivery │ ├── delivery.go │ ├── login.go │ ├── order.go │ ├── routes.go │ └── service.go ├── notification │ ├── notify.go │ ├── routes.go │ └── service.go ├── restaurant │ ├── menu.go │ ├── restaurant.go │ ├── routes.go │ ├── service.go │ └── utils.go ├── review │ ├── review.go │ ├── routes.go │ └── service.go ├── server.go └── user │ ├── routes.go │ ├── service.go │ └── user.go ├── nats └── nats_server.go ├── service ├── announcements │ ├── announcement.go │ ├── events.json │ └── service.go ├── cart_order │ ├── add_item_to_cart.go │ ├── create_cart.go │ ├── delete_item_from_cart.go │ ├── delivery_info.go │ ├── get_cart.go │ ├── get_items_in_cart.go │ ├── order_list.go │ ├── place_order.go │ └── service.go ├── delivery │ ├── 2fa.go │ ├── add_delivery_person.go │ ├── deliveries_listing.go │ ├── order_placement.go │ ├── service.go │ └── validation.go ├── notification │ └── service.go ├── restaurant │ ├── add_menu.go │ ├── add_restaurant.go │ ├── delete_menu.go │ ├── delete_restaurant.go │ ├── list_menus.go │ ├── list_restaurant_by_id.go │ ├── list_restaurants.go │ ├── service.go │ └── unsplash │ │ ├── download.go │ │ ├── image.go │ │ └── unsplash.go ├── review │ ├── add_review.go │ ├── delete_review.go │ ├── list_reviews.go │ └── service.go └── user │ ├── add_user.go │ ├── delete_user.go │ ├── login_user.go │ ├── service.go │ └── utils.go ├── storage ├── file.go ├── local.go └── utils.go └── tests ├── cart └── cart_test.go ├── common ├── unsplash_download_test.go └── unsplash_test.go ├── database └── database_test.go ├── delivery └── delivery_test.go ├── restaurant ├── common.go ├── restaurant_menu_test.go ├── restaurant_test.go └── review_test.go ├── setup.go └── user └── user_test.go /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | name: Run Tests 10 | services: 11 | docker: 12 | image: docker:27.1.1 13 | options: --privileged 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. 20 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. 21 | 22 | - name: Setup go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: '1.23' 26 | 27 | - name: Run Test 28 | run: | 29 | go test -v ./... -covermode=count -coverprofile=coverage.out 30 | go tool cover -func=coverage.out -o=coverage.out 31 | 32 | - name: Go Coverage Badge # Pass the `coverage.out` output to this action 33 | uses: tj-actions/coverage-badge-go@v2 34 | with: 35 | filename: coverage.out 36 | 37 | - name: Verify Changed files 38 | uses: tj-actions/verify-changed-files@v20 39 | id: verify-changed-files 40 | with: 41 | files: README.md 42 | 43 | - name: Commit changes 44 | if: steps.verify-changed-files.outputs.files_changed == 'true' 45 | run: | 46 | git config --local user.email "action@github.com" 47 | git config --local user.name "GitHub Action" 48 | git add README.md 49 | git commit -m "chore: Updated coverage badge." 50 | 51 | - name: Push changes 52 | if: steps.verify-changed-files.outputs.files_changed == 'true' 53 | uses: ad-m/github-push-action@master 54 | with: 55 | github_token: ${{ github.token }} 56 | branch: ${{ github.head_ref }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | -------------------------------------------------------------------------------- /.run/Run Food Delivery App.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Food Delivery 2 | 3 | ![Coverage](https://img.shields.io/badge/Coverage-50.4%25-yellow) 4 | ![Workflow](https://github.com/mukulmantosh/Go_Food_Delivery/actions/workflows/test.yaml/badge.svg) 5 | 6 | ![background](./misc/images/background.png) 7 | 8 | 9 | ### Prerequisites 10 | 11 | Before starting up this project, make sure you have the necessary dependencies installed in your machine. 12 | 13 | ### Installation 14 | 15 | - [x] [Go](https://go.dev/) - Go is an open source programming language that makes it simple to build secure, scalable systems. 16 | 17 | - [x] [Docker](https://www.docker.com/) - Docker helps developers bring their ideas to life by conquering the complexity of app development. 18 | 19 | - [x] [PostgreSQL](https://www.postgresql.org/) - The World's Most Advanced Open Source Relational Database 20 | 21 | - [x] [NATS](https://nats.io/) - NATS is an open-source messaging system. The NATS server is written in the Go programming language 22 | 23 | 24 | 25 | #### Running Postgres Database 26 | 27 | ```bash 28 | docker run --name food-delivery -p 5432:5432 -e POSTGRES_PASSWORD=****** -d postgres 29 | ``` 30 | 31 | #### Running NATS 32 | 33 | ```bash 34 | docker network create nats 35 | docker run --name nats -d --network nats --rm -p 4222:4222 -p 8222:8222 nats --http_port 8222 --cluster_name NATS --cluster nats://0.0.0.0:6222 36 | ``` 37 | 38 | 39 | ### Environment Variables 40 | 41 | Be sure to place the `.env` file in the project root and update the information according to your settings. Refer to the example below. 42 | 43 | ``` 44 | APP_ENV=dev 45 | DB_HOST=localhost 46 | DB_USERNAME=postgres 47 | DB_PASSWORD=************* 48 | DB_NAME=food_delivery 49 | DB_PORT=5432 50 | STORAGE_TYPE=local 51 | STORAGE_DIRECTORY=uploads 52 | LOCAL_STORAGE_PATH=C:\Users\win10\GolandProjects\Go_Food_Delivery\uploads 53 | UNSPLASH_API_KEY=******************* 54 | JWT_SECRET_KEY=******************** 55 | PASSWORD_SALT=******************** 56 | ``` 57 | 58 | ### External APIs 59 | 60 | We are using [UnSplash](https://unsplash.com/) to generate images. So, you need to have an API key to work with the application. 61 | 62 | 63 | ### Frontend (UI) 64 | 65 | To configure the application's frontend UI, be sure to follow the instructions in this [repository][repo]. 66 | 67 | 68 | [repo]: https://github.com/mukulmantosh/food_delivery_frontend 69 | 70 | ### HTTP Client 71 | 72 | We have covered the APIs which you can directly test out from the IDE. 73 | 74 | ![httpclient](./misc/images/httpclient.png) 75 | 76 | ### Get Set Go 🚀 77 | 78 | Once you’re ready, clone this repo in [GoLand,](https://www.jetbrains.com/go/) and you're good to go. 79 | 80 | ![go_run_config](./misc/images/go_run_config.png) 81 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | tasks: 3 | build: 4 | desc: Build Application 5 | cmds: 6 | - go build -o food_delivery ./cmd/api 7 | run: 8 | desc: Run Application 9 | cmds: 10 | - go run ./cmd/api 11 | test: 12 | desc: Run Tests 13 | cmds: 14 | - go test -v ./... 15 | all: 16 | desc: Build & Test 17 | deps: 18 | - build 19 | - test -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | "Go_Food_Delivery/pkg/database" 6 | "Go_Food_Delivery/pkg/handler" 7 | "Go_Food_Delivery/pkg/handler/annoucements" 8 | crt "Go_Food_Delivery/pkg/handler/cart" 9 | delv "Go_Food_Delivery/pkg/handler/delivery" 10 | notify "Go_Food_Delivery/pkg/handler/notification" 11 | "Go_Food_Delivery/pkg/handler/restaurant" 12 | revw "Go_Food_Delivery/pkg/handler/review" 13 | "Go_Food_Delivery/pkg/handler/user" 14 | "Go_Food_Delivery/pkg/nats" 15 | "Go_Food_Delivery/pkg/service/announcements" 16 | "Go_Food_Delivery/pkg/service/cart_order" 17 | "Go_Food_Delivery/pkg/service/delivery" 18 | "Go_Food_Delivery/pkg/service/notification" 19 | restro "Go_Food_Delivery/pkg/service/restaurant" 20 | "Go_Food_Delivery/pkg/service/review" 21 | usr "Go_Food_Delivery/pkg/service/user" 22 | "github.com/gin-gonic/gin" 23 | "github.com/go-playground/validator/v10" 24 | "github.com/gorilla/websocket" 25 | "github.com/joho/godotenv" 26 | "log" 27 | "os" 28 | ) 29 | 30 | func main() { 31 | // load .env file 32 | err := godotenv.Load() 33 | if err != nil { 34 | log.Fatal("Error loading .env file") 35 | } 36 | 37 | env := os.Getenv("APP_ENV") 38 | db := database.New() 39 | // Create Tables 40 | if err := db.Migrate(); err != nil { 41 | log.Fatalf("Error migrating database: %s", err) 42 | } 43 | 44 | // Connect NATS 45 | natServer, err := nats.NewNATS("nats://127.0.0.1:4222") 46 | 47 | // WebSocket Clients 48 | wsClients := make(map[string]*websocket.Conn) 49 | 50 | s := handler.NewServer(db, true) 51 | 52 | // Initialize Validator 53 | validate := validator.New() 54 | 55 | // Middlewares List 56 | middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} 57 | 58 | // User 59 | userService := usr.NewUserService(db, env) 60 | user.NewUserHandler(s, "/user", userService, validate) 61 | 62 | // Restaurant 63 | restaurantService := restro.NewRestaurantService(db, env) 64 | restaurant.NewRestaurantHandler(s, "/restaurant", restaurantService) 65 | 66 | // Reviews 67 | reviewService := review.NewReviewService(db, env) 68 | revw.NewReviewProtectedHandler(s, "/review", reviewService, middlewares, validate) 69 | 70 | // Cart 71 | cartService := cart_order.NewCartService(db, env, natServer) 72 | crt.NewCartHandler(s, "/cart", cartService, middlewares, validate) 73 | 74 | // Delivery 75 | deliveryService := delivery.NewDeliveryService(db, env, natServer) 76 | delv.NewDeliveryHandler(s, "/delivery", deliveryService, middlewares, validate) 77 | 78 | // Notification 79 | notifyService := notification.NewNotificationService(db, env, natServer) 80 | 81 | // Subscribe to multiple events. 82 | _ = notifyService.SubscribeNewOrders(wsClients) 83 | _ = notifyService.SubscribeOrderStatus(wsClients) 84 | 85 | notify.NewNotifyHandler(s, "/notify", notifyService, middlewares, validate, wsClients) 86 | 87 | // Events/Announcements 88 | announceService := announcements.NewAnnouncementService(db, env) 89 | annoucements.NewAnnouncementHandler(s, "/announcements", announceService, middlewares, validate) 90 | log.Fatal(s.Run()) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /cmd/api/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/golang-jwt/jwt/v5" 6 | "log" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | type UserClaims struct { 14 | UserID int64 `json:"user_id"` 15 | Name string `json:"name"` 16 | jwt.RegisteredClaims 17 | } 18 | 19 | func ValidateToken(token string) (bool, int64) { 20 | 21 | tokenInfo, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { 22 | return []byte(os.Getenv("JWT_SECRET_KEY")), nil 23 | }) 24 | if err != nil { 25 | slog.Error("AuthMiddleware", "validateToken", err.Error()) 26 | return false, 0 27 | } else if claims, ok := tokenInfo.Claims.(*UserClaims); ok { 28 | return true, claims.UserID 29 | } else { 30 | log.Fatal("unknown claims type, cannot proceed") 31 | return false, 0 32 | } 33 | } 34 | 35 | func AuthMiddleware() gin.HandlerFunc { 36 | return func(c *gin.Context) { 37 | authHeader := c.GetHeader("Authorization") 38 | if authHeader == "" { 39 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization Header!!"}) 40 | c.Abort() 41 | return 42 | } 43 | 44 | // Extract Token 45 | tokenParts := strings.Split(authHeader, " ") 46 | if len(tokenParts) != 2 || tokenParts[0] != "Bearer" { 47 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization"}) 48 | c.Abort() 49 | return 50 | } 51 | token := tokenParts[1] 52 | 53 | tokenValidation, userID := ValidateToken(token) 54 | if !tokenValidation { 55 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Token"}) 56 | c.Abort() 57 | return 58 | } 59 | 60 | c.Set("userID", userID) 61 | 62 | c.Next() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module Go_Food_Delivery 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/docker/docker v27.3.1+incompatible 7 | github.com/docker/go-connections v0.5.0 8 | github.com/gin-contrib/cors v1.7.2 9 | github.com/gin-gonic/gin v1.10.0 10 | github.com/go-faker/faker/v4 v4.4.2 11 | github.com/go-playground/validator/v10 v10.22.0 12 | github.com/golang-jwt/jwt/v5 v5.2.1 13 | github.com/gorilla/websocket v1.5.3 14 | github.com/joho/godotenv v1.5.1 15 | github.com/nats-io/nats.go v1.37.0 16 | github.com/pquerna/otp v1.4.0 17 | github.com/samber/slog-gin v1.13.3 18 | github.com/stretchr/testify v1.9.0 19 | github.com/testcontainers/testcontainers-go v0.33.0 20 | github.com/testcontainers/testcontainers-go/modules/nats v0.33.0 21 | github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0 22 | github.com/uptrace/bun v1.2.1 23 | github.com/uptrace/bun/dialect/pgdialect v1.2.1 24 | github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 25 | github.com/uptrace/bun/driver/pgdriver v1.2.1 26 | github.com/uptrace/bun/driver/sqliteshim v1.2.1 27 | golang.org/x/crypto v0.25.0 28 | ) 29 | 30 | require ( 31 | dario.cat/mergo v1.0.0 // indirect 32 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect 33 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 34 | github.com/Microsoft/go-winio v0.6.2 // indirect 35 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 36 | github.com/bytedance/sonic v1.12.0 // indirect 37 | github.com/bytedance/sonic/loader v0.2.0 // indirect 38 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 39 | github.com/cloudwego/base64x v0.1.4 // indirect 40 | github.com/cloudwego/iasm v0.2.0 // indirect 41 | github.com/containerd/log v0.1.0 // indirect 42 | github.com/containerd/platforms v0.2.1 // indirect 43 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 44 | github.com/davecgh/go-spew v1.1.1 // indirect 45 | github.com/distribution/reference v0.6.0 // indirect 46 | github.com/docker/go-units v0.5.0 // indirect 47 | github.com/dustin/go-humanize v1.0.1 // indirect 48 | github.com/felixge/httpsnoop v1.0.4 // indirect 49 | github.com/gabriel-vasile/mimetype v1.4.5 // indirect 50 | github.com/gin-contrib/sse v0.1.0 // indirect 51 | github.com/go-logr/logr v1.4.1 // indirect 52 | github.com/go-logr/stdr v1.2.2 // indirect 53 | github.com/go-ole/go-ole v1.2.6 // indirect 54 | github.com/go-playground/locales v0.14.1 // indirect 55 | github.com/go-playground/universal-translator v0.18.1 // indirect 56 | github.com/goccy/go-json v0.10.3 // indirect 57 | github.com/gogo/protobuf v1.3.2 // indirect 58 | github.com/google/uuid v1.6.0 // indirect 59 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 60 | github.com/jinzhu/inflection v1.0.0 // indirect 61 | github.com/json-iterator/go v1.1.12 // indirect 62 | github.com/klauspost/compress v1.17.4 // indirect 63 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 64 | github.com/leodido/go-urn v1.4.0 // indirect 65 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 66 | github.com/magiconair/properties v1.8.7 // indirect 67 | github.com/mattn/go-isatty v0.0.20 // indirect 68 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 69 | github.com/moby/docker-image-spec v1.3.1 // indirect 70 | github.com/moby/patternmatcher v0.6.0 // indirect 71 | github.com/moby/sys/sequential v0.5.0 // indirect 72 | github.com/moby/sys/user v0.1.0 // indirect 73 | github.com/moby/sys/userns v0.1.0 // indirect 74 | github.com/moby/term v0.5.0 // indirect 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 76 | github.com/modern-go/reflect2 v1.0.2 // indirect 77 | github.com/morikuni/aec v1.0.0 // indirect 78 | github.com/nats-io/nkeys v0.4.7 // indirect 79 | github.com/nats-io/nuid v1.0.1 // indirect 80 | github.com/ncruces/go-strftime v0.1.9 // indirect 81 | github.com/opencontainers/go-digest v1.0.0 // indirect 82 | github.com/opencontainers/image-spec v1.1.0 // indirect 83 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 84 | github.com/pkg/errors v0.9.1 // indirect 85 | github.com/pmezard/go-difflib v1.0.0 // indirect 86 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 87 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 88 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 89 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 90 | github.com/sirupsen/logrus v1.9.3 // indirect 91 | github.com/tklauser/go-sysconf v0.3.12 // indirect 92 | github.com/tklauser/numcpus v0.6.1 // indirect 93 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 94 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 95 | github.com/ugorji/go/codec v1.2.12 // indirect 96 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 97 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 98 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 99 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 100 | go.opentelemetry.io/otel v1.24.0 // indirect 101 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect 102 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 103 | go.opentelemetry.io/otel/sdk v1.19.0 // indirect 104 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 105 | golang.org/x/arch v0.8.0 // indirect 106 | golang.org/x/net v0.27.0 // indirect 107 | golang.org/x/sys v0.22.0 // indirect 108 | golang.org/x/text v0.16.0 // indirect 109 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 110 | google.golang.org/protobuf v1.34.2 // indirect 111 | gopkg.in/yaml.v3 v3.0.1 // indirect 112 | mellium.im/sasl v0.3.1 // indirect 113 | modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect 114 | modernc.org/libc v1.49.0 // indirect 115 | modernc.org/mathutil v1.6.0 // indirect 116 | modernc.org/memory v1.7.2 // indirect 117 | modernc.org/sqlite v1.29.5 // indirect 118 | modernc.org/strutil v1.2.0 // indirect 119 | modernc.org/token v1.1.0 // indirect 120 | ) 121 | -------------------------------------------------------------------------------- /httpclient/FoodDelivery.http: -------------------------------------------------------------------------------- 1 | # FoodDelivery 2 | @HTTP_URL = http://localhost:8080 3 | @USER_TOKEN = "************" 4 | @DELIVERY_PERSON_TOKEN = "**************" 5 | 6 | ### 7 | # group: User 8 | # @name Create a new User 9 | POST {{HTTP_URL}}/user/ 10 | Content-Type: application/json 11 | 12 | { 13 | "name": "sample1", 14 | "email": "sample@yahoo.com", 15 | "password": "sample123" 16 | } 17 | 18 | 19 | ### 20 | # group: User 21 | # @name Delete User 22 | DELETE {{HTTP_URL}}/user/15 23 | 24 | ### 25 | # group: User 26 | # @name Login User 27 | POST {{HTTP_URL}}/user/login 28 | Content-Type: application/json 29 | 30 | { 31 | "email": "sample@yahoo.com", 32 | "password": "sample123" 33 | } 34 | 35 | 36 | ### 37 | # group: Restaurant 38 | # @name Create a new Restaurant 39 | POST {{HTTP_URL}}/restaurant 40 | Content-Type: multipart/form-data; boundary=WebAppBoundary 41 | 42 | --WebAppBoundary 43 | Content-Disposition: form-data; name="name" 44 | 45 | Starbucks (Park Row at Beekman St) 46 | --WebAppBoundary 47 | Content-Disposition: form-data; name="description" 48 | 49 | Starbucks (Park Row at Beekman St) in the City Hall area of Manhattan is a popular destination for coffee and tea enthusiasts. This location offers a variety of cold and hot beverages, including Iced Caffè Latte, Starbucks® Cold Brew Coffee, and Cappuccino, which are especially favored by patrons. Visitors also enjoy a range of Frappuccino® blended beverages and have a selection of bakery items and lunch options to choose from. Commonly ordered together are the Featured Medium Roast Pike Place® Roast and Starbucks® Cold Brew Coffee. The establishment holds a customer rating of 4.2, making it a well-regarded spot in its neighborhood. 50 | --WebAppBoundary 51 | Content-Disposition: form-data; name="address" 52 | 53 | 38 Park Row 54 | --WebAppBoundary 55 | Content-Disposition: form-data; name="city" 56 | 57 | New York 58 | --WebAppBoundary 59 | Content-Disposition: form-data; name="state" 60 | 61 | NY 62 | --WebAppBoundary 63 | Content-Disposition: form-data; name="file"; filename="starbucks.jpg" 64 | 65 | < /C:/Users/win10/Downloads/starbucks.jpg 66 | --WebAppBoundary 67 | 68 | ### 69 | # group: Restaurant 70 | # @name Create Menu Item 71 | POST {{HTTP_URL}}/restaurant/menu 72 | Content-Type: application/json 73 | 74 | { 75 | "restaurant_id": 1, 76 | "name": "Caffe Americano", 77 | "description": "Espresso shots topped with hot water create a light layer of crema culminating in this wonderfully rich cup with depth and nuance.", 78 | "price": 5.25, 79 | "category": "BEVERAGES", 80 | "available": true 81 | } 82 | 83 | ### 84 | # group: Restaurant 85 | # @name List Menus 86 | GET {{HTTP_URL}}/restaurant/menu 87 | 88 | ### 89 | # group: Restaurant 90 | # @name List all restaurants 91 | GET {{HTTP_URL}}/restaurant 92 | 93 | ### 94 | # group: Restaurant 95 | # @name List restaurant by ID 96 | GET {{HTTP_URL}}/restaurant/1 97 | 98 | ### 99 | # group: Restaurant 100 | # @name Delete Restaurant 101 | DELETE {{HTTP_URL}}/restaurant/1 102 | 103 | ### 104 | # group: Restaurant 105 | # @name Delete Menu 106 | DELETE {{HTTP_URL}}/restaurant/menu/2/4 107 | 108 | ### 109 | # group: Reviews 110 | # @name New Review 111 | POST {{HTTP_URL}}/review/1 112 | Authorization: Bearer {{USER_TOKEN}} 113 | Content-Type: application/json 114 | 115 | { 116 | "rating": 4, 117 | "comment": "cool!" 118 | } 119 | 120 | ### 121 | # group: Reviews 122 | # @name List Reviews 123 | GET {{HTTP_URL}}/review/1 124 | Authorization: Bearer {{USER_TOKEN}} 125 | 126 | ### 127 | @REVIEW_ID = 1 128 | # group: Reviews 129 | # @name Delete Review 130 | DELETE {{HTTP_URL}}/review/{{REVIEW_ID}} 131 | Authorization: Bearer {{USER_TOKEN}} 132 | 133 | ### 134 | # group: Cart 135 | # @name Add Item to Cart 136 | POST {{HTTP_URL}}/cart/add 137 | Authorization: Bearer {{USER_TOKEN}} 138 | Content-Type: application/json 139 | 140 | { 141 | "item_id": 1, 142 | "restaurant_id": 1, 143 | "quantity": 1 144 | } 145 | 146 | ### 147 | # group: Cart 148 | # @name Lists Cart Items 149 | GET {{HTTP_URL}}/cart/list 150 | Authorization: Bearer {{USER_TOKEN}} 151 | 152 | ### 153 | # group: Cart 154 | # @name Remove Item from Cart 155 | DELETE {{HTTP_URL}}/cart/remove/2 156 | Authorization: Bearer {{USER_TOKEN}} 157 | 158 | ### 159 | # group: Cart 160 | # @name Place a new order 161 | POST {{HTTP_URL}}/cart/order/new 162 | Authorization: Bearer {{USER_TOKEN}} 163 | 164 | ### 165 | # group: DeliveryPerson 166 | # @name Add a new delivery person 167 | POST {{HTTP_URL}}/delivery/add 168 | Content-Type: application/json 169 | 170 | { 171 | "name": "John Wick", 172 | "phone": "78784512458", 173 | "vehicle_details": "OX-25895-8547" 174 | } 175 | 176 | ### 177 | # group: DeliveryPerson 178 | # @name Login as DeliveryPerson 179 | 180 | # To generate an OTP, check the delivery_person table 181 | # for the auth_key column. You'll need to register this key 182 | # in Google Authenticator to create the OTP. 183 | 184 | POST {{HTTP_URL}}/delivery/login 185 | Content-Type: application/json 186 | 187 | { 188 | "phone": "78784512458", 189 | "otp": "614550" 190 | } 191 | 192 | ### 193 | # group: DeliveryPerson 194 | # @name Get all delivery order lists 195 | GET {{HTTP_URL}}/delivery/deliveries/87 196 | Authorization: Bearer {{DELIVERY_PERSON_TOKEN}} 197 | 198 | ### 199 | # group: DeliveryPerson 200 | # @name Update Order Status 201 | POST {{HTTP_URL}}/delivery/update-order 202 | Authorization: Bearer {{DELIVERY_PERSON_TOKEN}} 203 | Content-Type: application/json 204 | 205 | { 206 | "order_id": 108, 207 | "status": "on_the_way" 208 | } 209 | 210 | ### 211 | # group: Announcements 212 | # @name Flash Events 213 | GET {{HTTP_URL}}/announcements/events -------------------------------------------------------------------------------- /httpclient/FoodDelivery.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "270f76d7-6a0e-4b12-ac79-c5951b3d2fca", 4 | "name": "FoodDelivery", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "5196778" 7 | }, 8 | "item": [ 9 | { 10 | "name": "User", 11 | "item": [ 12 | { 13 | "name": "Create a new User", 14 | "request": { 15 | "method": "POST", 16 | "header": [], 17 | "body": { 18 | "mode": "raw", 19 | "raw": "{\r\n \"name\": \"sample1\",\r\n \"email\": \"sample@yahoo.com\",\r\n \"password\": \"sample123\"\r\n}\r\n", 20 | "options": { 21 | "raw": { 22 | "language": "json" 23 | } 24 | } 25 | }, 26 | "url": { 27 | "raw": "{{HTTP_URL}}/user/", 28 | "host": [ 29 | "{{HTTP_URL}}" 30 | ], 31 | "path": [ 32 | "user", 33 | "" 34 | ] 35 | } 36 | }, 37 | "response": [] 38 | }, 39 | { 40 | "name": "Delete User", 41 | "request": { 42 | "method": "DELETE", 43 | "header": [], 44 | "url": { 45 | "raw": "{{HTTP_URL}}/user/15", 46 | "host": [ 47 | "{{HTTP_URL}}" 48 | ], 49 | "path": [ 50 | "user", 51 | "15" 52 | ] 53 | } 54 | }, 55 | "response": [] 56 | }, 57 | { 58 | "name": "Login User", 59 | "request": { 60 | "method": "POST", 61 | "header": [], 62 | "body": { 63 | "mode": "raw", 64 | "raw": "{\r\n \"email\": \"sample@yahoo.com\",\r\n \"password\": \"sample123\"\r\n}\r\n", 65 | "options": { 66 | "raw": { 67 | "language": "json" 68 | } 69 | } 70 | }, 71 | "url": { 72 | "raw": "{{HTTP_URL}}/user/login", 73 | "host": [ 74 | "{{HTTP_URL}}" 75 | ], 76 | "path": [ 77 | "user", 78 | "login" 79 | ] 80 | } 81 | }, 82 | "response": [] 83 | } 84 | ] 85 | }, 86 | { 87 | "name": "Restaurant", 88 | "item": [ 89 | { 90 | "name": "Create a new Restaurant", 91 | "request": { 92 | "method": "POST", 93 | "header": [], 94 | "body": { 95 | "mode": "formdata", 96 | "formdata": [ 97 | { 98 | "key": "name", 99 | "value": "Starbucks (Park Row at Beekman St)", 100 | "type": "text" 101 | }, 102 | { 103 | "key": "description", 104 | "value": "Starbucks (Park Row at Beekman St) in the City Hall area of Manhattan is a popular destination for coffee and tea enthusiasts. This location offers a variety of cold and hot beverages, including Iced Caffè Latte, Starbucks® Cold Brew Coffee, and Cappuccino, which are especially favored by patrons. Visitors also enjoy a range of Frappuccino® blended beverages and have a selection of bakery items and lunch options to choose from. Commonly ordered together are the Featured Medium Roast Pike Place® Roast and Starbucks® Cold Brew Coffee. The establishment holds a customer rating of 4.2, making it a well-regarded spot in its neighborhood.", 105 | "type": "text" 106 | }, 107 | { 108 | "key": "address", 109 | "value": "38 Park Row", 110 | "type": "text" 111 | }, 112 | { 113 | "key": "city", 114 | "value": "New York", 115 | "type": "text" 116 | }, 117 | { 118 | "key": "state", 119 | "value": "NY", 120 | "type": "text" 121 | }, 122 | { 123 | "key": "file", 124 | "type": "file", 125 | "src": "/C:/Users/win10/Downloads/starbucks.jpg" 126 | } 127 | ] 128 | }, 129 | "url": { 130 | "raw": "{{HTTP_URL}}/restaurant", 131 | "host": [ 132 | "{{HTTP_URL}}" 133 | ], 134 | "path": [ 135 | "restaurant" 136 | ] 137 | } 138 | }, 139 | "response": [] 140 | }, 141 | { 142 | "name": "Create Menu Item", 143 | "request": { 144 | "method": "POST", 145 | "header": [], 146 | "body": { 147 | "mode": "raw", 148 | "raw": "{\r\n \"restaurant_id\": 1,\r\n \"name\": \"Caffe Americano\",\r\n \"description\": \"Espresso shots topped with hot water create a light layer of crema culminating in this wonderfully rich cup with depth and nuance.\",\r\n \"price\": 5.25,\r\n \"category\": \"BEVERAGES\",\r\n \"available\": true\r\n}", 149 | "options": { 150 | "raw": { 151 | "language": "json" 152 | } 153 | } 154 | }, 155 | "url": { 156 | "raw": "{{HTTP_URL}}/restaurant/menu", 157 | "host": [ 158 | "{{HTTP_URL}}" 159 | ], 160 | "path": [ 161 | "restaurant", 162 | "menu" 163 | ] 164 | } 165 | }, 166 | "response": [] 167 | }, 168 | { 169 | "name": "List Menus", 170 | "request": { 171 | "method": "GET", 172 | "header": [], 173 | "url": { 174 | "raw": "{{HTTP_URL}}/restaurant/menu", 175 | "host": [ 176 | "{{HTTP_URL}}" 177 | ], 178 | "path": [ 179 | "restaurant", 180 | "menu" 181 | ] 182 | } 183 | }, 184 | "response": [] 185 | }, 186 | { 187 | "name": "List all restaurants", 188 | "request": { 189 | "method": "GET", 190 | "header": [], 191 | "url": { 192 | "raw": "{{HTTP_URL}}/restaurant", 193 | "host": [ 194 | "{{HTTP_URL}}" 195 | ], 196 | "path": [ 197 | "restaurant" 198 | ] 199 | } 200 | }, 201 | "response": [] 202 | }, 203 | { 204 | "name": "List restaurant by ID", 205 | "request": { 206 | "method": "GET", 207 | "header": [], 208 | "url": { 209 | "raw": "{{HTTP_URL}}/restaurant/1", 210 | "host": [ 211 | "{{HTTP_URL}}" 212 | ], 213 | "path": [ 214 | "restaurant", 215 | "1" 216 | ] 217 | } 218 | }, 219 | "response": [] 220 | }, 221 | { 222 | "name": "Delete Restaurant", 223 | "request": { 224 | "method": "DELETE", 225 | "header": [], 226 | "url": { 227 | "raw": "{{HTTP_URL}}/restaurant/1", 228 | "host": [ 229 | "{{HTTP_URL}}" 230 | ], 231 | "path": [ 232 | "restaurant", 233 | "1" 234 | ] 235 | } 236 | }, 237 | "response": [] 238 | }, 239 | { 240 | "name": "Delete Menu", 241 | "request": { 242 | "method": "DELETE", 243 | "header": [], 244 | "url": { 245 | "raw": "{{HTTP_URL}}/restaurant/menu/2/4", 246 | "host": [ 247 | "{{HTTP_URL}}" 248 | ], 249 | "path": [ 250 | "restaurant", 251 | "menu", 252 | "2", 253 | "4" 254 | ] 255 | } 256 | }, 257 | "response": [] 258 | } 259 | ] 260 | }, 261 | { 262 | "name": "Reviews", 263 | "item": [ 264 | { 265 | "name": "New Review", 266 | "request": { 267 | "method": "POST", 268 | "header": [ 269 | { 270 | "key": "Authorization", 271 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJpc3MiOiJHb19Gb29kX0RlbGl2ZXJ5IiwiZXhwIjoxNzIzNTQ3NDI4fQ.q45k3qv3gfBV2Rm-5SODYFgCnmF74oJIpDza2mVPJhM", 272 | "type": "text" 273 | } 274 | ], 275 | "body": { 276 | "mode": "raw", 277 | "raw": "{\r\n \"rating\": 4,\r\n \"comment\": \"cool!\"\r\n}", 278 | "options": { 279 | "raw": { 280 | "language": "json" 281 | } 282 | } 283 | }, 284 | "url": { 285 | "raw": "{{HTTP_URL}}/review/1", 286 | "host": [ 287 | "{{HTTP_URL}}" 288 | ], 289 | "path": [ 290 | "review", 291 | "1" 292 | ] 293 | } 294 | }, 295 | "response": [] 296 | }, 297 | { 298 | "name": "List Reviews", 299 | "request": { 300 | "method": "GET", 301 | "header": [ 302 | { 303 | "key": "Authorization", 304 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJpc3MiOiJHb19Gb29kX0RlbGl2ZXJ5IiwiZXhwIjoxNzIzNTQ3NDI4fQ.q45k3qv3gfBV2Rm-5SODYFgCnmF74oJIpDza2mVPJhM", 305 | "type": "text" 306 | } 307 | ], 308 | "url": { 309 | "raw": "{{HTTP_URL}}/review/1", 310 | "host": [ 311 | "{{HTTP_URL}}" 312 | ], 313 | "path": [ 314 | "review", 315 | "1" 316 | ] 317 | } 318 | }, 319 | "response": [] 320 | }, 321 | { 322 | "name": "Delete Review", 323 | "request": { 324 | "method": "DELETE", 325 | "header": [ 326 | { 327 | "key": "Authorization", 328 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJpc3MiOiJHb19Gb29kX0RlbGl2ZXJ5IiwiZXhwIjoxNzIzODAxMjYwfQ.Lv42GA5zYKbJGIbwsVQtXiNssjDPfoP3ni1QPzTBu-M", 329 | "type": "text" 330 | } 331 | ], 332 | "url": { 333 | "raw": "{{HTTP_URL}}/review/2", 334 | "host": [ 335 | "{{HTTP_URL}}" 336 | ], 337 | "path": [ 338 | "review", 339 | "2" 340 | ] 341 | } 342 | }, 343 | "response": [] 344 | } 345 | ] 346 | }, 347 | { 348 | "name": "Cart", 349 | "item": [ 350 | { 351 | "name": "Add Item to Cart", 352 | "request": { 353 | "method": "POST", 354 | "header": [ 355 | { 356 | "key": "Authorization", 357 | "value": "{{TOKEN}}", 358 | "type": "text" 359 | } 360 | ], 361 | "body": { 362 | "mode": "raw", 363 | "raw": "{\r\n \"item_id\":1,\r\n \"restaurant_id\":1,\r\n \"quantity\":1\r\n}", 364 | "options": { 365 | "raw": { 366 | "language": "json" 367 | } 368 | } 369 | }, 370 | "url": { 371 | "raw": "{{HTTP_URL}}/cart/add", 372 | "host": [ 373 | "{{HTTP_URL}}" 374 | ], 375 | "path": [ 376 | "cart", 377 | "add" 378 | ] 379 | } 380 | }, 381 | "response": [] 382 | }, 383 | { 384 | "name": "Lists Cart Items", 385 | "request": { 386 | "method": "GET", 387 | "header": [ 388 | { 389 | "key": "Authorization", 390 | "value": "{{TOKEN}}", 391 | "type": "text" 392 | } 393 | ], 394 | "url": { 395 | "raw": "{{HTTP_URL}}/cart/list", 396 | "host": [ 397 | "{{HTTP_URL}}" 398 | ], 399 | "path": [ 400 | "cart", 401 | "list" 402 | ] 403 | } 404 | }, 405 | "response": [] 406 | }, 407 | { 408 | "name": "Remove Item from Cart", 409 | "request": { 410 | "method": "DELETE", 411 | "header": [ 412 | { 413 | "key": "Authorization", 414 | "value": "{{TOKEN}}", 415 | "type": "text" 416 | } 417 | ], 418 | "url": { 419 | "raw": "{{HTTP_URL}}/cart/remove/2", 420 | "host": [ 421 | "{{HTTP_URL}}" 422 | ], 423 | "path": [ 424 | "cart", 425 | "remove", 426 | "2" 427 | ] 428 | } 429 | }, 430 | "response": [] 431 | }, 432 | { 433 | "name": "Place a new order", 434 | "request": { 435 | "method": "POST", 436 | "header": [ 437 | { 438 | "key": "Authorization", 439 | "value": "{{TOKEN}}", 440 | "type": "text" 441 | } 442 | ], 443 | "url": { 444 | "raw": "{{HTTP_URL}}/cart/order/new", 445 | "host": [ 446 | "{{HTTP_URL}}" 447 | ], 448 | "path": [ 449 | "cart", 450 | "order", 451 | "new" 452 | ] 453 | } 454 | }, 455 | "response": [] 456 | } 457 | ] 458 | }, 459 | { 460 | "name": "DeliveryPerson", 461 | "item": [ 462 | { 463 | "name": "Add a new delivery person", 464 | "request": { 465 | "method": "POST", 466 | "header": [], 467 | "body": { 468 | "mode": "raw", 469 | "raw": "{\r\n \"name\": \"John Wick\",\r\n \"phone\": \"78784512458\",\r\n \"vehicle_details\": \"OX-25895-8547\"\r\n}", 470 | "options": { 471 | "raw": { 472 | "language": "json" 473 | } 474 | } 475 | }, 476 | "url": { 477 | "raw": "{{HTTP_URL}}/delivery/add", 478 | "host": [ 479 | "{{HTTP_URL}}" 480 | ], 481 | "path": [ 482 | "delivery", 483 | "add" 484 | ] 485 | } 486 | }, 487 | "response": [] 488 | }, 489 | { 490 | "name": "Login as DeliveryPerson", 491 | "request": { 492 | "method": "POST", 493 | "header": [], 494 | "body": { 495 | "mode": "raw", 496 | "raw": "{\r\n \"phone\": \"78784512458\",\r\n \"otp\": \"614550\"\r\n}", 497 | "options": { 498 | "raw": { 499 | "language": "json" 500 | } 501 | } 502 | }, 503 | "url": { 504 | "raw": "{{HTTP_URL}}/delivery/login", 505 | "host": [ 506 | "{{HTTP_URL}}" 507 | ], 508 | "path": [ 509 | "delivery", 510 | "login" 511 | ] 512 | } 513 | }, 514 | "response": [] 515 | }, 516 | { 517 | "name": "Get all delivery order lists", 518 | "request": { 519 | "method": "GET", 520 | "header": [ 521 | { 522 | "key": "Authorization", 523 | "value": "{{DELIVERY_PERSON_TOKEN}}", 524 | "type": "text" 525 | } 526 | ], 527 | "url": { 528 | "raw": "{{HTTP_URL}}/delivery/deliveries/87", 529 | "host": [ 530 | "{{HTTP_URL}}" 531 | ], 532 | "path": [ 533 | "delivery", 534 | "deliveries", 535 | "87" 536 | ] 537 | } 538 | }, 539 | "response": [] 540 | }, 541 | { 542 | "name": "Update Order Status", 543 | "request": { 544 | "method": "POST", 545 | "header": [ 546 | { 547 | "key": "Authorization", 548 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJuYW1lIjoiSm9obiBXaWNrIiwiaXNzIjoiR29fRm9vZF9EZWxpdmVyeSIsImV4cCI6MTcyNjgzMjgwOH0.AhbVmNiNqU6x8VMSW-yZT0j_kP9F2MTKcSvfkqxCbZ0", 549 | "type": "text" 550 | } 551 | ], 552 | "body": { 553 | "mode": "raw", 554 | "raw": "{\r\n \"order_id\": 108,\r\n \"status\": \"on_the_way\"\r\n}", 555 | "options": { 556 | "raw": { 557 | "language": "json" 558 | } 559 | } 560 | }, 561 | "url": { 562 | "raw": "{{HTTP_URL}}/delivery/update-order", 563 | "host": [ 564 | "{{HTTP_URL}}" 565 | ], 566 | "path": [ 567 | "delivery", 568 | "update-order" 569 | ] 570 | } 571 | }, 572 | "response": [] 573 | } 574 | ] 575 | }, 576 | { 577 | "name": "Announcements", 578 | "item": [ 579 | { 580 | "name": "Flash Events", 581 | "request": { 582 | "method": "GET", 583 | "header": [], 584 | "url": { 585 | "raw": "{{HTTP_URL}}/announcements/events", 586 | "host": [ 587 | "{{HTTP_URL}}" 588 | ], 589 | "path": [ 590 | "announcements", 591 | "events" 592 | ] 593 | } 594 | }, 595 | "response": [] 596 | } 597 | ] 598 | } 599 | ], 600 | "event": [ 601 | { 602 | "listen": "prerequest", 603 | "script": { 604 | "type": "text/javascript", 605 | "packages": {}, 606 | "exec": [ 607 | "" 608 | ] 609 | } 610 | }, 611 | { 612 | "listen": "test", 613 | "script": { 614 | "type": "text/javascript", 615 | "packages": {}, 616 | "exec": [ 617 | "" 618 | ] 619 | } 620 | } 621 | ], 622 | "variable": [ 623 | { 624 | "key": "HTTP_URL", 625 | "value": "http://localhost:8080", 626 | "type": "string" 627 | }, 628 | { 629 | "key": "TOKEN", 630 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJuYW1lIjoic2FtcGxlMSIsImlzcyI6IkdvX0Zvb2RfRGVsaXZlcnkiLCJleHAiOjE3MjY1NzQ1NzZ9.xMOzKrCWQl4Gidq8m_lu2eTTRgSn-Ax2Jfc5GiXMoNY", 631 | "type": "string" 632 | }, 633 | { 634 | "key": "DELIVERY_PERSON_TOKEN", 635 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJuYW1lIjoiSm9obiBXaWNrIiwiaXNzIjoiR29fRm9vZF9EZWxpdmVyeSIsImV4cCI6MTcyNjgzMjgwOH0.AhbVmNiNqU6x8VMSW-yZT0j_kP9F2MTKcSvfkqxCbZ0", 636 | "type": "string" 637 | } 638 | ] 639 | } -------------------------------------------------------------------------------- /misc/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/Go_Food_Delivery/b5552e3ee26fa7d3bcccbf15e378f81d0e14f76e/misc/images/background.png -------------------------------------------------------------------------------- /misc/images/go_run_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/Go_Food_Delivery/b5552e3ee26fa7d3bcccbf15e378f81d0e14f76e/misc/images/go_run_config.png -------------------------------------------------------------------------------- /misc/images/httpclient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/Go_Food_Delivery/b5552e3ee26fa7d3bcccbf15e378f81d0e14f76e/misc/images/httpclient.png -------------------------------------------------------------------------------- /pkg/abstract/announcements/events.go: -------------------------------------------------------------------------------- 1 | package announcements 2 | 3 | type FlashEvents struct { 4 | Headline string `json:"headline"` 5 | Message string `json:"message"` 6 | } 7 | 8 | type Announcement interface { 9 | FlashEvents() (*[]FlashEvents, error) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/abstract/cart/cart.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "context" 6 | ) 7 | 8 | type Cart interface { 9 | Create(ctx context.Context, cart *cart.Cart) (*cart.Cart, error) 10 | GetCartId(ctx context.Context, UserId int64) (*cart.Cart, error) 11 | AddItem(ctx context.Context, Item *cart.CartItems) (*cart.CartItems, error) 12 | } 13 | 14 | type CartItems interface { 15 | ListItems(ctx context.Context, cartId int64) (*[]cart.CartItems, error) 16 | DeleteItem(ctx context.Context, cartItemId int64) (bool, error) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/abstract/cart/order.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "Go_Food_Delivery/pkg/database/models/order" 6 | "context" 7 | ) 8 | 9 | type Order interface { 10 | PlaceOrder(ctx context.Context, cartId int64, userId int64) (*order.Order, error) 11 | OrderList(ctx context.Context, userId int64) (*[]order.Order, error) 12 | RemoveItemsFromCart(ctx context.Context, cartId int64) error 13 | DeliveryInformation(ctx context.Context, orderId int64, userId int64) (*[]delivery.DeliveryListResponse, error) 14 | } 15 | 16 | type Notification interface { 17 | NewOrderPlacedNotification(userId int64, orderId int64) error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/abstract/delivery/delivery.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | ) 7 | 8 | type DeliveryPerson interface { 9 | AddDeliveryPerson(ctx context.Context, deliveryPerson *delivery.DeliveryPerson) (bool, error) 10 | } 11 | 12 | type Validation interface { 13 | GenerateTOTP(_ context.Context, phone string) (string, string, error) 14 | ValidateOTP(_ context.Context, secretKey string, otp string) bool 15 | ValidateAccountDetails(ctx context.Context, phone string) (*delivery.DeliveryPerson, error) 16 | Verify(ctx context.Context, phone string, otp string) bool 17 | } 18 | 19 | type DeliveryLogin interface { 20 | GenerateJWT(ctx context.Context, userId int64, name string) (string, error) 21 | } 22 | 23 | type Deliveries interface { 24 | OrderPlacement(ctx context.Context, 25 | deliveryPersonID int64, orderID int64, deliveryStatus string) (bool, error) 26 | DeliveryListing(ctx context.Context, orderID int64) (*[]delivery.Deliveries, error) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/abstract/restaurant/menu.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | type MenuItems interface { 9 | AddMenu(ctx context.Context, menu *restaurant.MenuItem) (*restaurant.MenuItem, error) 10 | UpdateMenuPhoto(ctx context.Context, menu *restaurant.MenuItem) 11 | ListMenus(ctx context.Context, restaurantId int64) ([]restaurant.MenuItem, error) 12 | ListAllMenus(ctx context.Context) ([]restaurant.MenuItem, error) 13 | DeleteMenu(ctx context.Context, menuId int64, restaurantId int64) (bool, error) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/abstract/restaurant/restaurant.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | type Restaurant interface { 9 | Add(ctx context.Context, user *restaurant.Restaurant) (bool, error) 10 | ListRestaurants(ctx context.Context) ([]restaurant.Restaurant, error) 11 | ListRestaurantById(ctx context.Context, restaurantId int64) (restaurant.Restaurant, error) 12 | DeleteRestaurant(ctx context.Context, restaurantId int64) (bool, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/abstract/review/review.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/review" 5 | "context" 6 | ) 7 | 8 | type Review interface { 9 | Add(ctx context.Context, review *review.Review) (bool, error) 10 | ListReviews(ctx context.Context, restaurantId int64) ([]review.Review, error) 11 | DeleteReview(ctx context.Context, reviewId int64, userId int64) (bool, error) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/abstract/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/user" 5 | "context" 6 | ) 7 | 8 | type User interface { 9 | Add(ctx context.Context, user *user.User) (bool, error) 10 | Delete(ctx context.Context, userId int64) (bool, error) 11 | Login(ctx context.Context, userID int64) (string, error) 12 | UserExist(ctx context.Context, email string, recordRequired bool) (bool, int64, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "Go_Food_Delivery/pkg/database/models/delivery" 6 | "Go_Food_Delivery/pkg/database/models/order" 7 | "Go_Food_Delivery/pkg/database/models/restaurant" 8 | "Go_Food_Delivery/pkg/database/models/review" 9 | "Go_Food_Delivery/pkg/database/models/user" 10 | "context" 11 | "database/sql" 12 | "fmt" 13 | "github.com/uptrace/bun" 14 | "github.com/uptrace/bun/dialect/pgdialect" 15 | "github.com/uptrace/bun/dialect/sqlitedialect" 16 | "github.com/uptrace/bun/driver/pgdriver" 17 | "github.com/uptrace/bun/driver/sqliteshim" 18 | "log" 19 | "log/slog" 20 | "os" 21 | "strconv" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | type Database interface { 27 | Insert(ctx context.Context, model any) (sql.Result, error) 28 | Delete(ctx context.Context, tableName string, filter Filter) (sql.Result, error) 29 | Select(ctx context.Context, model any, columnName string, parameter any) error 30 | SelectAll(ctx context.Context, tableName string, model any) error 31 | SelectWithRelation(ctx context.Context, model any, relations []string, Condition Filter) error 32 | SelectWithMultipleFilter(ctx context.Context, model any, Condition Filter) error 33 | Raw(ctx context.Context, model any, query string, args ...interface{}) error 34 | Update(ctx context.Context, tableName string, Set Filter, Condition Filter) (sql.Result, error) 35 | Count(ctx context.Context, tableName string, ColumnExpression string, columnName string, parameter any) (int64, error) 36 | Migrate() error 37 | HealthCheck() bool 38 | Close() error 39 | } 40 | 41 | type Filter map[string]any 42 | 43 | type DB struct { 44 | db *bun.DB 45 | } 46 | 47 | func (d *DB) Insert(ctx context.Context, model any) (sql.Result, error) { 48 | result, err := d.db.NewInsert().Model(model).Exec(ctx) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return result, nil 53 | } 54 | 55 | func (d *DB) Update(ctx context.Context, tableName string, Set Filter, Condition Filter) (sql.Result, error) { 56 | result, err := d.db.NewUpdate().Table(tableName).Set(d.whereCondition(Set, "SET")).Where(d.whereCondition(Condition, "AND")).Exec(ctx) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return result, nil 61 | } 62 | 63 | func (d *DB) Delete(ctx context.Context, tableName string, filter Filter) (sql.Result, error) { 64 | result, err := d.db.NewDelete().Table(tableName).Where(d.whereCondition(filter, "AND")).Exec(ctx) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return result, err 69 | } 70 | 71 | func (d *DB) Select(ctx context.Context, model any, columnName string, parameter any) error { 72 | err := d.db.NewSelect().Model(model).Where(fmt.Sprintf("%s = ?", columnName), parameter).Scan(ctx) 73 | if err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func (d *DB) SelectWithMultipleFilter(ctx context.Context, model any, Condition Filter) error { 80 | err := d.db.NewSelect().Model(model).Where(d.whereCondition(Condition, "AND")).Scan(ctx) 81 | if err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | 87 | func (d *DB) SelectAll(ctx context.Context, tableName string, model any) error { 88 | if err := d.db.NewSelect().Table(tableName).Scan(ctx, model); err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | func (d *DB) SelectWithRelation(ctx context.Context, model any, relations []string, Condition Filter) error { 95 | query := d.db.NewSelect().Model(model) 96 | for _, relation := range relations { 97 | query.Relation(relation) 98 | } 99 | 100 | err := query.Where(d.whereCondition(Condition, "AND")).Scan(ctx) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (d *DB) Raw(ctx context.Context, model any, query string, args ...interface{}) error { 109 | if err := d.db.NewRaw(query, args...).Scan(ctx, model); err != nil { 110 | return err 111 | } 112 | return nil 113 | } 114 | 115 | func (d *DB) Count(ctx context.Context, tableName string, ColumnExpression string, columnName string, parameter any) (int64, error) { 116 | var count int64 117 | err := d.db.NewSelect().Table(tableName).ColumnExpr(ColumnExpression). 118 | Where(fmt.Sprintf("%s = ?", columnName), parameter).Scan(ctx, &count) 119 | if err != nil { 120 | return 0, err 121 | } 122 | return count, nil 123 | } 124 | 125 | func (d *DB) whereCondition(filter Filter, ConditionType string) string { 126 | var whereClauses []string 127 | for key, value := range filter { 128 | var formattedValue string 129 | switch v := value.(type) { 130 | case string: 131 | // Quote string values 132 | formattedValue = fmt.Sprintf("'%s'", v) 133 | case int, int64: 134 | formattedValue = fmt.Sprintf("%d", v) 135 | case float64: 136 | formattedValue = fmt.Sprintf("%.2f", v) 137 | default: 138 | log.Fatal("DB::Query:: Un-handled type for where condition!") 139 | 140 | } 141 | whereClauses = append(whereClauses, fmt.Sprintf("%s=%s", key, formattedValue)) 142 | } 143 | 144 | var result string 145 | if len(whereClauses) > 0 { 146 | if ConditionType == "SET" { 147 | result = strings.Join(whereClauses, " , ") 148 | } else if ConditionType == "AND" { 149 | result = strings.Join(whereClauses, " AND ") 150 | } 151 | } 152 | return result 153 | } 154 | 155 | func (d *DB) HealthCheck() bool { 156 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 157 | defer cancel() 158 | 159 | err := d.db.PingContext(ctx) 160 | if err != nil { 161 | slog.Error("DB::error", "error", err.Error()) 162 | return false 163 | } 164 | return true 165 | } 166 | 167 | func (d *DB) Close() error { 168 | slog.Info("DB::Closing database connection") 169 | return d.db.Close() 170 | } 171 | 172 | func New() Database { 173 | dbHost := os.Getenv("DB_HOST") 174 | dbUsername := os.Getenv("DB_USERNAME") 175 | dbPassword := os.Getenv("DB_PASSWORD") 176 | dbName := os.Getenv("DB_NAME") 177 | dbPort := os.Getenv("DB_PORT") 178 | databasePort, err := strconv.Atoi(dbPort) 179 | if err != nil { 180 | log.Fatal("Invalid DB Port") 181 | } 182 | 183 | dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", dbUsername, dbPassword, dbHost, databasePort, dbName) 184 | database := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn))) 185 | db := bun.NewDB(database, pgdialect.New()) 186 | return &DB{db: db} 187 | 188 | } 189 | 190 | // NewTestDB creates a new in-memory test database. 191 | func NewTestDB() Database { 192 | database, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") 193 | if err != nil { 194 | panic(err) 195 | } 196 | db := bun.NewDB(database, sqlitedialect.New()) 197 | return &DB{db} 198 | } 199 | 200 | func (d *DB) Migrate() error { 201 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 202 | defer cancel() 203 | models := []interface{}{ 204 | (*user.User)(nil), 205 | (*restaurant.Restaurant)(nil), 206 | (*restaurant.MenuItem)(nil), 207 | (*review.Review)(nil), 208 | (*order.Order)(nil), 209 | (*order.OrderItems)(nil), 210 | (*cart.Cart)(nil), 211 | (*cart.CartItems)(nil), 212 | (*delivery.DeliveryPerson)(nil), 213 | (*delivery.Deliveries)(nil), 214 | } 215 | 216 | for _, model := range models { 217 | if _, err := d.db.NewCreateTable().Model(model).WithForeignKeys().IfNotExists().Exec(ctx); err != nil { 218 | return err 219 | } 220 | } 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /pkg/database/models/cart/cart.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | userModel "Go_Food_Delivery/pkg/database/models/user" 6 | "Go_Food_Delivery/pkg/database/models/utils" 7 | "github.com/uptrace/bun" 8 | ) 9 | 10 | type Cart struct { 11 | bun.BaseModel `bun:"table:cart"` 12 | CartID int64 `bun:",pk,autoincrement" json:"cart_id"` 13 | UserID int64 `bun:"user_id,notnull" json:"user_id"` 14 | utils.Timestamp 15 | User *userModel.User `bun:"rel:belongs-to,join:user_id=id" json:"-"` 16 | } 17 | 18 | type CartItems struct { 19 | bun.BaseModel `bun:"table:cart_items"` 20 | CartItemID int64 `bun:",pk,autoincrement" json:"cart_item_id"` 21 | CartID int64 `bun:"cart_id,notnull" json:"cart_id"` 22 | ItemID int64 `bun:"item_id,notnull" json:"item_id"` 23 | RestaurantID int64 `bun:"restaurant_id,notnull" json:"restaurant_id"` 24 | Quantity int64 `bun:"quantity,notnull" json:"quantity"` 25 | utils.Timestamp 26 | Restaurant *restaurant.Restaurant `bun:"rel:belongs-to,join:restaurant_id=restaurant_id" json:"-"` 27 | MenuItem *restaurant.MenuItem `bun:"rel:belongs-to,join:item_id=menu_id" json:"menu_item"` 28 | Cart *Cart `bun:"rel:belongs-to,join:cart_id=cart_id" json:"-"` 29 | } 30 | 31 | type CartItemParams struct { 32 | CartID int64 `json:"cart_id"` 33 | ItemID int64 `json:"item_id"` 34 | RestaurantID int64 `json:"restaurant_id"` 35 | Quantity int64 `json:"quantity"` 36 | } 37 | -------------------------------------------------------------------------------- /pkg/database/models/delivery/delivery.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/order" 5 | "Go_Food_Delivery/pkg/database/models/utils" 6 | "github.com/uptrace/bun" 7 | "time" 8 | ) 9 | 10 | type DeliveryPerson struct { 11 | bun.BaseModel `bun:"table:delivery_person"` 12 | DeliveryPersonID int64 `bun:",pk,autoincrement" json:"delivery_person_id"` 13 | Name string `bun:"name,notnull" json:"name"` 14 | Phone string `bun:"phone,unique,notnull" json:"phone"` 15 | VehicleDetails string `bun:"vehicle_details,notnull" json:"vehicle_details"` 16 | Status string `bun:"status,notnull" json:"status"` 17 | AuthKey string `bun:"auth_key,notnull" json:"auth_key"` 18 | AuthKeyURL string `bun:"auth_key_url,notnull" json:"auth_key_url"` 19 | IsAuthSet bool `bun:"is_auth_set,notnull" json:"is_auth_set"` 20 | utils.Timestamp 21 | } 22 | 23 | type Deliveries struct { 24 | bun.BaseModel `bun:"table:deliveries"` 25 | DeliveryID int64 `bun:",pk,autoincrement" json:"delivery_id"` 26 | DeliveryPersonID int64 `bun:"delivery_person_id,notnull" json:"-"` 27 | OrderID int64 `bun:"order_id,notnull" json:"order_id"` 28 | DeliveryStatus string `bun:"delivery_status,notnull" json:"delivery_status"` 29 | DeliveryTime time.Time `bun:",nullzero"` 30 | Order *order.Order `bun:"rel:belongs-to,join:order_id=order_id" json:"-"` 31 | DeliveryPerson *DeliveryPerson `bun:"rel:belongs-to,join:delivery_person_id=delivery_person_id" json:"-"` 32 | utils.Timestamp 33 | } 34 | 35 | type DeliveryPersonParams struct { 36 | Name string `json:"name"` 37 | Phone string `json:"phone"` 38 | VehicleDetails string `json:"vehicle_details"` 39 | } 40 | 41 | type DeliveryLoginParams struct { 42 | Phone string `json:"phone"` 43 | OTP string `json:"otp"` 44 | } 45 | 46 | type DeliveryOrderPlacementParams struct { 47 | OrderID int64 `json:"order_id"` 48 | Status string `json:"status"` 49 | } 50 | 51 | type DeliveryListResponse struct { 52 | DeliveryId int `json:"delivery_id"` 53 | DeliveryPersonId int `json:"delivery_person_id"` 54 | DeliveryStatus string `json:"delivery_status"` 55 | DeliveryTime time.Time `json:"delivery_time"` 56 | CreatedAt time.Time `json:"created_at"` 57 | Name string `json:"name"` 58 | VehicleDetails string `json:"vehicle_details"` 59 | Phone string `json:"phone"` 60 | } 61 | -------------------------------------------------------------------------------- /pkg/database/models/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | userModel "Go_Food_Delivery/pkg/database/models/user" 6 | "Go_Food_Delivery/pkg/database/models/utils" 7 | "github.com/uptrace/bun" 8 | ) 9 | 10 | type Order struct { 11 | bun.BaseModel `bun:"table:orders"` 12 | OrderID int64 `bun:",pk,autoincrement" json:"order_id"` 13 | UserID int64 `bun:"user_id,notnull" json:"-"` 14 | OrderStatus string `bun:"order_status,notnull" json:"order_status"` 15 | TotalAmount float64 `bun:"total_amount,notnull" json:"total_amount"` 16 | DeliveryAddress string `bun:"delivery_address,notnull" json:"delivery_address"` 17 | utils.Timestamp 18 | User *userModel.User `bun:"rel:belongs-to,join:user_id=id" json:"-"` 19 | } 20 | 21 | type OrderItems struct { 22 | bun.BaseModel `bun:"table:order_items"` 23 | OrderItemID int64 `bun:",pk,autoincrement" json:"order_item_id"` 24 | OrderID int64 `bun:"order_id,notnull" json:"order_id"` 25 | ItemID int64 `bun:"item_id,notnull" json:"item_id"` 26 | RestaurantID int64 `bun:"restaurant_id,notnull" json:"restaurant_id"` 27 | Quantity int64 `bun:"quantity,notnull" json:"quantity"` 28 | Price float64 `bun:"price,notnull" json:"price"` 29 | utils.Timestamp 30 | MenuItem *restaurant.MenuItem `bun:"rel:belongs-to,join:item_id=menu_id" json:"MenuItem"` 31 | Restaurant *restaurant.Restaurant `bun:"rel:belongs-to,join:restaurant_id=restaurant_id"` 32 | Order *Order `bun:"rel:belongs-to,join:order_id=order_id" json:"-"` 33 | } 34 | -------------------------------------------------------------------------------- /pkg/database/models/restaurant/restaurant.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/utils" 5 | "github.com/uptrace/bun" 6 | ) 7 | 8 | type Restaurant struct { 9 | bun.BaseModel `bun:"table:restaurant"` 10 | RestaurantID int64 `bun:",pk,autoincrement" json:"restaurant_id"` 11 | Name string `bun:",notnull" json:"name"` 12 | Photo string `bun:"store_image,nullzero" json:"store_image"` 13 | Description string `bun:",notnull" json:"description"` 14 | Address string `bun:"address,notnull" json:"address"` 15 | City string `bun:"city,notnull" json:"city"` 16 | State string `bun:"state,notnull" json:"state"` 17 | utils.Timestamp 18 | MenuItems []MenuItem `bun:"rel:has-many,join:restaurant_id=menu_id" json:"-"` 19 | } 20 | 21 | type MenuItem struct { 22 | bun.BaseModel `bun:"table:menu_item"` 23 | MenuID int64 `bun:",pk,autoincrement" json:"menu_id"` 24 | RestaurantID int64 `bun:"restaurant_id,notnull" json:"restaurant_id"` 25 | Name string `bun:"name,notnull" json:"name"` 26 | Description string `bun:"description,notnull" json:"description"` 27 | Photo string `bun:"photo,nullzero" json:"photo"` 28 | Price float64 `bun:"price,notnull" json:"price"` 29 | Category string `bun:"category,notnull" json:"category"` 30 | Available bool `bun:"available,default:True" json:"available"` 31 | utils.Timestamp 32 | Restaurant *Restaurant `bun:"rel:belongs-to,join:restaurant_id=restaurant_id" json:"-"` 33 | } 34 | -------------------------------------------------------------------------------- /pkg/database/models/review/review.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "Go_Food_Delivery/pkg/database/models/user" 6 | "Go_Food_Delivery/pkg/database/models/utils" 7 | "errors" 8 | "github.com/go-playground/validator/v10" 9 | "github.com/uptrace/bun" 10 | ) 11 | 12 | type Review struct { 13 | bun.BaseModel `bun:"table:reviews"` 14 | 15 | ReviewID int64 `bun:"review_id,pk,autoincrement" json:"review_id"` 16 | UserID int64 `bun:"user_id" json:"user_id"` 17 | RestaurantID int64 `bun:"restaurant_id" json:"restaurant_id"` 18 | Rating int `bun:"rating"` 19 | Comment string `bun:"comment"` 20 | utils.Timestamp 21 | 22 | User *user.User `bun:"rel:belongs-to,join:user_id=id" json:"-"` 23 | Restaurant *restaurant.Restaurant `bun:"rel:belongs-to,join:restaurant_id=restaurant_id" json:"-"` 24 | } 25 | 26 | type ReviewParams struct { 27 | Rating int `json:"rating" validate:"rating"` 28 | Comment string `json:"comment"` 29 | } 30 | 31 | func RatingValidator(fl validator.FieldLevel) bool { 32 | rating, ok := fl.Field().Interface().(int) 33 | return ok && rating >= 1 && rating <= 5 34 | } 35 | 36 | func ReviewValidationError(err error) map[string]string { 37 | var validationErrors validator.ValidationErrors 38 | if !errors.As(err, &validationErrors) { 39 | return map[string]string{"error": "Unknown error"} 40 | } 41 | 42 | errorsMap := make(map[string]string) 43 | for _, e := range validationErrors { 44 | field := e.Field() 45 | switch e.Tag() { 46 | case "rating": 47 | errorsMap[field] = "Rating must be between 1 and 5" 48 | default: 49 | errorsMap[field] = "Invalid" 50 | } 51 | } 52 | return errorsMap 53 | } 54 | -------------------------------------------------------------------------------- /pkg/database/models/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "github.com/go-playground/validator/v10" 6 | "github.com/uptrace/bun" 7 | "golang.org/x/crypto/bcrypt" 8 | "log" 9 | "regexp" 10 | ) 11 | 12 | type User struct { 13 | bun.BaseModel `bun:"table:users"` 14 | ID int64 `bun:",pk,autoincrement" json:"id"` 15 | Name string `bun:",notnull" json:"name" validate:"name"` 16 | Email string `bun:",unique,notnull" json:"email" validate:"email"` 17 | Password string `bun:",notnull" json:"password"` 18 | } 19 | 20 | type LoginUser struct { 21 | Email string `json:"email"` 22 | Password string `json:"password"` 23 | } 24 | 25 | func (u *User) HashPassword() { 26 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) 27 | if err != nil { 28 | log.Fatal("error hashing password") 29 | } 30 | u.Password = string(hashedPassword) 31 | } 32 | 33 | func (l *LoginUser) CheckPassword(hashPassword string) error { 34 | return bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(l.Password)) 35 | } 36 | 37 | func NameValidator(fl validator.FieldLevel) bool { 38 | str, ok := fl.Field().Interface().(string) 39 | return ok && str != "" 40 | } 41 | 42 | func EmailValidator(fl validator.FieldLevel) bool { 43 | email, ok := fl.Field().Interface().(string) 44 | if !ok { 45 | return false 46 | } 47 | // Basic email regex pattern 48 | re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) 49 | return re.MatchString(email) 50 | } 51 | 52 | func UserValidationError(err error) map[string]string { 53 | var validationErrors validator.ValidationErrors 54 | if !errors.As(err, &validationErrors) { 55 | return map[string]string{"error": "Unknown error"} 56 | } 57 | 58 | errorsMap := make(map[string]string) 59 | for _, e := range validationErrors { 60 | field := e.Field() 61 | switch e.Tag() { 62 | case "name": 63 | errorsMap[field] = "Provide your full name" 64 | case "email": 65 | errorsMap[field] = "Provide valid email address" 66 | default: 67 | errorsMap[field] = "Invalid" 68 | } 69 | } 70 | return errorsMap 71 | } 72 | -------------------------------------------------------------------------------- /pkg/database/models/utils/timestamp.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | type Timestamp struct { 6 | CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` 7 | UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` 8 | } 9 | -------------------------------------------------------------------------------- /pkg/handler/annoucements/events.go: -------------------------------------------------------------------------------- 1 | package annoucements 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "time" 7 | ) 8 | 9 | func (s *AnnouncementHandler) flashNews(c *gin.Context) { 10 | _, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 11 | defer cancel() 12 | 13 | events, err := s.service.FlashEvents() 14 | if err != nil { 15 | c.AbortWithStatusJSON(500, gin.H{"error": err.Error()}) 16 | return 17 | } 18 | 19 | // Set headers for SSE 20 | c.Header("Content-Type", "text/event-stream") 21 | c.Header("Cache-Control", "no-cache") 22 | c.Header("Connection", "keep-alive") 23 | 24 | ticker := time.NewTicker(6 * time.Second) 25 | defer ticker.Stop() 26 | 27 | eventIndex := 0 28 | 29 | for { 30 | select { 31 | case <-ticker.C: 32 | // Send the current event 33 | event := (*events)[eventIndex] 34 | c.SSEvent("message", event.Message) 35 | c.Writer.Flush() 36 | 37 | // Move to the next event 38 | eventIndex = (eventIndex + 1) % len(*events) 39 | case <-c.Request.Context().Done(): 40 | ticker.Stop() 41 | return 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pkg/handler/annoucements/routes.go: -------------------------------------------------------------------------------- 1 | package annoucements 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *AnnouncementHandler) registerMiddlewareGroup(middleware ...gin.HandlerFunc) gin.IRoutes { 9 | return s.serve.Gin.Group(s.group).Use(middleware...) 10 | } 11 | 12 | func (s *AnnouncementHandler) registerGroup() gin.IRoutes { 13 | return s.serve.Gin.Group(s.group) 14 | } 15 | 16 | func (s *AnnouncementHandler) regularRoutes() http.Handler { 17 | s.router.GET("/events", s.flashNews) 18 | 19 | return s.serve.Gin 20 | } 21 | 22 | func (s *AnnouncementHandler) middlewareRoutes() http.Handler { 23 | return s.serve.Gin 24 | } 25 | -------------------------------------------------------------------------------- /pkg/handler/annoucements/service.go: -------------------------------------------------------------------------------- 1 | package annoucements 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/service/announcements" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-playground/validator/v10" 8 | ) 9 | 10 | type AnnouncementHandler struct { 11 | serve *handler.Server 12 | group string 13 | middlewareGuarded gin.IRoutes 14 | router gin.IRoutes 15 | service *announcements.AnnouncementService 16 | middleware []gin.HandlerFunc 17 | validate *validator.Validate 18 | } 19 | 20 | func NewAnnouncementHandler(s *handler.Server, group string, 21 | service *announcements.AnnouncementService, middleware []gin.HandlerFunc, 22 | validate *validator.Validate) { 23 | 24 | cartHandler := &AnnouncementHandler{ 25 | s, 26 | group, 27 | nil, 28 | nil, 29 | service, 30 | middleware, 31 | validate, 32 | } 33 | cartHandler.middlewareGuarded = cartHandler.registerMiddlewareGroup(middleware...) 34 | cartHandler.router = cartHandler.registerGroup() 35 | cartHandler.regularRoutes() 36 | cartHandler.middlewareRoutes() 37 | cartHandler.registerValidator() 38 | } 39 | 40 | func (s *AnnouncementHandler) registerValidator() { 41 | 42 | } 43 | -------------------------------------------------------------------------------- /pkg/handler/cart/cart.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func (s *CartHandler) addToCart(c *gin.Context) { 13 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 14 | defer cancel() 15 | 16 | var cartID int64 17 | 18 | userID := c.GetInt64("userID") 19 | 20 | // Check Cart Exists in DB 21 | cartInfo, err := s.service.GetCartId(ctx, userID) 22 | if err != nil { 23 | // Create a new cart. 24 | var cartData cart.Cart 25 | cartData.UserID = userID 26 | 27 | newCart, err := s.service.Create(ctx, &cartData) 28 | if err != nil { 29 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 30 | return 31 | } 32 | cartID = newCart.CartID 33 | } else { 34 | cartID = cartInfo.CartID 35 | } 36 | 37 | var cartItemParam cart.CartItemParams 38 | cartItemParam.CartID = cartID 39 | 40 | if err := c.BindJSON(&cartItemParam); err != nil { 41 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 42 | return 43 | } 44 | 45 | cartItem := &cart.CartItems{ 46 | CartID: cartItemParam.CartID, 47 | ItemID: cartItemParam.ItemID, 48 | RestaurantID: cartItemParam.RestaurantID, 49 | Quantity: cartItemParam.Quantity, 50 | } 51 | 52 | _, err = s.service.AddItem(ctx, cartItem) 53 | if err != nil { 54 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | c.JSON(http.StatusCreated, gin.H{"message": "Items added to cart!"}) 59 | 60 | } 61 | 62 | func (s *CartHandler) getItems(c *gin.Context) { 63 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 64 | defer cancel() 65 | 66 | userID := c.GetInt64("userID") 67 | cartInfo, err := s.service.GetCartId(ctx, userID) 68 | if err != nil { 69 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 70 | return 71 | } 72 | 73 | items, err := s.service.ListItems(ctx, cartInfo.CartID) 74 | if err != nil { 75 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | c.JSON(http.StatusOK, gin.H{"items": items}) 80 | return 81 | } 82 | 83 | func (s *CartHandler) deleteItemFromCart(c *gin.Context) { 84 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 85 | defer cancel() 86 | 87 | cartItemId := c.Param("id") 88 | cartItemID, _ := strconv.ParseInt(cartItemId, 10, 64) 89 | 90 | _, err := s.service.DeleteItem(ctx, cartItemID) 91 | if err != nil { 92 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 93 | return 94 | } 95 | 96 | c.Status(http.StatusNoContent) 97 | 98 | } 99 | -------------------------------------------------------------------------------- /pkg/handler/cart/order.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | func (s *CartHandler) PlaceNewOrder(c *gin.Context) { 12 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 13 | defer cancel() 14 | 15 | userID := c.GetInt64("userID") 16 | cartInfo, err := s.service.GetCartId(ctx, userID) 17 | if err != nil { 18 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 19 | return 20 | } 21 | // Place a new order. 22 | newOrder, err := s.service.PlaceOrder(ctx, cartInfo.CartID, userID) 23 | if err != nil { 24 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 25 | return 26 | } 27 | // Remove all items from the cart after placing order. 28 | err = s.service.RemoveItemsFromCart(ctx, cartInfo.CartID) 29 | if err != nil { 30 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 31 | return 32 | } 33 | 34 | // Notify new order 35 | err = s.service.NewOrderPlacedNotification(userID, newOrder.OrderID) 36 | if err != nil { 37 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Notification failed!"}) 38 | return 39 | } 40 | 41 | c.JSON(http.StatusCreated, gin.H{"message": "Order placed!"}) 42 | 43 | } 44 | 45 | func (s *CartHandler) getOrderList(c *gin.Context) { 46 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 47 | defer cancel() 48 | 49 | userID := c.GetInt64("userID") 50 | orders, err := s.service.OrderList(ctx, userID) 51 | if err != nil { 52 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 53 | return 54 | } 55 | 56 | c.JSON(http.StatusOK, gin.H{"orders": orders}) 57 | return 58 | } 59 | 60 | func (s *CartHandler) getOrderItemsList(c *gin.Context) { 61 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 62 | defer cancel() 63 | 64 | userID := c.GetInt64("userID") 65 | orderID, _ := strconv.ParseInt(c.Param("id"), 10, 64) 66 | 67 | orders, err := s.service.OrderItemsList(ctx, userID, orderID) 68 | if err != nil { 69 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 70 | return 71 | } 72 | 73 | c.JSON(http.StatusOK, gin.H{"orders": orders}) 74 | return 75 | } 76 | 77 | func (s *CartHandler) getDeliveriesList(c *gin.Context) { 78 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 79 | defer cancel() 80 | 81 | userID := c.GetInt64("userID") 82 | orderID, _ := strconv.ParseInt(c.Param("id"), 10, 64) 83 | 84 | deliveries, err := s.service.DeliveryInformation(ctx, orderID, userID) 85 | if err != nil { 86 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | 90 | c.JSON(http.StatusOK, gin.H{"delivery_info": deliveries}) 91 | return 92 | } 93 | -------------------------------------------------------------------------------- /pkg/handler/cart/routes.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *CartHandler) registerGroup(middleware ...gin.HandlerFunc) gin.IRoutes { 9 | return s.serve.Gin.Group(s.group).Use(middleware...) 10 | } 11 | 12 | func (s *CartHandler) routes() http.Handler { 13 | s.router.POST("/add", s.addToCart) 14 | s.router.GET("/list", s.getItems) 15 | s.router.DELETE("/remove/:id", s.deleteItemFromCart) 16 | s.router.POST("/order/new", s.PlaceNewOrder) 17 | s.router.GET("/orders", s.getOrderList) 18 | s.router.GET("/orders/:id", s.getOrderItemsList) 19 | s.router.GET("/orders/deliveries/:id", s.getDeliveriesList) 20 | return s.serve.Gin 21 | } 22 | -------------------------------------------------------------------------------- /pkg/handler/cart/service.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/service/cart_order" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-playground/validator/v10" 8 | ) 9 | 10 | type CartHandler struct { 11 | serve *handler.Server 12 | group string 13 | router gin.IRoutes 14 | service *cart_order.CartService 15 | middleware []gin.HandlerFunc 16 | validate *validator.Validate 17 | } 18 | 19 | func NewCartHandler(s *handler.Server, groupName string, 20 | service *cart_order.CartService, middleware []gin.HandlerFunc, 21 | validate *validator.Validate) { 22 | 23 | cartHandler := &CartHandler{ 24 | s, 25 | groupName, 26 | nil, 27 | service, 28 | middleware, 29 | validate, 30 | } 31 | cartHandler.router = cartHandler.registerGroup(middleware...) 32 | cartHandler.routes() 33 | cartHandler.registerValidator() 34 | } 35 | 36 | func (s *CartHandler) registerValidator() { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /pkg/handler/delivery/delivery.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func (s *DeliveryHandler) addDeliveryPerson(c *gin.Context) { 12 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 13 | defer cancel() 14 | 15 | var deliverPerson delivery.DeliveryPersonParams 16 | var deliverPersonModel delivery.DeliveryPerson 17 | 18 | if err := c.BindJSON(&deliverPerson); err != nil { 19 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 20 | return 21 | } 22 | 23 | deliverPersonModel.Name = deliverPerson.Name 24 | deliverPersonModel.Phone = deliverPerson.Phone 25 | deliverPersonModel.VehicleDetails = deliverPerson.VehicleDetails 26 | deliverPersonModel.Status = "AVAILABLE" 27 | 28 | authKey, authKeyURL, err := s.service.GenerateTOTP(ctx, deliverPerson.Phone) 29 | if err != nil { 30 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 31 | return 32 | } 33 | deliverPersonModel.AuthKey = authKey 34 | deliverPersonModel.AuthKeyURL = authKeyURL 35 | deliverPersonModel.IsAuthSet = false 36 | 37 | _, err = s.service.AddDeliveryPerson(ctx, &deliverPersonModel) 38 | if err != nil { 39 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 40 | return 41 | } 42 | 43 | c.JSON(http.StatusCreated, gin.H{"message": "Delivery Person Added Successfully!"}) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pkg/handler/delivery/login.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func (s *DeliveryHandler) loginDelivery(c *gin.Context) { 12 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 13 | defer cancel() 14 | var token string 15 | var deliverLoginPerson delivery.DeliveryLoginParams 16 | 17 | if err := c.BindJSON(&deliverLoginPerson); err != nil { 18 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 19 | return 20 | } 21 | 22 | verify := s.service.Verify(ctx, deliverLoginPerson.Phone, deliverLoginPerson.OTP) 23 | if !verify { 24 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Either Phone or OTP is incorrect or user is inactive. Please contact administrator."}) 25 | return 26 | } else { 27 | deliveryLoginDetails, err := s.service.ValidateAccountDetails(ctx, deliverLoginPerson.Phone) 28 | if err != nil { 29 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unable to fetch delivery person details. Please contact administrator."}) 30 | return 31 | } 32 | token, err = s.service.GenerateJWT(ctx, deliveryLoginDetails.DeliveryPersonID, deliveryLoginDetails.Name) 33 | if err != nil { 34 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unable to generate login information. Please contact administrator."}) 35 | return 36 | } 37 | 38 | } 39 | 40 | c.JSON(http.StatusCreated, gin.H{"token": token}) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/handler/delivery/order.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func (s *DeliveryHandler) updateOrder(c *gin.Context) { 13 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 14 | defer cancel() 15 | 16 | var deliveryOrder delivery.DeliveryOrderPlacementParams 17 | if err := c.BindJSON(&deliveryOrder); err != nil { 18 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 19 | return 20 | } 21 | 22 | userID := c.GetInt64("userID") 23 | 24 | _, err := s.service.OrderPlacement(ctx, userID, deliveryOrder.OrderID, deliveryOrder.Status) 25 | if err != nil { 26 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 27 | return 28 | } 29 | 30 | c.JSON(http.StatusCreated, gin.H{"message": "Order Updated!"}) 31 | 32 | } 33 | 34 | func (s *DeliveryHandler) deliveryListing(c *gin.Context) { 35 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 36 | defer cancel() 37 | 38 | orderId := c.Param("order_id") 39 | 40 | // Convert to integer 41 | orderID, _ := strconv.ParseInt(orderId, 10, 64) 42 | userID := c.GetInt64("userID") 43 | 44 | deliveries, err := s.service.DeliveryListing(ctx, orderID, userID) 45 | if err != nil { 46 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 47 | return 48 | } 49 | 50 | c.JSON(http.StatusOK, gin.H{"deliveries": deliveries}) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/handler/delivery/routes.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *DeliveryHandler) registerMiddlewareGroup(middleware ...gin.HandlerFunc) gin.IRoutes { 9 | return s.serve.Gin.Group(s.group).Use(middleware...) 10 | } 11 | 12 | func (s *DeliveryHandler) registerGroup() gin.IRoutes { 13 | return s.serve.Gin.Group(s.group) 14 | } 15 | 16 | func (s *DeliveryHandler) regularRoutes() http.Handler { 17 | s.router.POST("/add", s.addDeliveryPerson) 18 | s.router.POST("/login", s.loginDelivery) 19 | return s.serve.Gin 20 | } 21 | 22 | func (s *DeliveryHandler) middlewareRoutes() http.Handler { 23 | s.middlewareGuarded.POST("/update-order", s.updateOrder) 24 | s.middlewareGuarded.GET("/deliveries/:order_id", s.deliveryListing) 25 | return s.serve.Gin 26 | } 27 | -------------------------------------------------------------------------------- /pkg/handler/delivery/service.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/service/delivery" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-playground/validator/v10" 8 | ) 9 | 10 | type DeliveryHandler struct { 11 | serve *handler.Server 12 | group string 13 | middlewareGuarded gin.IRoutes 14 | router gin.IRoutes 15 | service *delivery.DeliveryService 16 | middleware []gin.HandlerFunc 17 | validate *validator.Validate 18 | } 19 | 20 | func NewDeliveryHandler(s *handler.Server, group string, 21 | service *delivery.DeliveryService, middleware []gin.HandlerFunc, 22 | validate *validator.Validate) { 23 | 24 | cartHandler := &DeliveryHandler{ 25 | s, 26 | group, 27 | nil, 28 | nil, 29 | service, 30 | middleware, 31 | validate, 32 | } 33 | cartHandler.middlewareGuarded = cartHandler.registerMiddlewareGroup(middleware...) 34 | cartHandler.router = cartHandler.registerGroup() 35 | cartHandler.regularRoutes() 36 | cartHandler.middlewareRoutes() 37 | cartHandler.registerValidator() 38 | } 39 | 40 | func (s *DeliveryHandler) registerValidator() { 41 | 42 | } 43 | -------------------------------------------------------------------------------- /pkg/handler/notification/notify.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "strconv" 8 | ) 9 | 10 | func (s *NotifyHandler) notifyOrders(c *gin.Context) { 11 | conn, err := s.ws.Upgrade(c.Writer, c.Request, nil) 12 | if err != nil { 13 | log.Println("Error while upgrading connection:", err) 14 | return 15 | } 16 | defer conn.Close() 17 | 18 | token := c.Query("token") 19 | if token == "" { 20 | log.Println("No Token Found!") 21 | conn.Close() 22 | return 23 | } 24 | valid, userIdInt := middleware.ValidateToken(token) 25 | if !valid { 26 | log.Println("Invalid Token!") 27 | conn.Close() 28 | return 29 | } 30 | 31 | userID := strconv.FormatInt(userIdInt, 10) 32 | 33 | s.clients[userID] = conn 34 | log.Printf("New client connected::%s", userID) 35 | 36 | for { 37 | _, _, err := conn.ReadMessage() 38 | if err != nil { 39 | log.Printf("Client disconnected: %s::%v", userID, err) 40 | delete(s.clients, userID) 41 | break 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/handler/notification/routes.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *NotifyHandler) registerMiddlewareGroup(middleware ...gin.HandlerFunc) gin.IRoutes { 9 | return s.serve.Gin.Group(s.group).Use(middleware...) 10 | } 11 | 12 | func (s *NotifyHandler) registerGroup() gin.IRoutes { 13 | return s.serve.Gin.Group(s.group) 14 | } 15 | 16 | func (s *NotifyHandler) regularRoutes() http.Handler { 17 | s.router.GET("/ws", s.notifyOrders) 18 | return s.serve.Gin 19 | } 20 | 21 | func (s *NotifyHandler) middlewareRoutes() http.Handler { 22 | 23 | return s.serve.Gin 24 | } 25 | -------------------------------------------------------------------------------- /pkg/handler/notification/service.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/service/notification" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-playground/validator/v10" 8 | "github.com/gorilla/websocket" 9 | "net/http" 10 | ) 11 | 12 | type NotifyHandler struct { 13 | serve *handler.Server 14 | group string 15 | middlewareGuarded gin.IRoutes 16 | router gin.IRoutes 17 | service *notification.NotificationService 18 | middleware []gin.HandlerFunc 19 | validate *validator.Validate 20 | ws *websocket.Upgrader 21 | clients map[string]*websocket.Conn 22 | } 23 | 24 | func NewNotifyHandler(s *handler.Server, group string, 25 | service *notification.NotificationService, middleware []gin.HandlerFunc, 26 | validate *validator.Validate, clients map[string]*websocket.Conn) { 27 | 28 | // WebSocket 29 | var ws = &websocket.Upgrader{ 30 | CheckOrigin: func(r *http.Request) bool { 31 | return true 32 | }, 33 | } 34 | 35 | cartHandler := &NotifyHandler{ 36 | s, 37 | group, 38 | nil, 39 | nil, 40 | service, 41 | middleware, 42 | validate, 43 | ws, 44 | clients, 45 | } 46 | cartHandler.middlewareGuarded = cartHandler.registerMiddlewareGroup(middleware...) 47 | cartHandler.router = cartHandler.registerGroup() 48 | cartHandler.regularRoutes() 49 | cartHandler.middlewareRoutes() 50 | cartHandler.registerValidator() 51 | } 52 | 53 | func (s *NotifyHandler) registerValidator() { 54 | 55 | } 56 | -------------------------------------------------------------------------------- /pkg/handler/restaurant/menu.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func (s *RestaurantHandler) addMenu(c *gin.Context) { 13 | ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) 14 | defer cancel() 15 | 16 | var menuItem restaurant.MenuItem 17 | if err := c.BindJSON(&menuItem); err != nil { 18 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 19 | return 20 | } 21 | menuObject, err := s.service.AddMenu(ctx, &menuItem) 22 | if err != nil { 23 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 24 | return 25 | } else { 26 | // Update Photo from UnSplash 27 | s.service.UpdateMenuPhoto(ctx, menuObject) 28 | } 29 | 30 | c.JSON(http.StatusCreated, gin.H{"message": "New Menu Added!"}) 31 | } 32 | 33 | func (s *RestaurantHandler) listMenus(c *gin.Context) { 34 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 35 | defer cancel() 36 | restaurantId := c.Query("restaurant_id") 37 | if restaurantId == "" { 38 | results, err := s.service.ListAllMenus(ctx) 39 | if err != nil { 40 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 41 | return 42 | } 43 | c.JSON(http.StatusOK, results) 44 | return 45 | 46 | } else { 47 | restaurantID, err := strconv.ParseInt(restaurantId, 10, 64) 48 | if err != nil { 49 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Invalid RestaurantID"}) 50 | return 51 | } 52 | results, err := s.service.ListMenus(ctx, restaurantID) 53 | if err != nil { 54 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 55 | return 56 | } 57 | 58 | if len(results) == 0 { 59 | c.JSON(http.StatusNotFound, gin.H{"error": "No results found"}) 60 | return 61 | } 62 | c.JSON(http.StatusOK, results) 63 | return 64 | } 65 | } 66 | 67 | func (s *RestaurantHandler) deleteMenu(c *gin.Context) { 68 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 69 | defer cancel() 70 | 71 | menuId, err := strconv.ParseInt(c.Param("menu_id"), 10, 64) 72 | if err != nil { 73 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Invalid MenuID"}) 74 | return 75 | } 76 | restaurantId, err := strconv.ParseInt(c.Param("restaurant_id"), 10, 64) 77 | if err != nil { 78 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Invalid RestaurantID"}) 79 | return 80 | } 81 | 82 | _, err = s.service.DeleteMenu(ctx, menuId, restaurantId) 83 | if err != nil { 84 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 85 | return 86 | } 87 | 88 | c.Status(http.StatusNoContent) 89 | 90 | } 91 | -------------------------------------------------------------------------------- /pkg/handler/restaurant/restaurant.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | restaurantModel "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | func (s *RestaurantHandler) addRestaurant(c *gin.Context) { 16 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 17 | defer cancel() 18 | _ = ctx 19 | 20 | file, fileHeader, err := c.Request.FormFile("file") 21 | if err != nil { 22 | c.JSON(http.StatusBadRequest, gin.H{"error": "File is required"}) 23 | return 24 | } 25 | 26 | originalFileName := fileHeader.Filename 27 | 28 | // Generate a new file name 29 | newFileName := generateFileName(originalFileName) 30 | 31 | _, err = s.Serve.Storage.Upload(newFileName, file) 32 | if err != nil { 33 | slog.Error("Error", "addRestaurant", err.Error()) 34 | } 35 | 36 | uploadedFile := filepath.Join(os.Getenv("STORAGE_DIRECTORY"), newFileName) 37 | 38 | var restaurant restaurantModel.Restaurant 39 | restaurant.Name = c.PostForm("name") 40 | restaurant.Description = c.PostForm("description") 41 | restaurant.Address = c.PostForm("address") 42 | restaurant.City = c.PostForm("city") 43 | restaurant.State = c.PostForm("state") 44 | restaurant.Photo = uploadedFile 45 | 46 | _, err = s.service.Add(ctx, &restaurant) 47 | if err != nil { 48 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 49 | return 50 | } 51 | c.JSON(http.StatusCreated, gin.H{"message": "Restaurant created successfully"}) 52 | } 53 | 54 | func (s *RestaurantHandler) listRestaurants(c *gin.Context) { 55 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 56 | defer cancel() 57 | 58 | results, err := s.service.ListRestaurants(ctx) 59 | if err != nil { 60 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 61 | return 62 | } 63 | if results == nil { 64 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "No restaurants found"}) 65 | return 66 | } 67 | c.JSON(http.StatusOK, results) 68 | } 69 | 70 | func (s *RestaurantHandler) listRestaurantById(c *gin.Context) { 71 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 72 | defer cancel() 73 | restaurantId := c.Param("id") 74 | restaurantID, _ := strconv.ParseInt(restaurantId, 10, 64) 75 | 76 | result, err := s.service.ListRestaurantById(ctx, restaurantID) 77 | if err != nil { 78 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 79 | return 80 | } 81 | c.JSON(http.StatusOK, result) 82 | } 83 | 84 | func (s *RestaurantHandler) deleteRestaurant(c *gin.Context) { 85 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 86 | defer cancel() 87 | 88 | restaurantId := c.Param("id") 89 | 90 | // Convert to integer 91 | restaurantID, _ := strconv.ParseInt(restaurantId, 10, 64) 92 | 93 | _, err := s.service.DeleteRestaurant(ctx, restaurantID) 94 | if err != nil { 95 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 96 | return 97 | } 98 | 99 | c.Status(http.StatusNoContent) 100 | 101 | } 102 | -------------------------------------------------------------------------------- /pkg/handler/restaurant/routes.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *RestaurantHandler) registerGroup() *gin.RouterGroup { 9 | return s.Serve.Gin.Group(s.group) 10 | } 11 | 12 | func (s *RestaurantHandler) routes() http.Handler { 13 | s.router.POST("/", s.addRestaurant) 14 | s.router.GET("/", s.listRestaurants) 15 | s.router.GET("/:id", s.listRestaurantById) 16 | s.router.DELETE("/:id", s.deleteRestaurant) 17 | s.router.POST("/menu", s.addMenu) 18 | s.router.GET("/menu", s.listMenus) 19 | s.router.DELETE("/menu/:restaurant_id/:menu_id", s.deleteMenu) 20 | return s.Serve.Gin 21 | } 22 | -------------------------------------------------------------------------------- /pkg/handler/restaurant/service.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/service/restaurant" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type RestaurantHandler struct { 10 | Serve *handler.Server 11 | group string 12 | router *gin.RouterGroup 13 | service *restaurant.RestaurantService 14 | } 15 | 16 | func NewRestaurantHandler(s *handler.Server, groupName string, service *restaurant.RestaurantService) { 17 | 18 | restroHandler := &RestaurantHandler{ 19 | s, 20 | groupName, 21 | &gin.RouterGroup{}, 22 | service, 23 | } 24 | 25 | restroHandler.router = restroHandler.registerGroup() 26 | restroHandler.routes() 27 | } 28 | -------------------------------------------------------------------------------- /pkg/handler/restaurant/utils.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func generateFileName(originalFileName string) string { 12 | timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) 13 | extension := filepath.Ext(originalFileName) 14 | nameWithoutExt := strings.TrimSuffix(originalFileName, extension) 15 | newFileName := fmt.Sprintf("%s_%s%s", timestamp, nameWithoutExt, extension) 16 | return newFileName 17 | } 18 | -------------------------------------------------------------------------------- /pkg/handler/review/review.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | reviewModel "Go_Food_Delivery/pkg/database/models/review" 5 | "context" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func (s *ReviewProtectedHandler) addReview(c *gin.Context) { 13 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 14 | defer cancel() 15 | 16 | userID := c.GetInt64("userID") 17 | restaurantId, err := strconv.ParseInt(c.Param("restaurant_id"), 10, 64) 18 | if err != nil { 19 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Invalid RestaurantID"}) 20 | return 21 | } 22 | 23 | var reviewParam reviewModel.ReviewParams 24 | var review reviewModel.Review 25 | if err := c.BindJSON(&reviewParam); err != nil { 26 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 27 | return 28 | } 29 | 30 | if err := s.validate.Struct(reviewParam); err != nil { 31 | validationError := reviewModel.ReviewValidationError(err) 32 | c.JSON(http.StatusBadRequest, gin.H{"error": validationError}) 33 | return 34 | } 35 | 36 | comment := reviewParam.Comment 37 | rating := reviewParam.Rating 38 | review.UserID = userID 39 | review.RestaurantID = restaurantId 40 | review.Rating = rating 41 | review.Comment = comment 42 | _, err = s.service.Add(ctx, &review) 43 | if err != nil { 44 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 45 | } 46 | c.JSON(http.StatusCreated, gin.H{"message": "Review Added!"}) 47 | 48 | } 49 | 50 | func (s *ReviewProtectedHandler) listReviews(c *gin.Context) { 51 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 52 | defer cancel() 53 | 54 | restaurantId, err := strconv.ParseInt(c.Param("restaurant_id"), 10, 64) 55 | if err != nil { 56 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "Invalid RestaurantID"}) 57 | return 58 | } 59 | 60 | results, err := s.service.ListReviews(ctx, restaurantId) 61 | if err != nil { 62 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 63 | return 64 | } 65 | if len(results) == 0 { 66 | c.JSON(http.StatusNotFound, gin.H{"error": "No results found"}) 67 | return 68 | } 69 | c.JSON(http.StatusOK, results) 70 | } 71 | 72 | func (s *ReviewProtectedHandler) deleteReview(c *gin.Context) { 73 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 74 | defer cancel() 75 | 76 | reviewId := c.Param("review_id") 77 | userID := c.GetInt64("userID") 78 | 79 | // Convert to integer 80 | reviewID, _ := strconv.ParseInt(reviewId, 10, 64) 81 | 82 | _, err := s.service.DeleteReview(ctx, reviewID, userID) 83 | if err != nil { 84 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 85 | return 86 | } 87 | 88 | c.Status(http.StatusNoContent) 89 | 90 | } 91 | -------------------------------------------------------------------------------- /pkg/handler/review/routes.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *ReviewProtectedHandler) registerGroup(middleware ...gin.HandlerFunc) gin.IRoutes { 9 | return s.serve.Gin.Group(s.group).Use(middleware...) 10 | } 11 | 12 | func (s *ReviewProtectedHandler) routes() http.Handler { 13 | s.router.POST("/:restaurant_id", s.addReview) 14 | s.router.GET("/:restaurant_id", s.listReviews) 15 | s.router.DELETE("/:review_id", s.deleteReview) 16 | 17 | return s.serve.Gin 18 | } 19 | -------------------------------------------------------------------------------- /pkg/handler/review/service.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | reviewValidate "Go_Food_Delivery/pkg/database/models/review" 5 | "Go_Food_Delivery/pkg/handler" 6 | "Go_Food_Delivery/pkg/service/review" 7 | "github.com/gin-gonic/gin" 8 | "github.com/go-playground/validator/v10" 9 | "log/slog" 10 | ) 11 | 12 | type ReviewProtectedHandler struct { 13 | serve *handler.Server 14 | group string 15 | router gin.IRoutes 16 | service *review.ReviewService 17 | middleware []gin.HandlerFunc 18 | validate *validator.Validate 19 | } 20 | 21 | func NewReviewProtectedHandler(s *handler.Server, groupName string, 22 | service *review.ReviewService, middleware []gin.HandlerFunc, validate *validator.Validate) { 23 | 24 | reviewHandler := &ReviewProtectedHandler{ 25 | s, 26 | groupName, 27 | nil, 28 | service, 29 | middleware, 30 | validate, 31 | } 32 | 33 | reviewHandler.router = reviewHandler.registerGroup(middleware...) 34 | reviewHandler.routes() 35 | reviewHandler.registerValidator() 36 | 37 | } 38 | 39 | func (s *ReviewProtectedHandler) registerValidator() { 40 | err := s.validate.RegisterValidation("rating", reviewValidate.RatingValidator) 41 | if err != nil { 42 | slog.Error("registerValidator", "NewReviewProtectedHandler", err) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pkg/handler/server.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/storage" 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | sloggin "github.com/samber/slog-gin" 9 | "log/slog" 10 | "os" 11 | ) 12 | 13 | type Server struct { 14 | Gin *gin.Engine 15 | db database.Database 16 | Storage storage.ImageStorage 17 | } 18 | 19 | func NewServer(db database.Database, setLog bool) *Server { 20 | 21 | ginEngine := gin.New() 22 | 23 | // CORS configuration 24 | corsConfig := cors.Config{ 25 | AllowOrigins: []string{"*"}, // List of allowed origins 26 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, // List of allowed methods 27 | AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow the necessary headers 28 | AllowCredentials: true, 29 | } 30 | 31 | // Setting Logger, CORS & MultipartMemory 32 | if setLog { 33 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 34 | ginEngine.Use(sloggin.New(logger)) 35 | } 36 | ginEngine.Use(gin.Recovery()) 37 | ginEngine.Use(cors.New(corsConfig)) 38 | ginEngine.MaxMultipartMemory = 8 << 20 // 8 MB 39 | 40 | localStoragePath := os.Getenv("LOCAL_STORAGE_PATH") 41 | if len(localStoragePath) > 0 { 42 | // Set static path 43 | ginEngine.Static(os.Getenv("STORAGE_DIRECTORY"), localStoragePath) 44 | } 45 | 46 | return &Server{ 47 | Gin: ginEngine, 48 | db: db, 49 | Storage: storage.CreateImageStorage(os.Getenv("STORAGE_TYPE")), 50 | } 51 | } 52 | 53 | func (server *Server) Run() error { 54 | return server.Gin.Run(":8080") 55 | } 56 | -------------------------------------------------------------------------------- /pkg/handler/user/routes.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func (s *UserHandler) registerGroup() *gin.RouterGroup { 9 | return s.Serve.Gin.Group(s.group) 10 | } 11 | 12 | func (s *UserHandler) routes() http.Handler { 13 | s.router.POST("/", s.addUser) 14 | s.router.DELETE("/:id", s.deleteUser) 15 | s.router.POST("/login", s.loginUser) 16 | return s.Serve.Gin 17 | } 18 | -------------------------------------------------------------------------------- /pkg/handler/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | userValidate "Go_Food_Delivery/pkg/database/models/user" 5 | "Go_Food_Delivery/pkg/handler" 6 | "Go_Food_Delivery/pkg/service/user" 7 | "github.com/gin-gonic/gin" 8 | "github.com/go-playground/validator/v10" 9 | ) 10 | 11 | type UserHandler struct { 12 | Serve *handler.Server 13 | group string 14 | router *gin.RouterGroup 15 | service *user.UsrService 16 | validate *validator.Validate 17 | } 18 | 19 | func NewUserHandler(s *handler.Server, groupName string, service *user.UsrService, validate *validator.Validate) { 20 | 21 | usrHandler := &UserHandler{ 22 | s, 23 | groupName, 24 | &gin.RouterGroup{}, 25 | service, 26 | validate, 27 | } 28 | usrHandler.router = usrHandler.registerGroup() 29 | usrHandler.routes() 30 | usrHandler.registerValidator() 31 | } 32 | 33 | func (s *UserHandler) registerValidator() { 34 | _ = s.validate.RegisterValidation("name", userValidate.NameValidator) 35 | _ = s.validate.RegisterValidation("email", userValidate.EmailValidator) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/handler/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | userModel "Go_Food_Delivery/pkg/database/models/user" 5 | database "Go_Food_Delivery/pkg/service/user" 6 | "context" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | func (s *UserHandler) addUser(c *gin.Context) { 14 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 15 | defer cancel() 16 | 17 | var user userModel.User 18 | if err := c.BindJSON(&user); err != nil { 19 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 20 | return 21 | } 22 | 23 | if err := s.validate.Struct(user); err != nil { 24 | validationError := userModel.UserValidationError(err) 25 | c.JSON(http.StatusBadRequest, gin.H{"error": validationError}) 26 | return 27 | } 28 | 29 | _, err := s.service.Add(ctx, &user) 30 | if err != nil { 31 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 32 | return 33 | } 34 | 35 | c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) 36 | 37 | } 38 | 39 | func (s *UserHandler) deleteUser(c *gin.Context) { 40 | ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 41 | defer cancel() 42 | 43 | userId := c.Param("id") 44 | 45 | // Convert to integer 46 | userID, _ := strconv.ParseInt(userId, 10, 64) 47 | 48 | _, err := s.service.Delete(ctx, userID) 49 | if err != nil { 50 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 51 | return 52 | } 53 | 54 | c.Status(http.StatusNoContent) 55 | 56 | } 57 | 58 | func (s *UserHandler) loginUser(c *gin.Context) { 59 | _, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) 60 | defer cancel() 61 | 62 | var user userModel.LoginUser 63 | if err := c.BindJSON(&user); err != nil { 64 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 65 | return 66 | } 67 | 68 | login := database.ValidateAccount(s.service.Login, s.service.UserExist, s.service.ValidatePassword) 69 | result, err := login(c, &userModel.LoginUser{Email: user.Email, Password: user.Password}) 70 | if err != nil { 71 | c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) 72 | return 73 | } 74 | c.JSON(http.StatusOK, gin.H{"token": result}) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/nats/nats_server.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "github.com/nats-io/nats.go" 6 | "log" 7 | "log/slog" 8 | "strings" 9 | ) 10 | 11 | type NATS struct { 12 | Conn *nats.Conn 13 | } 14 | 15 | func NewNATS(url string) (*NATS, error) { 16 | nc, err := nats.Connect(url, nats.Name("food-delivery-nats")) 17 | if err != nil { 18 | log.Fatalf("Error connecting to NATS:: %s", err) 19 | } 20 | return &NATS{Conn: nc}, err 21 | } 22 | 23 | func (n *NATS) Pub(topic string, message []byte) error { 24 | err := n.Conn.Publish(topic, message) 25 | if err != nil { 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | func (n *NATS) Sub(topic string, clients map[string]*websocket.Conn) error { 32 | 33 | _, err := n.Conn.Subscribe(topic, func(msg *nats.Msg) { 34 | message := string(msg.Data) 35 | slog.Info("MESSAGE_REPLY_FROM_NATS", "RECEIVED_MESSAGE", message) 36 | userId, messageData := n.formatMessage(message) 37 | if conn, ok := clients[userId]; ok { 38 | err := conn.WriteMessage(websocket.TextMessage, []byte(messageData)) 39 | if err != nil { 40 | log.Println("Error sending message to client:", err) 41 | conn.Close() 42 | delete(clients, userId) 43 | } 44 | } 45 | }) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func (n *NATS) formatMessage(message string) (userId string, messageData string) { 53 | parts := strings.Split(message, "|") 54 | result := make(map[string]string) 55 | for _, part := range parts { 56 | kv := strings.SplitN(part, ":", 2) // Split into key and value 57 | if len(kv) == 2 { 58 | result[kv[0]] = kv[1] // Store in a map 59 | } 60 | } 61 | return result["USER_ID"], result["MESSAGE"] 62 | } 63 | -------------------------------------------------------------------------------- /pkg/service/announcements/announcement.go: -------------------------------------------------------------------------------- 1 | package announcements 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/abstract/announcements" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "runtime" 12 | ) 13 | 14 | func (eventSrv *AnnouncementService) FlashEvents() (*[]announcements.FlashEvents, error) { 15 | currentDir, err := getCurrentDirectory() 16 | if err != nil { 17 | return nil, errors.New("unable to get current directory") 18 | } 19 | fileName := filepath.Join(currentDir, "events.json") 20 | 21 | file, err := os.Open(fileName) 22 | if err != nil { 23 | log.Println("Error opening file:", err) 24 | return nil, errors.New("unable to open file") 25 | } 26 | defer file.Close() 27 | 28 | var newsEvents []announcements.FlashEvents 29 | err = json.NewDecoder(file).Decode(&newsEvents) 30 | if err != nil { 31 | return nil, errors.New("unable to decode JSON") 32 | } 33 | 34 | return &newsEvents, nil 35 | } 36 | 37 | func getCurrentDirectory() (string, error) { 38 | // Get the caller's information 39 | _, currentFile, _, ok := runtime.Caller(1) 40 | if !ok { 41 | return "", fmt.Errorf("unable to get current file path") 42 | } 43 | 44 | // Get the directory from the current file path 45 | currentDir := filepath.Dir(currentFile) 46 | 47 | return currentDir, nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/service/announcements/events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "headline": "New Local Flavors", 4 | "message": "🚀 Introducing exclusive menu items from your favorite local restaurants! Discover new tastes every week!" 5 | }, 6 | { 7 | "headline": "Limited-Time Offers", 8 | "message": "🎉 Get 20% off your next order! Use code TASTY20 at checkout. Offer valid until Sunday!" 9 | }, 10 | { 11 | "headline": "Sustainability Initiative", 12 | "message": "🌍 We’re going green! Starting this month, all deliveries will feature eco-friendly packaging. Join us in making a difference!" 13 | }, 14 | { 15 | "headline": "New Partnerships", 16 | "message": "🍔 We’ve partnered with McDonald! Enjoy their delicious dishes delivered straight to your door!" 17 | }, 18 | { 19 | "headline": "Loyalty Program Launch", 20 | "message": "🏆 Introducing our new loyalty program! Earn points for every order and redeem them for exclusive rewards!" 21 | }, 22 | { 23 | "headline": "Delivery Speed Upgrade", 24 | "message": "⚡ We’ve optimized our delivery network! Expect your meals faster than ever—hot and fresh at your doorstep!" 25 | }, 26 | { 27 | "headline": "Customer Feedback Drive", 28 | "message": "📝 We value your input! Participate in our survey for a chance to win a $50 gift card!" 29 | }, 30 | { 31 | "headline": "Meal Kits Now Available", 32 | "message": "🍽️ Craving a cooking adventure? Try our new meal kits! Fresh ingredients delivered with easy-to-follow recipes!" 33 | }, 34 | { 35 | "headline": "New Cuisine Alert", 36 | "message": "🥢 Explore the flavors of Sushi! Now available for delivery—order today!" 37 | }, 38 | { 39 | "headline": "Weekend Specials", 40 | "message": "🍕 Enjoy our weekend special! Buy one pizza, get one half off! Limited time only!" 41 | }, 42 | { 43 | "headline": "App Update", 44 | "message": "📱 Check out our new app features! Easier navigation and faster checkout for a smoother experience!" 45 | }, 46 | { 47 | "headline": "Chef's Recommendation", 48 | "message": "👨‍🍳 Try our chef's special this week! A must-taste dish from [Restaurant Name]!" 49 | }, 50 | { 51 | "headline": "Free Delivery Day", 52 | "message": "🚚 This Thursday, enjoy FREE delivery on all orders over $30! Don’t miss out!" 53 | }, 54 | { 55 | "headline": "Seasonal Menu Launch", 56 | "message": "🍂 Our fall menu is here! Discover seasonal dishes that celebrate the flavors of autumn!" 57 | }, 58 | { 59 | "headline": "Happy Hour Deals", 60 | "message": "🍹 Join us for happy hour! Enjoy 50% off select drinks from 4 PM to 6 PM!" 61 | }, 62 | { 63 | "headline": "Referral Bonus", 64 | "message": "👥 Refer a friend and both of you get $10 off your next order! Share the love!" 65 | }, 66 | { 67 | "headline": "Exclusive Chef Collaboration", 68 | "message": "✨ We’re teaming up with [Famous Chef] for a limited-time menu! Don’t miss these exclusive dishes!" 69 | }, 70 | { 71 | "headline": "Healthy Choices", 72 | "message": "🥗 Looking for something light? Check out our new range of healthy meal options!" 73 | }, 74 | { 75 | "headline": "Breakfast All Day", 76 | "message": "🌞 Breakfast lovers rejoice! Enjoy breakfast items all day long, any day of the week!" 77 | }, 78 | { 79 | "headline": "Limited Edition Desserts", 80 | "message": "🍰 Indulge in our limited-edition desserts available for this month only!" 81 | }, 82 | { 83 | "headline": "Community Support", 84 | "message": "🤝 We're donating a portion of every order this month to local charities. Help us give back!" 85 | }, 86 | { 87 | "headline": "Curbside Pickup Now Available", 88 | "message": "🚗 Order online and enjoy convenient curbside pickup at select locations!" 89 | }, 90 | { 91 | "headline": "New Payment Options", 92 | "message": "💳 We now accept [Payment Method]! Enjoy more flexibility with your orders!" 93 | }, 94 | { 95 | "headline": "Food Truck Festival", 96 | "message": "🎪 Join us for the upcoming food truck festival! Sample dishes from various local food trucks!" 97 | }, 98 | { 99 | "headline": "Customer Appreciation Week", 100 | "message": "🎊 It’s customer appreciation week! Enjoy surprise discounts and giveaways all week long!" 101 | }, 102 | { 103 | "headline": "Online Cooking Classes", 104 | "message": "👩‍🍳 Join our online cooking classes with professional chefs and learn to create your favorite dishes!" 105 | }, 106 | { 107 | "headline": "Flash Sales", 108 | "message": "⚡ Don’t miss our flash sales! Follow us for exclusive 1-hour deals throughout the week!" 109 | }, 110 | { 111 | "headline": "Family Meal Bundles", 112 | "message": "👨‍👩‍👧‍👦 Enjoy our family meal bundles! Perfect for sharing with loved ones at great prices!" 113 | }, 114 | { 115 | "headline": "Late Night Delivery", 116 | "message": "🌙 Craving a midnight snack? We now offer late-night delivery on select items!" 117 | }, 118 | { 119 | "headline": "Global Flavors Series", 120 | "message": "🌎 Dive into our Global Flavors series—each week featuring a different country’s cuisine!" 121 | }, 122 | { 123 | "headline": "New Dessert Line", 124 | "message": "🍦 Satisfy your sweet tooth with our new dessert line! Available for delivery now!" 125 | } 126 | ] 127 | -------------------------------------------------------------------------------- /pkg/service/announcements/service.go: -------------------------------------------------------------------------------- 1 | package announcements 2 | 3 | import "Go_Food_Delivery/pkg/database" 4 | 5 | type AnnouncementService struct { 6 | db database.Database 7 | env string 8 | } 9 | 10 | func NewAnnouncementService(db database.Database, env string) *AnnouncementService { 11 | return &AnnouncementService{db, env} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/service/cart_order/add_item_to_cart.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "context" 6 | ) 7 | 8 | func (cartSrv *CartService) AddItem(ctx context.Context, Item *cart.CartItems) (*cart.CartItems, error) { 9 | _, err := cartSrv.db.Insert(ctx, Item) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return Item, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/cart_order/create_cart.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "context" 6 | ) 7 | 8 | func (cartSrv *CartService) Create(ctx context.Context, cart *cart.Cart) (*cart.Cart, error) { 9 | _, err := cartSrv.db.Insert(ctx, cart) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return cart, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/cart_order/delete_item_from_cart.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | ) 7 | 8 | func (cartSrv *CartService) DeleteItem(ctx context.Context, cartItemId int64) (bool, error) { 9 | filter := database.Filter{"cart_item_id": cartItemId} 10 | 11 | _, err := cartSrv.db.Delete(ctx, "cart_items", filter) 12 | if err != nil { 13 | return false, err 14 | } 15 | return true, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/cart_order/delivery_info.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | ) 7 | 8 | func (cartSrv *CartService) DeliveryInformation(ctx context.Context, orderId int64, userId int64) (*[]delivery.DeliveryListResponse, error) { 9 | var deliveryList []delivery.DeliveryListResponse 10 | 11 | err := cartSrv.db.Raw(ctx, &deliveryList, ` 12 | SELECT d.delivery_id, d.delivery_person_id, 13 | d.delivery_status, d.delivery_time, d.created_at, 14 | dp.name, dp.vehicle_details, dp.phone 15 | FROM deliveries AS d 16 | JOIN orders AS o ON d.order_id = o.order_id 17 | JOIN delivery_person AS dp ON d.delivery_person_id = dp.delivery_person_id 18 | WHERE o.user_id = ? AND d.order_id = ?;`, 19 | userId, orderId) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &deliveryList, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/service/cart_order/get_cart.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/cart" 5 | "context" 6 | ) 7 | 8 | func (cartSrv *CartService) GetCartId(ctx context.Context, UserId int64) (*cart.Cart, error) { 9 | var cartInfo cart.Cart 10 | 11 | err := cartSrv.db.Select(ctx, &cartInfo, "user_id", UserId) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return &cartInfo, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/cart_order/get_items_in_cart.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/database/models/cart" 6 | "context" 7 | ) 8 | 9 | func (cartSrv *CartService) ListItems(ctx context.Context, cartId int64) (*[]cart.CartItems, error) { 10 | var cartItems []cart.CartItems 11 | var relatedFields = []string{"Cart", "Restaurant", "MenuItem"} 12 | whereFilter := database.Filter{"cart_items.cart_id": cartId} 13 | err := cartSrv.db.SelectWithRelation(ctx, &cartItems, relatedFields, whereFilter) 14 | 15 | if err != nil { 16 | return nil, err 17 | } 18 | return &cartItems, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/service/cart_order/order_list.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/database/models/order" 6 | "context" 7 | "errors" 8 | ) 9 | 10 | func (cartSrv *CartService) OrderList(ctx context.Context, userId int64) (*[]order.Order, error) { 11 | var ordersList []order.Order 12 | 13 | err := cartSrv.db.Select(ctx, &ordersList, "user_id", userId) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return &ordersList, nil 18 | } 19 | 20 | func (cartSrv *CartService) OrderItemsList(ctx context.Context, userId int64, orderId int64) (*[]order.OrderItems, error) { 21 | var ordersItemsList []order.OrderItems 22 | 23 | count, err := cartSrv.db.Count(ctx, "orders", "COUNT(*)", "user_id", userId) 24 | if err != nil { 25 | return nil, errors.New("invalid order") 26 | } 27 | 28 | if count == 0 { 29 | return nil, errors.New("invalid order. No order found") 30 | } 31 | 32 | var relatedFields = []string{"Restaurant", "MenuItem"} 33 | whereFilter := database.Filter{"order_id": orderId} 34 | 35 | err = cartSrv.db.SelectWithRelation(ctx, &ordersItemsList, relatedFields, whereFilter) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return &ordersItemsList, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/service/cart_order/place_order.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/database/models/cart" 6 | "Go_Food_Delivery/pkg/database/models/order" 7 | "context" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | func (cartSrv *CartService) PlaceOrder(ctx context.Context, cartId int64, userId int64) (*order.Order, error) { 13 | var cartItems []cart.CartItems 14 | var newOrder order.Order 15 | var newOrderItems []order.OrderItems 16 | var orderTotal float64 = 0.0 17 | var relatedFields = []string{"MenuItem"} 18 | whereFilter := database.Filter{"cart_items.cart_id": cartId} 19 | 20 | err := cartSrv.db.SelectWithRelation(ctx, &cartItems, relatedFields, whereFilter) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | if len(cartItems) == 0 { 26 | return nil, errors.New("no items in cart") 27 | } 28 | 29 | // Creating a new order. 30 | newOrder.UserID = userId 31 | newOrder.OrderStatus = "pending" 32 | newOrder.TotalAmount = orderTotal 33 | newOrder.DeliveryAddress = "New Delhi" 34 | 35 | _, err = cartSrv.db.Insert(ctx, &newOrder) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | newOrderItems = make([]order.OrderItems, len(cartItems)) 41 | for i, cartItem := range cartItems { 42 | newOrderItems[i].OrderID = newOrder.OrderID 43 | newOrderItems[i].ItemID = cartItem.ItemID 44 | newOrderItems[i].RestaurantID = cartItem.RestaurantID 45 | newOrderItems[i].Quantity = cartItem.Quantity 46 | newOrderItems[i].Price = cartItem.MenuItem.Price * float64(cartItem.Quantity) 47 | _, err = cartSrv.db.Insert(ctx, &newOrderItems[i]) 48 | if err != nil { 49 | return nil, err 50 | } 51 | orderTotal += newOrderItems[i].Price 52 | } 53 | 54 | _, err = cartSrv.db.Update(ctx, "orders", database.Filter{"total_amount": orderTotal, "order_status": "in_progress"}, 55 | database.Filter{"order_id": newOrder.OrderID}) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &newOrder, nil 61 | 62 | } 63 | 64 | func (cartSrv *CartService) RemoveItemsFromCart(ctx context.Context, cartId int64) error { 65 | //remove all items from the cart. 66 | filter := database.Filter{"cart_id": cartId} 67 | _, err := cartSrv.db.Delete(ctx, "cart_items", filter) 68 | if err != nil { 69 | return errors.New("failed to delete cart items") 70 | } 71 | return nil 72 | } 73 | 74 | func (cartSrv *CartService) NewOrderPlacedNotification(userId int64, orderId int64) error { 75 | message := fmt.Sprintf("USER_ID:%d|MESSAGE:Your order number %d has been successfully placed, and the chef has begun the cooking process.", userId, orderId) 76 | topic := fmt.Sprintf("orders.new.%d", userId) 77 | err := cartSrv.nats.Pub(topic, []byte(message)) 78 | if err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/service/cart_order/service.go: -------------------------------------------------------------------------------- 1 | package cart_order 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/nats" 6 | ) 7 | 8 | type CartService struct { 9 | db database.Database 10 | env string 11 | nats *nats.NATS 12 | } 13 | 14 | func NewCartService(db database.Database, env string, nats *nats.NATS) *CartService { 15 | return &CartService{db, env, nats} 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/delivery/2fa.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | "Go_Food_Delivery/pkg/database/models/delivery" 6 | "context" 7 | "errors" 8 | "github.com/golang-jwt/jwt/v5" 9 | "github.com/pquerna/otp/totp" 10 | "log/slog" 11 | "os" 12 | "time" 13 | ) 14 | 15 | func (deliverSrv *DeliveryService) GenerateTOTP(_ context.Context, phone string) (string, string, error) { 16 | key, err := totp.Generate(totp.GenerateOpts{ 17 | Issuer: "Food Delivery", 18 | AccountName: phone, 19 | }) 20 | if err != nil { 21 | return "", "", errors.New("error generating key") 22 | } 23 | return key.Secret(), key.URL(), nil 24 | } 25 | 26 | func (deliverSrv *DeliveryService) ValidateAccountDetails(ctx context.Context, phone string) (*delivery.DeliveryPerson, error) { 27 | var deliveryAccountInfo delivery.DeliveryPerson 28 | err := deliverSrv.db.Select(ctx, &deliveryAccountInfo, "phone", phone) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if deliveryAccountInfo.Status != "AVAILABLE" { 33 | return nil, errors.New("account is inactive or not available") 34 | } 35 | return &deliveryAccountInfo, nil 36 | } 37 | 38 | func (deliverSrv *DeliveryService) ValidateOTP(_ context.Context, secretKey string, otp string) bool { 39 | return totp.Validate(otp, secretKey) 40 | } 41 | 42 | func (deliverSrv *DeliveryService) Verify(ctx context.Context, phone string, otp string) bool { 43 | accDetail, err := deliverSrv.ValidateAccountDetails(ctx, phone) 44 | if err != nil { 45 | slog.Error("Error::validating account details", "err", err) 46 | return false 47 | } 48 | 49 | valid := deliverSrv.ValidateOTP(ctx, accDetail.AuthKey, otp) 50 | return valid 51 | } 52 | 53 | func (deliverSrv *DeliveryService) GenerateJWT(_ context.Context, userId int64, name string) (string, error) { 54 | 55 | claims := middleware.UserClaims{UserID: userId, Name: name, 56 | RegisteredClaims: jwt.RegisteredClaims{ 57 | 58 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(2))), 59 | Issuer: "Go_Food_Delivery", 60 | }} 61 | 62 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 63 | return token.SignedString([]byte(os.Getenv("JWT_SECRET_KEY"))) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/service/delivery/add_delivery_person.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/delivery" 5 | "context" 6 | ) 7 | 8 | func (deliverSrv *DeliveryService) AddDeliveryPerson(ctx context.Context, deliveryPerson *delivery.DeliveryPerson) (bool, error) { 9 | _, err := deliverSrv.db.Insert(ctx, deliveryPerson) 10 | if err != nil { 11 | return false, err 12 | } 13 | return true, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/delivery/deliveries_listing.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/database/models/delivery" 6 | "context" 7 | ) 8 | 9 | func (deliverSrv *DeliveryService) DeliveryListing(ctx context.Context, orderID int64, userID int64) (*[]delivery.Deliveries, error) { 10 | var deliveriesList []delivery.Deliveries 11 | whereFilter := database.Filter{"order_id": orderID, "delivery_person_id": userID} 12 | err := deliverSrv.db.SelectWithMultipleFilter(ctx, &deliveriesList, whereFilter) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return &deliveriesList, nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/service/delivery/order_placement.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/database/models/delivery" 6 | "Go_Food_Delivery/pkg/database/models/order" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | func (deliverSrv *DeliveryService) updateOrderStatus(ctx context.Context, orderID int64, status string) error { 14 | _, err := deliverSrv.db.Update(ctx, "orders", database.Filter{"order_status": status}, 15 | database.Filter{"order_id": orderID}) 16 | if err != nil { 17 | return err 18 | } 19 | _, err = deliverSrv.db.Update(ctx, "deliveries", database.Filter{"delivery_status": status}, 20 | database.Filter{"order_id": orderID}) 21 | return nil 22 | } 23 | 24 | func (deliverSrv *DeliveryService) OrderPlacement(ctx context.Context, 25 | deliveryPersonID int64, orderID int64, deliveryStatus string) (bool, error) { 26 | var orderInfo order.Order 27 | setFilter := database.Filter{"order_status": deliveryStatus} 28 | whereFilter := database.Filter{"order_id": orderID} 29 | 30 | // Check the order is valid or not. 31 | err := deliverSrv.db.Select(ctx, &orderInfo, "order_id", orderID) 32 | if err != nil { 33 | return false, err 34 | } 35 | 36 | // Perform generic validation. 37 | _, err = deliverSrv.orderValidation(ctx, &orderInfo, deliveryPersonID) 38 | if err != nil { 39 | return false, err 40 | } 41 | 42 | invalidStatuses := map[string]bool{ 43 | "cancelled": true, 44 | "completed": true, 45 | "failed": true, 46 | "delivered": true, 47 | } 48 | 49 | if invalidStatuses[orderInfo.OrderStatus] { 50 | return false, errors.New("this order is invalid or it has been already delivered,failed or cancelled") 51 | } 52 | 53 | switch orderInfo.OrderStatus { 54 | case "in_progress", "on_the_way": 55 | // Update Order 56 | _, err := deliverSrv.db.Update(ctx, "orders", setFilter, whereFilter) 57 | if err != nil { 58 | return false, err 59 | } 60 | 61 | // Add info. Into the delivery table. 62 | deliver := new(delivery.Deliveries) 63 | deliver.DeliveryPersonID = deliveryPersonID 64 | deliver.OrderID = orderID 65 | deliver.DeliveryStatus = deliveryStatus 66 | 67 | if deliveryStatus == "delivered" { 68 | deliver.DeliveryTime = time.Now() 69 | 70 | } 71 | 72 | _, err = deliverSrv.db.Insert(ctx, deliver) 73 | if err != nil { 74 | return false, err 75 | } 76 | // Notify User. 77 | err = deliverSrv.notifyDeliveryStatusToUser(&orderInfo, deliveryStatus) 78 | if err != nil { 79 | return false, err 80 | } 81 | return true, nil 82 | default: 83 | return false, errors.New("unknown order status") 84 | } 85 | 86 | } 87 | 88 | func (deliverSrv *DeliveryService) notifyDeliveryStatusToUser(order *order.Order, status string) error { 89 | var message string 90 | 91 | switch status { 92 | case "delivered": 93 | message = fmt.Sprintf("USER_ID:%d|MESSAGE:Your order no.%d has been successfully %s", order.UserID, order.OrderID, status) 94 | case "failed": 95 | message = fmt.Sprintf("USER_ID:%d|MESSAGE:Your order no.%d has been %s", order.UserID, order.OrderID, status) 96 | case "cancelled": 97 | message = fmt.Sprintf("USER_ID:%d|MESSAGE:Your order no.%d has been %s", order.UserID, order.OrderID, status) 98 | case "on_the_way": 99 | message = fmt.Sprintf("USER_ID:%d|MESSAGE:Your order no.%d is %s", order.UserID, order.OrderID, status) 100 | default: 101 | return fmt.Errorf("invalid status: %s", status) 102 | } 103 | 104 | topic := fmt.Sprintf("orders.status.%d", order.OrderID) 105 | err := deliverSrv.nats.Pub(topic, []byte(message)) 106 | if err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/service/delivery/service.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/nats" 6 | ) 7 | 8 | type DeliveryService struct { 9 | db database.Database 10 | env string 11 | nats *nats.NATS 12 | } 13 | 14 | func NewDeliveryService(db database.Database, env string, nats *nats.NATS) *DeliveryService { 15 | return &DeliveryService{db, env, nats} 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/delivery/validation.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/order" 5 | "context" 6 | "errors" 7 | "time" 8 | ) 9 | 10 | func (deliverSrv *DeliveryService) orderValidation(ctx context.Context, order *order.Order, deliveryPersonID int64) (bool, error) { 11 | orderPlacedTime := order.CreatedAt 12 | currentTime := time.Now() 13 | 14 | var deliveryExists int64 15 | var deliveryCancelCount int64 16 | var totalDeliveryCancellationCount int64 17 | err := deliverSrv.db.Raw(ctx, &deliveryExists, 18 | `SELECT COUNT(*) FROM deliveries WHERE order_id=? AND delivery_person_id=? AND delivery_status='on_the_way';`, 19 | order.OrderID, deliveryPersonID) 20 | if err != nil { 21 | return false, err 22 | } 23 | 24 | err = deliverSrv.db.Raw(ctx, &deliveryCancelCount, 25 | `SELECT COUNT(*) FROM deliveries WHERE order_id=? AND delivery_person_id=? AND delivery_status='cancelled';`, 26 | order.OrderID, deliveryPersonID) 27 | if err != nil { 28 | return false, err 29 | } 30 | 31 | err = deliverSrv.db.Raw(ctx, &totalDeliveryCancellationCount, 32 | `SELECT count(*) FROM deliveries WHERE delivery_person_id = ? 33 | AND delivery_status = 'cancelled' 34 | AND created_at >= now() - interval '1 hour';`, deliveryPersonID) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | // If the order remains unclaimed by any delivery partner for more than 5 minutes, it will be canceled. 40 | if order.OrderStatus == "in_progress" { 41 | if currentTime.Sub(orderPlacedTime) > 5*time.Minute { 42 | _ = deliverSrv.updateOrderStatus(ctx, order.OrderID, "cancelled") 43 | return false, errors.New("this order was not accepted by the restaurant " + 44 | "for more than 5 minutes. It has been cancelled") 45 | } 46 | } 47 | 48 | //If a delivery partner accepts an order, it should no longer be visible to other delivery partners. 49 | if order.OrderStatus == "on_the_way" && deliveryExists == 0 { 50 | return false, errors.New("this order was already accepted by another delivery partner") 51 | } 52 | // If a delivery partner cancels an order, it should no longer be visible to them but will remain visible to other delivery partners. 53 | if deliveryCancelCount == 1 { 54 | return false, errors.New("you won't be able to accept this order again. It has been cancelled") 55 | } 56 | 57 | // If a delivery partner cancels three or more orders within an hour of accepting them, they will be blocked from receiving orders for three hours. 58 | 59 | return true, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/service/notification/service.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "Go_Food_Delivery/pkg/nats" 6 | "github.com/gorilla/websocket" 7 | "log/slog" 8 | ) 9 | 10 | type NotificationService struct { 11 | db database.Database 12 | env string 13 | nats *nats.NATS 14 | } 15 | 16 | func NewNotificationService(db database.Database, env string, nats *nats.NATS) *NotificationService { 17 | return &NotificationService{db, env, nats} 18 | } 19 | 20 | func (s *NotificationService) SubscribeNewOrders(clients map[string]*websocket.Conn) error { 21 | slog.Info("Listening to ==> NotificationService::SubscribeNewOrders") 22 | 23 | err := s.nats.Sub("orders.new.*", clients) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | func (s *NotificationService) SubscribeOrderStatus(clients map[string]*websocket.Conn) error { 31 | slog.Info("Listening to ==> NotificationService::SubscribeOrderStatus") 32 | err := s.nats.Sub("orders.status.*", clients) 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/service/restaurant/add_menu.go: -------------------------------------------------------------------------------- 1 | //go:build !test 2 | 3 | package restaurant 4 | 5 | import ( 6 | "Go_Food_Delivery/pkg/database" 7 | "Go_Food_Delivery/pkg/database/models/restaurant" 8 | "Go_Food_Delivery/pkg/service/restaurant/unsplash" 9 | "context" 10 | "fmt" 11 | "log/slog" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "sync" 16 | ) 17 | 18 | var ImageUpdateLock *sync.Mutex = &sync.Mutex{} 19 | 20 | func (restSrv *RestaurantService) AddMenu(ctx context.Context, menu *restaurant.MenuItem) (*restaurant.MenuItem, error) { 21 | _, err := restSrv.db.Insert(ctx, menu) 22 | if err != nil { 23 | return &restaurant.MenuItem{}, err 24 | } 25 | return menu, nil 26 | } 27 | 28 | func (restSrv *RestaurantService) UpdateMenuPhoto(ctx context.Context, menu *restaurant.MenuItem) { 29 | if restSrv.env == "TEST" { 30 | return 31 | } 32 | client := &http.Client{} 33 | downloadClient := &unsplash.DefaultHTTPImageClient{} 34 | fs := &unsplash.DefaultFileSystem{} 35 | menuImageURL := unsplash.GetUnSplashImageURL(client, menu.Name) 36 | imageFileName := fmt.Sprintf("menu_item_%d.jpg", menu.MenuID) 37 | imageFileLocalPath := fmt.Sprintf("uploads/%s", imageFileName) 38 | imageFilePath := filepath.Join(os.Getenv("LOCAL_STORAGE_PATH"), imageFileName) 39 | err := unsplash.DownloadImageToDisk(downloadClient, fs, menuImageURL, imageFilePath) 40 | if err != nil { 41 | slog.Info("UnSplash Failed to Download Image", "error", err) 42 | } 43 | 44 | go func() { 45 | ImageUpdateLock.Lock() 46 | defer ImageUpdateLock.Unlock() 47 | setFilter := database.Filter{"photo": imageFileLocalPath} 48 | whereFilter := database.Filter{"menu_id": menu.MenuID} 49 | select { 50 | case <-ctx.Done(): 51 | slog.Error("UnSplash Worker::", "error", ctx.Err().Error()) 52 | return 53 | default: 54 | _, err := restSrv.db.Update(context.Background(), "menu_item", setFilter, whereFilter) 55 | if err != nil { 56 | slog.Info("UnSplash DB Image Update", "error", err) 57 | } 58 | } 59 | }() 60 | } 61 | -------------------------------------------------------------------------------- /pkg/service/restaurant/add_restaurant.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) Add(ctx context.Context, restaurant *restaurant.Restaurant) (bool, error) { 9 | _, err := restSrv.db.Insert(ctx, restaurant) 10 | if err != nil { 11 | return false, err 12 | } 13 | return true, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/restaurant/delete_menu.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) DeleteMenu(ctx context.Context, menuId int64, restaurantId int64) (bool, error) { 9 | filter := database.Filter{"menu_id": menuId, "restaurant_id": restaurantId} 10 | 11 | _, err := restSrv.db.Delete(ctx, "menu_item", filter) 12 | if err != nil { 13 | return false, err 14 | } 15 | return true, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/restaurant/delete_restaurant.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) DeleteRestaurant(ctx context.Context, restaurantId int64) (bool, error) { 9 | filter := database.Filter{"restaurant_id": restaurantId} 10 | 11 | _, err := restSrv.db.Delete(ctx, "restaurant", filter) 12 | if err != nil { 13 | return false, err 14 | } 15 | return true, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/restaurant/list_menus.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | restaurantModel "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) ListMenus(ctx context.Context, restaurantId int64) ([]restaurantModel.MenuItem, error) { 9 | var menuItems []restaurantModel.MenuItem 10 | 11 | err := restSrv.db.Select(ctx, &menuItems, "restaurant_id", restaurantId) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return menuItems, nil 16 | } 17 | 18 | func (restSrv *RestaurantService) ListAllMenus(ctx context.Context) ([]restaurantModel.MenuItem, error) { 19 | var menuItems []restaurantModel.MenuItem 20 | 21 | err := restSrv.db.SelectAll(ctx, "menu_item", &menuItems) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return menuItems, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/service/restaurant/list_restaurant_by_id.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) ListRestaurantById(ctx context.Context, restaurantId int64) (restaurant.Restaurant, error) { 9 | var restro restaurant.Restaurant 10 | 11 | err := restSrv.db.Select(ctx, &restro, "restaurant_id", restaurantId) 12 | if err != nil { 13 | return restaurant.Restaurant{}, err 14 | } 15 | return restro, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/restaurant/list_restaurants.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | restaurantModel "Go_Food_Delivery/pkg/database/models/restaurant" 5 | "context" 6 | ) 7 | 8 | func (restSrv *RestaurantService) ListRestaurants(ctx context.Context) ([]restaurantModel.Restaurant, error) { 9 | var restaurants []restaurantModel.Restaurant 10 | 11 | err := restSrv.db.SelectAll(ctx, "restaurant", &restaurants) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return restaurants, nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/service/restaurant/service.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | ) 6 | 7 | type RestaurantService struct { 8 | db database.Database 9 | env string 10 | } 11 | 12 | func NewRestaurantService(db database.Database, env string) *RestaurantService { 13 | return &RestaurantService{db, env} 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/restaurant/unsplash/download.go: -------------------------------------------------------------------------------- 1 | package unsplash 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | type ImageClient interface { 11 | Get(url string) (*http.Response, error) 12 | } 13 | 14 | type FileSystem interface { 15 | Create(name string) (*os.File, error) 16 | } 17 | 18 | type DefaultHTTPImageClient struct{} 19 | 20 | func (c *DefaultHTTPImageClient) Get(url string) (*http.Response, error) { 21 | return http.Get(url) 22 | } 23 | 24 | type DefaultFileSystem struct{} 25 | 26 | func (fs *DefaultFileSystem) Create(name string) (*os.File, error) { 27 | return os.Create(name) 28 | } 29 | 30 | func DownloadImageToDisk(client ImageClient, fs FileSystem, url string, filepath string) error { 31 | response, err := client.Get(url) 32 | if err != nil { 33 | return fmt.Errorf("failed to download image: %v", err) 34 | } 35 | defer response.Body.Close() 36 | 37 | if response.StatusCode != http.StatusOK { 38 | return fmt.Errorf("failed to download image: status code %d", response.StatusCode) 39 | } 40 | 41 | file, err := fs.Create(filepath) 42 | if err != nil { 43 | return fmt.Errorf("failed to create file: %v", err) 44 | } 45 | defer file.Close() 46 | 47 | _, err = io.Copy(file, response.Body) 48 | if err != nil { 49 | return fmt.Errorf("failed to save image: %v", err) 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/service/restaurant/unsplash/image.go: -------------------------------------------------------------------------------- 1 | package unsplash 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | ) 12 | 13 | type HttpClient interface { 14 | Do(req *http.Request) (*http.Response, error) 15 | } 16 | 17 | func GetUnSplashImageURL(client HttpClient, menuItem string) string { 18 | 19 | imageUrl := "https://api.unsplash.com/search/photos/?page=1&query=" + url.QueryEscape(menuItem) + "&w=400&h=400" 20 | req, err := http.NewRequest("GET", imageUrl, nil) 21 | if err != nil { 22 | log.Fatalf("Failed to create request: %v", err) 23 | } 24 | 25 | req.Header.Set("Authorization", fmt.Sprintf("%s %s", "Client-ID", os.Getenv("UNSPLASH_API_KEY"))) 26 | 27 | resp, err := client.Do(req) 28 | if err != nil { 29 | log.Fatalf("Failed to send request: %v", err) 30 | } 31 | defer resp.Body.Close() 32 | 33 | body, err := io.ReadAll(resp.Body) 34 | if err != nil { 35 | log.Fatalf("Failed to read response body: %v", err) 36 | } 37 | 38 | // Decode the JSON response into the struct 39 | var apiResponse UnSplash 40 | if err := json.Unmarshal(body, &apiResponse); err != nil { 41 | log.Fatalf("UnSplash::Failed to decode JSON response: %v", err) 42 | } 43 | 44 | return apiResponse.Results[0].Urls.Small 45 | } 46 | -------------------------------------------------------------------------------- /pkg/service/restaurant/unsplash/unsplash.go: -------------------------------------------------------------------------------- 1 | package unsplash 2 | 3 | import "time" 4 | 5 | type UnSplash struct { 6 | Total int `json:"total"` 7 | TotalPages int `json:"total_pages"` 8 | Results []struct { 9 | Id string `json:"id"` 10 | Slug string `json:"slug"` 11 | AlternativeSlugs struct { 12 | En string `json:"en"` 13 | Es string `json:"es"` 14 | Ja string `json:"ja"` 15 | Fr string `json:"fr"` 16 | It string `json:"it"` 17 | Ko string `json:"ko"` 18 | De string `json:"de"` 19 | Pt string `json:"pt"` 20 | } `json:"alternative_slugs"` 21 | CreatedAt time.Time `json:"created_at"` 22 | UpdatedAt time.Time `json:"updated_at"` 23 | PromotedAt time.Time `json:"promoted_at"` 24 | Width int `json:"width"` 25 | Height int `json:"height"` 26 | Color string `json:"color"` 27 | BlurHash string `json:"blur_hash"` 28 | Description string `json:"description"` 29 | AltDescription string `json:"alt_description"` 30 | Breadcrumbs []interface{} `json:"breadcrumbs"` 31 | Urls struct { 32 | Raw string `json:"raw"` 33 | Full string `json:"full"` 34 | Regular string `json:"regular"` 35 | Small string `json:"small"` 36 | Thumb string `json:"thumb"` 37 | SmallS3 string `json:"small_s3"` 38 | } `json:"urls"` 39 | Links struct { 40 | Self string `json:"self"` 41 | Html string `json:"html"` 42 | Download string `json:"download"` 43 | DownloadLocation string `json:"download_location"` 44 | } `json:"links"` 45 | Likes int `json:"likes"` 46 | LikedByUser bool `json:"liked_by_user"` 47 | CurrentUserCollections []interface{} `json:"current_user_collections"` 48 | Sponsorship interface{} `json:"sponsorship"` 49 | TopicSubmissions struct { 50 | FoodDrink struct { 51 | Status string `json:"status"` 52 | ApprovedOn time.Time `json:"approved_on"` 53 | } `json:"food-drink"` 54 | } `json:"topic_submissions"` 55 | AssetType string `json:"asset_type"` 56 | User struct { 57 | Id string `json:"id"` 58 | UpdatedAt time.Time `json:"updated_at"` 59 | Username string `json:"username"` 60 | Name string `json:"name"` 61 | FirstName string `json:"first_name"` 62 | LastName string `json:"last_name"` 63 | TwitterUsername string `json:"twitter_username"` 64 | PortfolioUrl string `json:"portfolio_url"` 65 | Bio string `json:"bio"` 66 | Location string `json:"location"` 67 | Links struct { 68 | Self string `json:"self"` 69 | Html string `json:"html"` 70 | Photos string `json:"photos"` 71 | Likes string `json:"likes"` 72 | Portfolio string `json:"portfolio"` 73 | Following string `json:"following"` 74 | Followers string `json:"followers"` 75 | } `json:"links"` 76 | ProfileImage struct { 77 | Small string `json:"small"` 78 | Medium string `json:"medium"` 79 | Large string `json:"large"` 80 | } `json:"profile_image"` 81 | InstagramUsername string `json:"instagram_username"` 82 | TotalCollections int `json:"total_collections"` 83 | TotalLikes int `json:"total_likes"` 84 | TotalPhotos int `json:"total_photos"` 85 | TotalPromotedPhotos int `json:"total_promoted_photos"` 86 | TotalIllustrations int `json:"total_illustrations"` 87 | TotalPromotedIllustrations int `json:"total_promoted_illustrations"` 88 | AcceptedTos bool `json:"accepted_tos"` 89 | ForHire bool `json:"for_hire"` 90 | Social struct { 91 | InstagramUsername string `json:"instagram_username"` 92 | PortfolioUrl string `json:"portfolio_url"` 93 | TwitterUsername string `json:"twitter_username"` 94 | PaypalEmail interface{} `json:"paypal_email"` 95 | } `json:"social"` 96 | } `json:"user"` 97 | Tags []struct { 98 | Type string `json:"type"` 99 | Title string `json:"title"` 100 | Source struct { 101 | Ancestry struct { 102 | Type struct { 103 | Slug string `json:"slug"` 104 | PrettySlug string `json:"pretty_slug"` 105 | } `json:"type"` 106 | Category struct { 107 | Slug string `json:"slug"` 108 | PrettySlug string `json:"pretty_slug"` 109 | } `json:"category"` 110 | } `json:"ancestry"` 111 | Title string `json:"title"` 112 | Subtitle string `json:"subtitle"` 113 | Description string `json:"description"` 114 | MetaTitle string `json:"meta_title"` 115 | MetaDescription string `json:"meta_description"` 116 | CoverPhoto struct { 117 | Id string `json:"id"` 118 | Slug string `json:"slug"` 119 | AlternativeSlugs struct { 120 | En string `json:"en"` 121 | Es string `json:"es"` 122 | Ja string `json:"ja"` 123 | Fr string `json:"fr"` 124 | It string `json:"it"` 125 | Ko string `json:"ko"` 126 | De string `json:"de"` 127 | Pt string `json:"pt"` 128 | } `json:"alternative_slugs"` 129 | CreatedAt time.Time `json:"created_at"` 130 | UpdatedAt time.Time `json:"updated_at"` 131 | PromotedAt time.Time `json:"promoted_at"` 132 | Width int `json:"width"` 133 | Height int `json:"height"` 134 | Color string `json:"color"` 135 | BlurHash string `json:"blur_hash"` 136 | Description string `json:"description"` 137 | AltDescription string `json:"alt_description"` 138 | Breadcrumbs []interface{} `json:"breadcrumbs"` 139 | Urls struct { 140 | Raw string `json:"raw"` 141 | Full string `json:"full"` 142 | Regular string `json:"regular"` 143 | Small string `json:"small"` 144 | Thumb string `json:"thumb"` 145 | SmallS3 string `json:"small_s3"` 146 | } `json:"urls"` 147 | Links struct { 148 | Self string `json:"self"` 149 | Html string `json:"html"` 150 | Download string `json:"download"` 151 | DownloadLocation string `json:"download_location"` 152 | } `json:"links"` 153 | Likes int `json:"likes"` 154 | LikedByUser bool `json:"liked_by_user"` 155 | CurrentUserCollections []interface{} `json:"current_user_collections"` 156 | Sponsorship interface{} `json:"sponsorship"` 157 | TopicSubmissions struct { 158 | Health struct { 159 | Status string `json:"status"` 160 | ApprovedOn time.Time `json:"approved_on"` 161 | } `json:"health"` 162 | } `json:"topic_submissions"` 163 | AssetType string `json:"asset_type"` 164 | User struct { 165 | Id string `json:"id"` 166 | UpdatedAt time.Time `json:"updated_at"` 167 | Username string `json:"username"` 168 | Name string `json:"name"` 169 | FirstName string `json:"first_name"` 170 | LastName string `json:"last_name"` 171 | TwitterUsername interface{} `json:"twitter_username"` 172 | PortfolioUrl string `json:"portfolio_url"` 173 | Bio string `json:"bio"` 174 | Location string `json:"location"` 175 | Links struct { 176 | Self string `json:"self"` 177 | Html string `json:"html"` 178 | Photos string `json:"photos"` 179 | Likes string `json:"likes"` 180 | Portfolio string `json:"portfolio"` 181 | Following string `json:"following"` 182 | Followers string `json:"followers"` 183 | } `json:"links"` 184 | ProfileImage struct { 185 | Small string `json:"small"` 186 | Medium string `json:"medium"` 187 | Large string `json:"large"` 188 | } `json:"profile_image"` 189 | InstagramUsername string `json:"instagram_username"` 190 | TotalCollections int `json:"total_collections"` 191 | TotalLikes int `json:"total_likes"` 192 | TotalPhotos int `json:"total_photos"` 193 | TotalPromotedPhotos int `json:"total_promoted_photos"` 194 | TotalIllustrations int `json:"total_illustrations"` 195 | TotalPromotedIllustrations int `json:"total_promoted_illustrations"` 196 | AcceptedTos bool `json:"accepted_tos"` 197 | ForHire bool `json:"for_hire"` 198 | Social struct { 199 | InstagramUsername string `json:"instagram_username"` 200 | PortfolioUrl string `json:"portfolio_url"` 201 | TwitterUsername interface{} `json:"twitter_username"` 202 | PaypalEmail interface{} `json:"paypal_email"` 203 | } `json:"social"` 204 | } `json:"user"` 205 | } `json:"cover_photo"` 206 | } `json:"source,omitempty"` 207 | } `json:"tags"` 208 | } `json:"results"` 209 | } 210 | -------------------------------------------------------------------------------- /pkg/service/review/add_review.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/review" 5 | "context" 6 | ) 7 | 8 | func (revSrv *ReviewService) Add(ctx context.Context, review *review.Review) (bool, error) { 9 | _, err := revSrv.db.Insert(ctx, review) 10 | if err != nil { 11 | return false, err 12 | } 13 | return true, nil 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/review/delete_review.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | ) 7 | 8 | func (revSrv *ReviewService) DeleteReview(ctx context.Context, reviewId int64, userId int64) (bool, error) { 9 | filter := database.Filter{"review_id": reviewId, "user_id": userId} 10 | 11 | _, err := revSrv.db.Delete(ctx, "reviews", filter) 12 | if err != nil { 13 | return false, err 14 | } 15 | return true, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/review/list_reviews.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/review" 5 | "context" 6 | ) 7 | 8 | func (revSrv *ReviewService) ListReviews(ctx context.Context, restaurantId int64) ([]review.Review, error) { 9 | var reviewList []review.Review 10 | 11 | err := revSrv.db.Select(ctx, &reviewList, "restaurant_id", restaurantId) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return reviewList, nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/service/review/service.go: -------------------------------------------------------------------------------- 1 | package review 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | ) 6 | 7 | type ReviewService struct { 8 | db database.Database 9 | env string 10 | } 11 | 12 | func NewReviewService(db database.Database, env string) *ReviewService { 13 | return &ReviewService{db, env} 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/user/add_user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/user" 5 | "context" 6 | "errors" 7 | ) 8 | 9 | func (usrSrv *UsrService) Add(ctx context.Context, user *user.User) (bool, error) { 10 | accountExists, _, _, _ := usrSrv.UserExist(ctx, user.Email, false) 11 | if accountExists { 12 | return false, errors.New("the user you are trying to register already exists") 13 | } else { 14 | user.HashPassword() 15 | _, err := usrSrv.db.Insert(ctx, user) 16 | if err != nil { 17 | return false, err 18 | } 19 | return true, nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/service/user/delete_user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | ) 7 | 8 | func (usrSrv *UsrService) Delete(ctx context.Context, userId int64) (bool, error) { 9 | filter := database.Filter{"id": userId} 10 | _, err := usrSrv.db.Delete(ctx, "users", filter) 11 | if err != nil { 12 | return false, err 13 | } 14 | return true, nil 15 | } 16 | -------------------------------------------------------------------------------- /pkg/service/user/login_user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | "Go_Food_Delivery/pkg/database/models/user" 6 | "context" 7 | "errors" 8 | "github.com/golang-jwt/jwt/v5" 9 | "log/slog" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func (usrSrv *UsrService) Login(_ context.Context, userID int64, Name string) (string, error) { 15 | 16 | claims := middleware.UserClaims{UserID: userID, Name: Name, 17 | RegisteredClaims: jwt.RegisteredClaims{ 18 | 19 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(1))), 20 | Issuer: "Go_Food_Delivery", 21 | }} 22 | 23 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 24 | return token.SignedString([]byte(os.Getenv("JWT_SECRET_KEY"))) 25 | } 26 | 27 | func (usrSrv *UsrService) UserExist(ctx context.Context, email string, recordRequired bool) (bool, int64, string, error) { 28 | count, err := usrSrv.db.Count(ctx, "users", "COUNT(*)", "email", email) 29 | if err != nil { 30 | slog.Info("UserService.UserExist::Error %v", "error", err) 31 | return false, 0, "", err 32 | } 33 | if count == 0 { 34 | return false, 0, "", nil 35 | } 36 | 37 | if recordRequired == true { 38 | // Fetch User Detail 39 | var accountInfo user.User 40 | err = usrSrv.db.Select(ctx, &accountInfo, "email", email) 41 | if err != nil { 42 | return false, 0, "", err 43 | } 44 | return true, accountInfo.ID, accountInfo.Name, nil 45 | } 46 | 47 | return true, 0, "", nil 48 | } 49 | 50 | func (usrSrv *UsrService) ValidatePassword(ctx context.Context, userInput *user.LoginUser) (bool, error) { 51 | var userAccount user.User 52 | err := usrSrv.db.Select(ctx, &userAccount, "email", userInput.Email) 53 | if err != nil { 54 | slog.Info("UserService.ValidatePassword::Error %v", "error", err) 55 | return false, err 56 | } 57 | 58 | err = userInput.CheckPassword(userAccount.Password) 59 | if err != nil { 60 | return false, errors.New("invalid password") 61 | } 62 | return true, nil 63 | 64 | } 65 | -------------------------------------------------------------------------------- /pkg/service/user/service.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | ) 6 | 7 | type UsrService struct { 8 | db database.Database 9 | env string 10 | } 11 | 12 | func NewUserService(db database.Database, env string) *UsrService { 13 | return &UsrService{db, env} 14 | } 15 | -------------------------------------------------------------------------------- /pkg/service/user/utils.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database/models/user" 5 | "context" 6 | "errors" 7 | ) 8 | 9 | func ValidateAccount(login func(ctx context.Context, userID int64, userName string) (string, error), 10 | accountExists func(ctx context.Context, email string, recordRequired bool) (bool, int64, string, error), 11 | validatePassword func(ctx context.Context, user *user.LoginUser) (bool, error)) func(ctx context.Context, user *user.LoginUser) (string, error) { 12 | return func(ctx context.Context, user *user.LoginUser) (string, error) { 13 | exists, userId, name, err := accountExists(ctx, user.Email, true) 14 | if err != nil { 15 | return "", err 16 | } 17 | if !exists { 18 | return "", errors.New("we did not find any account with this user") 19 | } 20 | 21 | _, err = validatePassword(ctx, user) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return login(ctx, userId, name) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type ImageStorage interface { 9 | Upload(fileName string, file io.Reader) (string, error) 10 | } 11 | 12 | func CreateImageStorage(storageType string) ImageStorage { 13 | switch storageType { 14 | case "local": 15 | localFileStore := &LocalFileStorage{BasePath: os.Getenv("LOCAL_STORAGE_PATH")} 16 | createUploadDirectory(localFileStore.BasePath) 17 | return localFileStore 18 | default: 19 | panic("Unsupported storage type: " + storageType) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/storage/local.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type LocalFileStorage struct { 10 | BasePath string 11 | } 12 | 13 | func (lf *LocalFileStorage) Upload(fileName string, file io.Reader) (string, error) { 14 | fullPath := filepath.Join(lf.BasePath, fileName) 15 | outFile, err := os.Create(fullPath) 16 | if err != nil { 17 | return "", err 18 | } 19 | defer outFile.Close() 20 | 21 | _, err = io.Copy(outFile, file) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return fullPath, nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/storage/utils.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | func createUploadDirectory(basePath string) { 9 | if err := os.MkdirAll(basePath, os.ModePerm); err != nil { 10 | panic(err) 11 | } 12 | 13 | err := os.Chmod(basePath, 0755) 14 | if err != nil { 15 | log.Fatalf("Permission Error:: %v", err) 16 | return 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/tests/cart/cart_test.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | restroTypes "Go_Food_Delivery/pkg/database/models/restaurant" 6 | userModel "Go_Food_Delivery/pkg/database/models/user" 7 | "Go_Food_Delivery/pkg/handler" 8 | crt "Go_Food_Delivery/pkg/handler/cart" 9 | "Go_Food_Delivery/pkg/handler/restaurant" 10 | "Go_Food_Delivery/pkg/handler/user" 11 | natsPkg "Go_Food_Delivery/pkg/nats" 12 | "Go_Food_Delivery/pkg/service/cart_order" 13 | restro "Go_Food_Delivery/pkg/service/restaurant" 14 | usr "Go_Food_Delivery/pkg/service/user" 15 | "Go_Food_Delivery/pkg/tests" 16 | common "Go_Food_Delivery/pkg/tests/restaurant" 17 | "bytes" 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "github.com/testcontainers/testcontainers-go" 22 | "github.com/testcontainers/testcontainers-go/modules/nats" 23 | "strings" 24 | "time" 25 | 26 | "github.com/gin-gonic/gin" 27 | "github.com/go-faker/faker/v4" 28 | "github.com/go-playground/validator/v10" 29 | "github.com/stretchr/testify/assert" 30 | "net/http" 31 | "net/http/httptest" 32 | "os" 33 | "testing" 34 | ) 35 | 36 | func TestCart(t *testing.T) { 37 | t.Setenv("APP_ENV", "TEST") 38 | t.Setenv("STORAGE_TYPE", "local") 39 | t.Setenv("STORAGE_DIRECTORY", "uploads") 40 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 41 | t.Setenv("GIN_MODE", "release") 42 | 43 | testDB := tests.Setup() 44 | AppEnv := os.Getenv("APP_ENV") 45 | testServer := handler.NewServer(testDB, false) 46 | validate := validator.New() 47 | 48 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(60*time.Second)) 49 | defer cancel() 50 | 51 | natsContainer, err := nats.Run(ctx, "nats:2.10.20", testcontainers.WithHostPortAccess(4222)) 52 | 53 | if err != nil { 54 | t.Logf("failed to start NATS container: %s", err) 55 | return 56 | } 57 | connectionString, err := natsContainer.ConnectionString(ctx) 58 | if err != nil { 59 | t.Log("NATS Connection String Error::", err) 60 | return 61 | } 62 | 63 | // Connect NATS 64 | natTestServer, err := natsPkg.NewNATS(connectionString) 65 | middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} 66 | 67 | // User 68 | userService := usr.NewUserService(testDB, AppEnv) 69 | user.NewUserHandler(testServer, "/user", userService, validate) 70 | 71 | // Restaurant 72 | restaurantService := restro.NewRestaurantService(testDB, AppEnv) 73 | restaurant.NewRestaurantHandler(testServer, "/restaurant", restaurantService) 74 | 75 | // Cart 76 | cartService := cart_order.NewCartService(testDB, AppEnv, natTestServer) 77 | crt.NewCartHandler(testServer, "/cart", cartService, middlewares, validate) 78 | 79 | var RestaurantResponseID int64 80 | var RestaurantMenuID int64 81 | name := faker.Name() 82 | file := []byte{10, 10, 10, 10, 10} // fake image bytes 83 | description := faker.Paragraph() 84 | address := faker.Word() 85 | city := faker.Word() 86 | state := faker.Word() 87 | 88 | type FakeRestaurantMenu struct { 89 | RestaurantID int64 `json:"restaurant_id"` 90 | Name string `json:"name"` 91 | Description string `json:"description"` 92 | Price float64 `json:"price"` 93 | Category string `json:"category"` 94 | Available bool `json:"available"` 95 | } 96 | 97 | type CartParams struct { 98 | ItemID int64 `json:"item_id"` 99 | RestaurantID int64 `json:"restaurant_id"` 100 | Quantity int64 `json:"quantity"` 101 | } 102 | 103 | form := common.FakeRestaurant{ 104 | Name: name, 105 | File: file, 106 | Description: description, 107 | Address: address, 108 | City: city, 109 | State: state, 110 | } 111 | 112 | body, contentType, err := common.GenerateData(form) 113 | if err != nil { 114 | t.Fatalf("Error generating form-data: %v", err) 115 | } 116 | 117 | type FakeUser struct { 118 | User string `json:"user" faker:"name"` 119 | Email string `json:"email" faker:"email"` 120 | Password string `json:"password" faker:"password"` 121 | } 122 | 123 | var customUser FakeUser 124 | var userInfo userModel.User 125 | _ = faker.FakeData(&customUser) 126 | userInfo.Email = customUser.Email 127 | userInfo.Password = customUser.Password 128 | 129 | _, err = userService.Add(ctx, &userInfo) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | 134 | loginToken, err := userService.Login(ctx, userInfo.ID, "Food Delivery") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | Token := fmt.Sprintf("Bearer %s", loginToken) 140 | 141 | t.Run("Cart::Restaurant::Create", func(t *testing.T) { 142 | 143 | req, _ := http.NewRequest(http.MethodPost, "/restaurant/", body) 144 | req.Header.Set("Content-Type", contentType) 145 | w := httptest.NewRecorder() 146 | testServer.Gin.ServeHTTP(w, req) 147 | assert.Equal(t, http.StatusCreated, w.Code) 148 | 149 | }) 150 | 151 | t.Run("Cart::Restaurant::Listing", func(t *testing.T) { 152 | 153 | type RestaurantResponse struct { 154 | RestaurantID int64 `json:"restaurant_id"` 155 | Name string `json:"name"` 156 | StoreImage string `json:"store_image"` 157 | Description string `json:"description"` 158 | Address string `json:"address"` 159 | City string `json:"city"` 160 | State string `json:"state"` 161 | CreatedAt string `json:"CreatedAt"` 162 | UpdatedAt string `json:"UpdatedAt"` 163 | } 164 | 165 | req, _ := http.NewRequest(http.MethodGet, "/restaurant/", nil) 166 | req.Header.Set("Content-Type", "application/json") 167 | w := httptest.NewRecorder() 168 | 169 | testServer.Gin.ServeHTTP(w, req) 170 | assert.Equal(t, http.StatusOK, w.Code) 171 | 172 | var restaurants []RestaurantResponse 173 | err := json.NewDecoder(strings.NewReader(w.Body.String())).Decode(&restaurants) 174 | if err != nil { 175 | t.Fatalf("Failed to decode response body: %v", err) 176 | } 177 | 178 | // set the restaurantID 179 | RestaurantResponseID = restaurants[0].RestaurantID 180 | 181 | }) 182 | 183 | t.Run("Cart::RestaurantMenu::Create", func(t *testing.T) { 184 | var customMenu FakeRestaurantMenu 185 | 186 | customMenu.Available = true 187 | customMenu.Price = 40.35 188 | customMenu.Name = "burger" 189 | customMenu.Description = "burger" 190 | customMenu.Category = "FAST_FOODS" 191 | customMenu.RestaurantID = RestaurantResponseID 192 | payload, err := json.Marshal(&customMenu) 193 | if err != nil { 194 | t.Fatal("Error::", err) 195 | } 196 | req, _ := http.NewRequest(http.MethodPost, "/restaurant/menu", bytes.NewBuffer(payload)) 197 | req.Header.Set("Content-Type", "application/json") 198 | w := httptest.NewRecorder() 199 | testServer.Gin.ServeHTTP(w, req) 200 | assert.Equal(t, http.StatusCreated, w.Code) 201 | 202 | }) 203 | 204 | t.Run("Cart::RestaurantMenu::List", func(t *testing.T) { 205 | req, _ := http.NewRequest(http.MethodGet, "/restaurant/menu", nil) 206 | req.Header.Set("Content-Type", "application/json") 207 | w := httptest.NewRecorder() 208 | testServer.Gin.ServeHTTP(w, req) 209 | var menuItems []restroTypes.MenuItem 210 | err := json.Unmarshal(w.Body.Bytes(), &menuItems) 211 | if err != nil { 212 | fmt.Println("Error unmarshalling JSON:", err) 213 | return 214 | } 215 | 216 | RestaurantMenuID = menuItems[0].MenuID 217 | 218 | assert.Equal(t, http.StatusOK, w.Code) 219 | 220 | }) 221 | 222 | t.Run("Cart::AddItemToCart", func(t *testing.T) { 223 | var cartParams CartParams 224 | cartParams.ItemID = RestaurantMenuID 225 | cartParams.RestaurantID = RestaurantResponseID 226 | cartParams.Quantity = 1 227 | payload, err := json.Marshal(&cartParams) 228 | if err != nil { 229 | t.Fatal("Error::", err) 230 | } 231 | req, _ := http.NewRequest(http.MethodPost, "/cart/add", bytes.NewBuffer(payload)) 232 | req.Header.Set("Content-Type", "application/json") 233 | req.Header.Set("Authorization", Token) 234 | w := httptest.NewRecorder() 235 | testServer.Gin.ServeHTTP(w, req) 236 | assert.Equal(t, http.StatusCreated, w.Code) 237 | 238 | }) 239 | 240 | t.Run("Cart::List", func(t *testing.T) { 241 | req, _ := http.NewRequest(http.MethodGet, "/cart/list", nil) 242 | req.Header.Set("Content-Type", "application/json") 243 | req.Header.Set("Authorization", Token) 244 | 245 | w := httptest.NewRecorder() 246 | testServer.Gin.ServeHTTP(w, req) 247 | assert.Equal(t, http.StatusOK, w.Code) 248 | 249 | }) 250 | 251 | t.Run("Cart::PlaceOrder", func(t *testing.T) { 252 | req, _ := http.NewRequest(http.MethodPost, "/cart/order/new", nil) 253 | req.Header.Set("Content-Type", "application/json") 254 | req.Header.Set("Authorization", Token) 255 | 256 | w := httptest.NewRecorder() 257 | testServer.Gin.ServeHTTP(w, req) 258 | assert.Equal(t, http.StatusCreated, w.Code) 259 | 260 | }) 261 | 262 | t.Cleanup(func() { 263 | tests.Teardown(testDB) 264 | }) 265 | } 266 | -------------------------------------------------------------------------------- /pkg/tests/common/unsplash_download_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/service/restaurant/unsplash" 5 | "bytes" 6 | "io" 7 | "net/http" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | type MockHTTPClient struct{} 13 | 14 | func (m *MockHTTPClient) Get(url string) (*http.Response, error) { 15 | return &http.Response{ 16 | StatusCode: http.StatusOK, 17 | Body: io.NopCloser(bytes.NewBufferString("image data")), 18 | }, nil 19 | } 20 | 21 | type MockFileSystem struct { 22 | Files map[string]*bytes.Buffer 23 | } 24 | 25 | func (m *MockFileSystem) Create(name string) (*os.File, error) { 26 | m.Files[name] = &bytes.Buffer{} 27 | return os.Create(name) 28 | } 29 | 30 | func TestDownloadImageToDisk(t *testing.T) { 31 | mockClient := &MockHTTPClient{} 32 | mockFS := &MockFileSystem{Files: make(map[string]*bytes.Buffer)} 33 | 34 | err := unsplash.DownloadImageToDisk(mockClient, mockFS, "https://example.com/image.jpg", "/tmp/image.jpg") 35 | if err != nil { 36 | t.Fatalf("expected no error, got %v", err) 37 | } 38 | 39 | // Validate that the file was created 40 | if _, exists := mockFS.Files["/tmp/image.jpg"]; !exists { 41 | t.Fatalf("expected file to be created, but it wasn't") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/tests/common/unsplash_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/service/restaurant/unsplash" 5 | "bytes" 6 | "io" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | type MockClient struct { 12 | DoFunc func(req *http.Request) (*http.Response, error) 13 | } 14 | 15 | func (mc *MockClient) Do(req *http.Request) (*http.Response, error) { 16 | return mc.DoFunc(req) 17 | } 18 | 19 | func TestGetUnSplashImageURL(t *testing.T) { 20 | mockResponse := `{ 21 | "results": [{ 22 | "urls": { 23 | "small": "https://example.com/image.jpg" 24 | } 25 | }] 26 | }` 27 | mockClient := &MockClient{ 28 | DoFunc: func(req *http.Request) (*http.Response, error) { 29 | return &http.Response{ 30 | StatusCode: 200, 31 | Body: io.NopCloser(bytes.NewBufferString(mockResponse)), 32 | Header: make(http.Header), 33 | }, nil 34 | }, 35 | } 36 | 37 | imageURL := unsplash.GetUnSplashImageURL(mockClient, "test") 38 | expectedURL := "https://example.com/image.jpg" 39 | if imageURL != expectedURL { 40 | t.Fatalf("expected %s, got %s", expectedURL, imageURL) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/tests/database/database_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "context" 6 | "log" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/testcontainers/testcontainers-go" 12 | "github.com/testcontainers/testcontainers-go/modules/postgres" 13 | "github.com/testcontainers/testcontainers-go/wait" 14 | ) 15 | 16 | var ( 17 | pgContainer testcontainers.Container 18 | containerImage string = "postgres:16.3" 19 | dbHost string 20 | dbPort string 21 | dbName string = "test-db" 22 | dbUsername string = "postgres" 23 | dbPassword string = "postgres" 24 | err error 25 | ) 26 | 27 | // Initialize the PostgreSQL container 28 | func setup() { 29 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 30 | defer cancel() 31 | 32 | pgContainer, err = postgres.Run(ctx, containerImage, 33 | postgres.WithDatabase(dbName), 34 | postgres.WithUsername(dbUsername), 35 | postgres.WithPassword(dbPassword), 36 | testcontainers.WithWaitStrategy( 37 | wait.ForLog("database system is ready to accept connections"). 38 | WithOccurrence(2).WithStartupTimeout(10*time.Second)), 39 | ) 40 | if err != nil { 41 | log.Fatalf("failed to start PostgreSQL container: %s", err) 42 | } 43 | 44 | // Set environment variables 45 | dbHost, _ = pgContainer.Host(ctx) 46 | mappedPort, _ := pgContainer.MappedPort(ctx, "5432/tcp") 47 | dbPort = mappedPort.Port() 48 | _ = os.Setenv("DB_USERNAME", dbUsername) 49 | _ = os.Setenv("DB_PASSWORD", dbPassword) 50 | _ = os.Setenv("DB_NAME", dbName) 51 | _ = os.Setenv("DB_PORT", dbPort) 52 | _ = os.Setenv("DB_HOST", dbHost) 53 | _ = os.Setenv("STORAGE_TYPE", "local") 54 | _ = os.Setenv("STORAGE_DIRECTORY", "uploads") 55 | _ = os.Setenv("LOCAL_STORAGE_PATH", "./tmp") 56 | 57 | } 58 | 59 | // Teardown function to terminate the container 60 | func teardown() { 61 | if err := pgContainer.Terminate(context.Background()); err != nil { 62 | log.Fatalf("failed to terminate PostgreSQL container: %s", err) 63 | } 64 | } 65 | 66 | func TestMain(m *testing.M) { 67 | setup() 68 | 69 | result := m.Run() 70 | 71 | //teardown() 72 | os.Exit(result) 73 | } 74 | 75 | func TestDatabase(t *testing.T) { 76 | dbTest := database.New() 77 | 78 | if dbTest.HealthCheck() != true { 79 | t.Fatalf("database health check failed") 80 | } 81 | if err := dbTest.Migrate(); err != nil { 82 | t.Fatalf("Error migrating database: %s", err) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /pkg/tests/delivery/delivery_test.go: -------------------------------------------------------------------------------- 1 | package delivery 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | "Go_Food_Delivery/pkg/handler" 6 | delv "Go_Food_Delivery/pkg/handler/delivery" 7 | "Go_Food_Delivery/pkg/handler/user" 8 | natsPkg "Go_Food_Delivery/pkg/nats" 9 | "Go_Food_Delivery/pkg/service/delivery" 10 | usr "Go_Food_Delivery/pkg/service/user" 11 | "Go_Food_Delivery/pkg/tests" 12 | "context" 13 | "encoding/json" 14 | "fmt" 15 | "github.com/gin-gonic/gin" 16 | "github.com/go-playground/validator/v10" 17 | "github.com/golang-jwt/jwt/v5" 18 | "github.com/pquerna/otp/totp" 19 | "github.com/stretchr/testify/assert" 20 | "github.com/testcontainers/testcontainers-go" 21 | "github.com/testcontainers/testcontainers-go/modules/nats" 22 | "net/http" 23 | "net/http/httptest" 24 | "os" 25 | "strings" 26 | "testing" 27 | "time" 28 | ) 29 | 30 | func parseToken(tokenString string) bool { 31 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 32 | // Don't forget to validate the alg is what you expect: 33 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 34 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 35 | } 36 | 37 | // return jwt secret key 38 | return []byte("sample123"), nil 39 | }) 40 | if err != nil { 41 | return false 42 | } 43 | 44 | if _, ok := token.Claims.(jwt.MapClaims); ok { 45 | return true 46 | } else { 47 | return false 48 | 49 | } 50 | 51 | } 52 | 53 | func TestDeliveryUser(t *testing.T) { 54 | t.Setenv("APP_ENV", "TEST") 55 | t.Setenv("STORAGE_TYPE", "local") 56 | t.Setenv("STORAGE_DIRECTORY", "uploads") 57 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 58 | testDB := tests.Setup() 59 | AppEnv := os.Getenv("APP_ENV") 60 | testServer := handler.NewServer(testDB, false) 61 | middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} 62 | 63 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(60*time.Second)) 64 | defer cancel() 65 | 66 | natsContainer, err := nats.Run(ctx, "nats:2.10.20", testcontainers.WithHostPortAccess(4222)) 67 | 68 | if err != nil { 69 | t.Logf("failed to start NATS container: %s", err) 70 | return 71 | } 72 | connectionString, err := natsContainer.ConnectionString(ctx) 73 | if err != nil { 74 | t.Log("NATS Connection String Error::", err) 75 | return 76 | } 77 | 78 | // Connect NATS 79 | natTestServer, _ := natsPkg.NewNATS(connectionString) 80 | 81 | validate := validator.New() 82 | userService := usr.NewUserService(testDB, AppEnv) 83 | user.NewUserHandler(testServer, "/user", userService, validate) 84 | 85 | deliveryService := delivery.NewDeliveryService(testDB, os.Getenv("APP_ENV"), natTestServer) 86 | delv.NewDeliveryHandler(testServer, "/delivery", deliveryService, middlewares, validate) 87 | 88 | type FakeDeliveryUser struct { 89 | Name string `json:"name"` 90 | Phone string `json:"phone"` 91 | VehicleDetails string `json:"vehicle_details"` 92 | } 93 | 94 | var customUser FakeDeliveryUser 95 | customUser.Name = "Test" 96 | customUser.Phone = "08090909090" 97 | customUser.VehicleDetails = "OX-25895-8547" 98 | 99 | t.Run("Delivery::User::Create", func(t *testing.T) { 100 | 101 | payload, err := json.Marshal(&customUser) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | req, _ := http.NewRequest(http.MethodPost, "/delivery/add", strings.NewReader(string(payload))) 107 | req.Header.Set("Content-Type", "application/json") 108 | w := httptest.NewRecorder() 109 | testServer.Gin.ServeHTTP(w, req) 110 | assert.Equal(t, http.StatusCreated, w.Code) 111 | }) 112 | 113 | t.Run("Delivery::User::TOTP", func(t *testing.T) { 114 | secret, _, err := deliveryService.GenerateTOTP(ctx, customUser.Phone) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | otp, err := totp.GenerateCode(secret, time.Now()) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | assert.True(t, deliveryService.ValidateOTP(ctx, secret, otp), true) 124 | 125 | }) 126 | 127 | t.Run("Delivery::User::ValidateAccount", func(t *testing.T) { 128 | deliveryPersonDetail, err := deliveryService.ValidateAccountDetails(ctx, customUser.Phone) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | assert.Equal(t, customUser.Name, deliveryPersonDetail.Name) 133 | }) 134 | 135 | } 136 | 137 | func TestDeliveryGenerateJWT(t *testing.T) { 138 | t.Setenv("APP_ENV", "TEST") 139 | t.Setenv("STORAGE_TYPE", "local") 140 | t.Setenv("STORAGE_DIRECTORY", "uploads") 141 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 142 | t.Setenv("JWT_SECRET_KEY", "sample123") 143 | testDB := tests.Setup() 144 | AppEnv := os.Getenv("APP_ENV") 145 | testServer := handler.NewServer(testDB, false) 146 | middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} 147 | 148 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(60*time.Second)) 149 | defer cancel() 150 | 151 | natsContainer, err := nats.Run(ctx, "nats:2.10.20", testcontainers.WithHostPortAccess(4222)) 152 | 153 | if err != nil { 154 | t.Logf("failed to start NATS container: %s", err) 155 | return 156 | } 157 | connectionString, err := natsContainer.ConnectionString(ctx) 158 | if err != nil { 159 | t.Log("NATS Connection String Error::", err) 160 | return 161 | } 162 | 163 | // Connect NATS 164 | natTestServer, _ := natsPkg.NewNATS(connectionString) 165 | 166 | validate := validator.New() 167 | userService := usr.NewUserService(testDB, AppEnv) 168 | user.NewUserHandler(testServer, "/user", userService, validate) 169 | 170 | deliveryService := delivery.NewDeliveryService(testDB, os.Getenv("APP_ENV"), natTestServer) 171 | delv.NewDeliveryHandler(testServer, "/delivery", deliveryService, middlewares, validate) 172 | 173 | t.Run("Delivery::User::GenerateJWT", func(t *testing.T) { 174 | token, err := deliveryService.GenerateJWT(ctx, 1, "SAMPLE") 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | assert.True(t, parseToken(token), true) 180 | }) 181 | 182 | } 183 | -------------------------------------------------------------------------------- /pkg/tests/restaurant/common.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "bytes" 5 | "mime/multipart" 6 | "time" 7 | ) 8 | 9 | type FakeRestaurant struct { 10 | Name string 11 | File []byte 12 | Description string 13 | Address string 14 | City string 15 | State string 16 | } 17 | 18 | type MenuItem struct { 19 | MenuID int `json:"menu_id"` 20 | RestaurantID int `json:"restaurant_id"` 21 | Name string `json:"name"` 22 | Description string `json:"description"` 23 | Photo string `json:"photo"` 24 | Price float64 `json:"price"` 25 | Category string `json:"category"` 26 | Available bool `json:"available"` 27 | CreatedAt time.Time `json:"CreatedAt"` 28 | UpdatedAt time.Time `json:"UpdatedAt"` 29 | } 30 | 31 | func GenerateData(restaurant FakeRestaurant) (*bytes.Buffer, string, error) { 32 | var buffer bytes.Buffer 33 | writer := multipart.NewWriter(&buffer) 34 | 35 | _ = writer.WriteField("name", restaurant.Name) 36 | 37 | fileWriter, _ := writer.CreateFormFile("file", "restaurant.jpg") 38 | _, _ = fileWriter.Write(restaurant.File) 39 | 40 | _ = writer.WriteField("description", restaurant.Description) 41 | _ = writer.WriteField("address", restaurant.Address) 42 | _ = writer.WriteField("city", restaurant.City) 43 | _ = writer.WriteField("state", restaurant.State) 44 | 45 | _ = writer.Close() 46 | 47 | return &buffer, writer.FormDataContentType(), nil 48 | 49 | } 50 | -------------------------------------------------------------------------------- /pkg/tests/restaurant/restaurant_menu_test.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/handler/restaurant" 6 | restro "Go_Food_Delivery/pkg/service/restaurant" 7 | "Go_Food_Delivery/pkg/tests" 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "github.com/go-faker/faker/v4" 12 | "github.com/stretchr/testify/assert" 13 | "net/http" 14 | "net/http/httptest" 15 | "os" 16 | "testing" 17 | ) 18 | 19 | func TestRestaurantMenu(t *testing.T) { 20 | t.Setenv("APP_ENV", "TEST") 21 | t.Setenv("STORAGE_TYPE", "local") 22 | t.Setenv("STORAGE_DIRECTORY", "uploads") 23 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 24 | testDB := tests.Setup() 25 | AppEnv := os.Getenv("APP_ENV") 26 | testServer := handler.NewServer(testDB, false) 27 | 28 | // Restaurant 29 | restaurantService := restro.NewRestaurantService(testDB, AppEnv) 30 | restaurant.NewRestaurantHandler(testServer, "/restaurant", restaurantService) 31 | 32 | var RestaurantResponseID int64 33 | var RestaurantMenuID int64 34 | name := faker.Name() 35 | file := []byte{10, 10, 10, 10, 10} // fake image bytes 36 | description := faker.Paragraph() 37 | address := faker.Word() 38 | city := faker.Word() 39 | state := faker.Word() 40 | 41 | type FakeRestaurantMenu struct { 42 | RestaurantID int64 `json:"restaurant_id"` 43 | Name string `json:"name"` 44 | Description string `json:"description"` 45 | Price float64 `json:"price"` 46 | Category string `json:"category"` 47 | Available bool `json:"available"` 48 | } 49 | 50 | form := FakeRestaurant{ 51 | Name: name, 52 | File: file, 53 | Description: description, 54 | Address: address, 55 | City: city, 56 | State: state, 57 | } 58 | 59 | body, contentType, err := GenerateData(form) 60 | if err != nil { 61 | t.Fatalf("Error generating form-data: %v", err) 62 | } 63 | 64 | t.Run("Restaurant::Create", func(t *testing.T) { 65 | 66 | req, _ := http.NewRequest(http.MethodPost, "/restaurant/", body) 67 | req.Header.Set("Content-Type", contentType) 68 | w := httptest.NewRecorder() 69 | testServer.Gin.ServeHTTP(w, req) 70 | assert.Equal(t, http.StatusCreated, w.Code) 71 | 72 | }) 73 | 74 | t.Run("Restaurant::Listing", func(t *testing.T) { 75 | 76 | type RestaurantResponse struct { 77 | RestaurantID int64 `json:"restaurant_id"` 78 | Name string `json:"name"` 79 | StoreImage string `json:"store_image"` 80 | Description string `json:"description"` 81 | Address string `json:"address"` 82 | City string `json:"city"` 83 | State string `json:"state"` 84 | CreatedAt string `json:"CreatedAt"` 85 | UpdatedAt string `json:"UpdatedAt"` 86 | } 87 | 88 | req, _ := http.NewRequest(http.MethodGet, "/restaurant/", nil) 89 | req.Header.Set("Content-Type", "application/json") 90 | w := httptest.NewRecorder() 91 | 92 | testServer.Gin.ServeHTTP(w, req) 93 | assert.Equal(t, http.StatusOK, w.Code) 94 | 95 | var restaurants []RestaurantResponse 96 | err := json.Unmarshal(w.Body.Bytes(), &restaurants) 97 | if err != nil { 98 | t.Fatalf("Failed to decode response body: %v", err) 99 | } 100 | 101 | // set the restaurantID 102 | RestaurantResponseID = restaurants[0].RestaurantID 103 | 104 | }) 105 | 106 | t.Run("RestaurantMenu::Create", func(t *testing.T) { 107 | var customMenu FakeRestaurantMenu 108 | 109 | customMenu.Available = true 110 | customMenu.Price = 40.35 111 | customMenu.Name = "burger" 112 | customMenu.Description = "burger" 113 | customMenu.Category = "FAST_FOODS" 114 | customMenu.RestaurantID = RestaurantResponseID 115 | payload, err := json.Marshal(&customMenu) 116 | if err != nil { 117 | t.Fatal("Error::", err) 118 | } 119 | req, _ := http.NewRequest(http.MethodPost, "/restaurant/menu", bytes.NewBuffer(payload)) 120 | req.Header.Set("Content-Type", "application/json") 121 | w := httptest.NewRecorder() 122 | testServer.Gin.ServeHTTP(w, req) 123 | assert.Equal(t, http.StatusCreated, w.Code) 124 | 125 | }) 126 | 127 | t.Run("RestaurantMenu::List", func(t *testing.T) { 128 | req, _ := http.NewRequest(http.MethodGet, "/restaurant/menu", nil) 129 | req.Header.Set("Content-Type", "application/json") 130 | w := httptest.NewRecorder() 131 | testServer.Gin.ServeHTTP(w, req) 132 | 133 | var menuItems []MenuItem 134 | err := json.Unmarshal(w.Body.Bytes(), &menuItems) 135 | if err != nil { 136 | fmt.Println("Error unmarshalling JSON:", err) 137 | return 138 | } 139 | 140 | RestaurantMenuID = int64(menuItems[0].MenuID) 141 | 142 | assert.Equal(t, http.StatusOK, w.Code) 143 | 144 | }) 145 | 146 | t.Run("RestaurantMenu::List::ById", func(t *testing.T) { 147 | url := fmt.Sprintf("%s%d", "/restaurant/menu?restaurant_id=", RestaurantMenuID) 148 | req, _ := http.NewRequest(http.MethodGet, url, nil) 149 | req.Header.Set("Content-Type", "application/json") 150 | w := httptest.NewRecorder() 151 | testServer.Gin.ServeHTTP(w, req) 152 | assert.Equal(t, http.StatusOK, w.Code) 153 | }) 154 | 155 | t.Run("RestaurantMenu::Delete", func(t *testing.T) { 156 | url := fmt.Sprintf("%s%d/%d", "/restaurant/menu/", RestaurantResponseID, RestaurantMenuID) 157 | req, _ := http.NewRequest(http.MethodDelete, url, nil) 158 | req.Header.Set("Content-Type", "application/json") 159 | w := httptest.NewRecorder() 160 | testServer.Gin.ServeHTTP(w, req) 161 | 162 | assert.Equal(t, http.StatusNoContent, w.Code) 163 | 164 | }) 165 | // Cleanup 166 | t.Cleanup(func() { 167 | tests.Teardown(testDB) 168 | }) 169 | 170 | } 171 | -------------------------------------------------------------------------------- /pkg/tests/restaurant/restaurant_test.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/handler" 5 | "Go_Food_Delivery/pkg/handler/restaurant" 6 | restro "Go_Food_Delivery/pkg/service/restaurant" 7 | "Go_Food_Delivery/pkg/tests" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/go-faker/faker/v4" 11 | "github.com/stretchr/testify/assert" 12 | "net/http" 13 | "net/http/httptest" 14 | "os" 15 | "testing" 16 | ) 17 | 18 | func TestRestaurant(t *testing.T) { 19 | t.Setenv("APP_ENV", "TEST") 20 | t.Setenv("STORAGE_TYPE", "local") 21 | t.Setenv("STORAGE_DIRECTORY", "uploads") 22 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 23 | testDB := tests.Setup() 24 | testServer := handler.NewServer(testDB, false) 25 | AppEnv := os.Getenv("APP_ENV") 26 | 27 | // Restaurant 28 | restaurantService := restro.NewRestaurantService(testDB, AppEnv) 29 | restaurant.NewRestaurantHandler(testServer, "/restaurant", restaurantService) 30 | 31 | var RestaurantResponseID int64 32 | name := faker.Name() 33 | file := []byte{10, 10, 10, 10, 10} // fake image bytes 34 | description := faker.Paragraph() 35 | address := faker.Word() 36 | city := faker.Word() 37 | state := faker.Word() 38 | 39 | form := FakeRestaurant{ 40 | Name: name, 41 | File: file, 42 | Description: description, 43 | Address: address, 44 | City: city, 45 | State: state, 46 | } 47 | 48 | body, contentType, err := GenerateData(form) 49 | if err != nil { 50 | t.Fatalf("Error generating form-data: %v", err) 51 | } 52 | 53 | t.Run("Restaurant::Create", func(t *testing.T) { 54 | 55 | req, _ := http.NewRequest(http.MethodPost, "/restaurant/", body) 56 | req.Header.Set("Content-Type", contentType) 57 | w := httptest.NewRecorder() 58 | testServer.Gin.ServeHTTP(w, req) 59 | assert.Equal(t, http.StatusCreated, w.Code) 60 | 61 | }) 62 | 63 | t.Run("Restaurant::Listing", func(t *testing.T) { 64 | 65 | type RestaurantResponse struct { 66 | RestaurantID int64 `json:"restaurant_id"` 67 | Name string `json:"name"` 68 | StoreImage string `json:"store_image"` 69 | Description string `json:"description"` 70 | Address string `json:"address"` 71 | City string `json:"city"` 72 | State string `json:"state"` 73 | CreatedAt string `json:"CreatedAt"` 74 | UpdatedAt string `json:"UpdatedAt"` 75 | } 76 | 77 | req, _ := http.NewRequest(http.MethodGet, "/restaurant/", nil) 78 | req.Header.Set("Content-Type", "application/Json") 79 | w := httptest.NewRecorder() 80 | testServer.Gin.ServeHTTP(w, req) 81 | assert.Equal(t, http.StatusOK, w.Code) 82 | 83 | var restaurants []RestaurantResponse 84 | err := json.Unmarshal(w.Body.Bytes(), &restaurants) 85 | if err != nil { 86 | t.Fatalf("Failed to decode response body: %v", err) 87 | } 88 | 89 | // set the restaurantID 90 | RestaurantResponseID = restaurants[0].RestaurantID 91 | 92 | }) 93 | 94 | t.Run("Restaurant::GetById", func(t *testing.T) { 95 | url := fmt.Sprintf("/restaurant/%d", RestaurantResponseID) 96 | req, _ := http.NewRequest(http.MethodGet, url, nil) 97 | req.Header.Set("Content-Type", "application/json") 98 | w := httptest.NewRecorder() 99 | testServer.Gin.ServeHTTP(w, req) 100 | assert.Equal(t, http.StatusOK, w.Code) 101 | }) 102 | 103 | t.Run("Restaurant::Delete", func(t *testing.T) { 104 | url := fmt.Sprintf("/restaurant/%d", RestaurantResponseID) 105 | req, _ := http.NewRequest(http.MethodDelete, url, nil) 106 | req.Header.Set("Content-Type", "application/json") 107 | w := httptest.NewRecorder() 108 | testServer.Gin.ServeHTTP(w, req) 109 | assert.Equal(t, http.StatusNoContent, w.Code) 110 | }) 111 | 112 | // Cleanup 113 | t.Cleanup(func() { 114 | tests.Teardown(testDB) 115 | }) 116 | 117 | } 118 | -------------------------------------------------------------------------------- /pkg/tests/restaurant/review_test.go: -------------------------------------------------------------------------------- 1 | package restaurant 2 | 3 | import ( 4 | "Go_Food_Delivery/cmd/api/middleware" 5 | restroModel "Go_Food_Delivery/pkg/database/models/restaurant" 6 | "Go_Food_Delivery/pkg/database/models/review" 7 | userModel "Go_Food_Delivery/pkg/database/models/user" 8 | "Go_Food_Delivery/pkg/handler" 9 | revw "Go_Food_Delivery/pkg/handler/review" 10 | restro "Go_Food_Delivery/pkg/service/restaurant" 11 | reviewSrv "Go_Food_Delivery/pkg/service/review" 12 | usr "Go_Food_Delivery/pkg/service/user" 13 | "Go_Food_Delivery/pkg/tests" 14 | "context" 15 | "encoding/json" 16 | "fmt" 17 | "github.com/gin-gonic/gin" 18 | "github.com/go-faker/faker/v4" 19 | "github.com/go-playground/validator/v10" 20 | "github.com/stretchr/testify/assert" 21 | "net/http" 22 | "net/http/httptest" 23 | "os" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | func TestReview(t *testing.T) { 29 | t.Setenv("APP_ENV", "TEST") 30 | t.Setenv("STORAGE_TYPE", "local") 31 | t.Setenv("STORAGE_DIRECTORY", "uploads") 32 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 33 | testDB := tests.Setup() 34 | validate := validator.New() 35 | AppEnv := os.Getenv("APP_ENV") 36 | testServer := handler.NewServer(testDB, false) 37 | middlewares := []gin.HandlerFunc{middleware.AuthMiddleware()} 38 | 39 | userService := usr.NewUserService(testDB, AppEnv) 40 | restaurantService := restro.NewRestaurantService(testDB, AppEnv) 41 | reviewService := reviewSrv.NewReviewService(testDB, AppEnv) 42 | revw.NewReviewProtectedHandler(testServer, "/review", reviewService, middlewares, validate) 43 | 44 | type FakeUser struct { 45 | User string `json:"user" faker:"name"` 46 | Email string `json:"email" faker:"email"` 47 | Password string `json:"password" faker:"password"` 48 | } 49 | var ReviewResponseID int64 50 | 51 | var customUser FakeUser 52 | var user userModel.User 53 | var restrro restroModel.Restaurant 54 | _ = faker.FakeData(&customUser) 55 | user.Email = customUser.Email 56 | user.Password = customUser.Password 57 | 58 | ctx := context.Background() 59 | _, err := userService.Add(ctx, &user) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | 64 | loginToken, err := userService.Login(ctx, user.ID, "Food Delivery") 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | Token := fmt.Sprintf("Bearer %s", loginToken) 70 | 71 | // Restaurant 72 | name := faker.Name() 73 | description := faker.Paragraph() 74 | address := faker.Word() 75 | city := faker.Word() 76 | state := faker.Word() 77 | 78 | restrro.Name = name 79 | restrro.Description = description 80 | restrro.Address = address 81 | restrro.City = city 82 | restrro.State = state 83 | 84 | _, err = restaurantService.Add(ctx, &restrro) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | restaurants, err := restaurantService.ListRestaurants(ctx) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | restaurantId := restaurants[0].RestaurantID 94 | 95 | t.Run("Review::Create", func(t *testing.T) { 96 | var reviewParam review.ReviewParams 97 | reviewParam.Comment = faker.Word() 98 | reviewParam.Rating = 4 99 | payload, err := json.Marshal(&reviewParam) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | req, _ := http.NewRequest(http.MethodPost, 105 | fmt.Sprintf("/review/%d", restaurantId), 106 | strings.NewReader(string(payload))) 107 | req.Header.Set("Content-Type", "application/json") 108 | req.Header.Set("Authorization", Token) 109 | w := httptest.NewRecorder() 110 | testServer.Gin.ServeHTTP(w, req) 111 | assert.Equal(t, http.StatusCreated, w.Code) 112 | 113 | }) 114 | 115 | t.Run("Review::List", func(t *testing.T) { 116 | type ReviewResponse struct { 117 | ReviewID int64 `json:"review_id"` 118 | UserID int64 `json:"user_id"` 119 | RestaurantID int64 `json:"restaurant_id"` 120 | Rating int `json:"rating"` 121 | Comment string `json:"comment"` 122 | } 123 | req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/review/%d", restaurantId), nil) 124 | req.Header.Set("Content-Type", "application/json") 125 | req.Header.Set("Authorization", Token) 126 | w := httptest.NewRecorder() 127 | testServer.Gin.ServeHTTP(w, req) 128 | assert.Equal(t, http.StatusOK, w.Code) 129 | 130 | var allReviews []ReviewResponse 131 | err := json.Unmarshal(w.Body.Bytes(), &allReviews) 132 | if err != nil { 133 | t.Fatalf("Failed to decode response body: %v", err) 134 | } 135 | 136 | ReviewResponseID = allReviews[0].ReviewID 137 | 138 | }) 139 | 140 | t.Run("Review::Delete", func(t *testing.T) { 141 | url := fmt.Sprintf("/review/%d", ReviewResponseID) 142 | req, _ := http.NewRequest(http.MethodDelete, url, nil) 143 | req.Header.Set("Content-Type", "application/json") 144 | req.Header.Set("Authorization", Token) 145 | w := httptest.NewRecorder() 146 | testServer.Gin.ServeHTTP(w, req) 147 | assert.Equal(t, http.StatusNoContent, w.Code) 148 | }) 149 | 150 | t.Cleanup(func() { 151 | tests.Teardown(testDB) 152 | }) 153 | 154 | } 155 | -------------------------------------------------------------------------------- /pkg/tests/setup.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "Go_Food_Delivery/pkg/database" 5 | "log" 6 | "log/slog" 7 | "os" 8 | ) 9 | 10 | // Setup will be bootstrapping our test db. 11 | func Setup() database.Database { 12 | slog.Info("Initializing Setup..") 13 | testDb := database.NewTestDB() 14 | 15 | if err := testDb.Migrate(); err != nil { 16 | log.Fatalf("Error migrating database: %s", err) 17 | } 18 | return testDb 19 | } 20 | 21 | func Teardown(testDB database.Database) { 22 | err := testDB.Close() 23 | if err != nil { 24 | log.Fatalf("Error closing testDB: %s", err) 25 | } 26 | err = os.RemoveAll("./tmp") 27 | if err != nil { 28 | log.Fatalf("Error removing ./tmp: %s", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/tests/user/user_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | userModel "Go_Food_Delivery/pkg/database/models/user" 5 | "Go_Food_Delivery/pkg/handler" 6 | "Go_Food_Delivery/pkg/handler/user" 7 | usr "Go_Food_Delivery/pkg/service/user" 8 | "Go_Food_Delivery/pkg/tests" 9 | "encoding/json" 10 | "github.com/go-faker/faker/v4" 11 | "github.com/go-playground/validator/v10" 12 | "github.com/stretchr/testify/assert" 13 | "net/http" 14 | "net/http/httptest" 15 | "os" 16 | "strings" 17 | "testing" 18 | ) 19 | 20 | func TestAddUser(t *testing.T) { 21 | t.Setenv("APP_ENV", "TEST") 22 | t.Setenv("STORAGE_TYPE", "local") 23 | t.Setenv("STORAGE_DIRECTORY", "uploads") 24 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 25 | testDB := tests.Setup() 26 | AppEnv := os.Getenv("APP_ENV") 27 | testServer := handler.NewServer(testDB, false) 28 | 29 | validate := validator.New() 30 | userService := usr.NewUserService(testDB, AppEnv) 31 | user.NewUserHandler(testServer, "/user", userService, validate) 32 | 33 | type FakeUser struct { 34 | Name string `json:"name" faker:"name"` 35 | Email string `json:"email" faker:"email"` 36 | Password string `json:"password" faker:"password"` 37 | } 38 | 39 | var loggedInUser userModel.LoginUser 40 | 41 | t.Run("User::Create", func(t *testing.T) { 42 | 43 | var customUser FakeUser 44 | _ = faker.FakeData(&customUser) 45 | payload, err := json.Marshal(&customUser) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | loggedInUser.Email = customUser.Email 51 | loggedInUser.Password = customUser.Password 52 | 53 | req, _ := http.NewRequest(http.MethodPost, "/user/", strings.NewReader(string(payload))) 54 | req.Header.Set("Content-Type", "application/json") 55 | w := httptest.NewRecorder() 56 | testServer.Gin.ServeHTTP(w, req) 57 | assert.Equal(t, http.StatusCreated, w.Code) 58 | }) 59 | 60 | t.Run("User::Login", func(t *testing.T) { 61 | 62 | payload, err := json.Marshal(&loggedInUser) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | req, _ := http.NewRequest(http.MethodPost, "/user/login", strings.NewReader(string(payload))) 68 | req.Header.Set("Content-Type", "application/json") 69 | w := httptest.NewRecorder() 70 | testServer.Gin.ServeHTTP(w, req) 71 | assert.Equal(t, http.StatusOK, w.Code) 72 | 73 | }) 74 | 75 | t.Cleanup(func() { 76 | tests.Teardown(testDB) 77 | }) 78 | 79 | } 80 | 81 | func TestDeleteUser(t *testing.T) { 82 | t.Setenv("APP_ENV", "TEST") 83 | t.Setenv("STORAGE_TYPE", "local") 84 | t.Setenv("STORAGE_DIRECTORY", "uploads") 85 | t.Setenv("LOCAL_STORAGE_PATH", "./tmp") 86 | testDB := tests.Setup() 87 | AppEnv := os.Getenv("APP_ENV") 88 | testServer := handler.NewServer(testDB, false) 89 | 90 | validate := validator.New() 91 | userService := usr.NewUserService(testDB, AppEnv) 92 | user.NewUserHandler(testServer, "/user", userService, validate) 93 | 94 | type FakeUser struct { 95 | Name string `json:"name" faker:"name"` 96 | Email string `json:"email" faker:"email"` 97 | Password string `json:"password" faker:"password"` 98 | } 99 | 100 | var loggedInUser userModel.LoginUser 101 | 102 | t.Run("User::Create", func(t *testing.T) { 103 | 104 | var customUser FakeUser 105 | _ = faker.FakeData(&customUser) 106 | payload, err := json.Marshal(&customUser) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | loggedInUser.Email = customUser.Email 112 | loggedInUser.Password = customUser.Password 113 | 114 | req, _ := http.NewRequest(http.MethodPost, "/user/", strings.NewReader(string(payload))) 115 | req.Header.Set("Content-Type", "application/json") 116 | w := httptest.NewRecorder() 117 | testServer.Gin.ServeHTTP(w, req) 118 | assert.Equal(t, http.StatusCreated, w.Code) 119 | }) 120 | 121 | t.Run("User::Delete", func(t *testing.T) { 122 | req, _ := http.NewRequest(http.MethodDelete, "/user/1", nil) 123 | req.Header.Set("Content-Type", "application/json") 124 | w := httptest.NewRecorder() 125 | testServer.Gin.ServeHTTP(w, req) 126 | assert.Equal(t, http.StatusNoContent, w.Code) 127 | 128 | }) 129 | 130 | t.Cleanup(func() { 131 | tests.Teardown(testDB) 132 | }) 133 | 134 | } 135 | --------------------------------------------------------------------------------