├── .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 | 
4 | 
5 |
6 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------