Repository: gwuah/postmates Branch: master Commit: 9317d13f54ff Files: 65 Total size: 109.9 KB Directory structure: gitextract_qfqtsf5j/ ├── .dockerignore ├── .gitignore ├── API_DOCS.md ├── Dockerfile ├── Dockerfile.wait ├── README.md ├── database/ │ ├── couriers.yml │ ├── customers.yml │ ├── models/ │ │ ├── courier.go │ │ ├── customer.go │ │ ├── delivery.go │ │ ├── order.go │ │ ├── product.go │ │ ├── tripPoint.go │ │ └── vehicle.go │ ├── postgres/ │ │ └── postgres.go │ ├── products.yml │ ├── redis/ │ │ └── redis.go │ ├── seeds.go │ └── vehicles.yml ├── demo/ │ ├── customer__closest_couriers.js │ ├── customer__delivery_request.js │ ├── electrons.js │ └── package.json ├── docker-compose.yml ├── go.mod ├── go.sum ├── handler/ │ ├── auth.go │ ├── base.go │ ├── courier.go │ ├── customer.go │ ├── delivery.go │ ├── handler.go │ ├── order.go │ └── ws.go ├── lib/ │ ├── billing/ │ │ └── billing.go │ ├── eta/ │ │ └── eta.go │ ├── sms/ │ │ └── sms.go │ └── ws/ │ ├── connection.go │ ├── hub.go │ └── room.go ├── main.go ├── middleware/ │ ├── cors.go │ └── jwt.go ├── plg/ │ └── handy.go ├── repository/ │ ├── courier.go │ ├── customer.go │ ├── delivery.go │ ├── order.go │ ├── product.go │ ├── repository.go │ └── tripPoint.go ├── server/ │ └── server.go ├── services/ │ ├── base.go │ ├── courier.go │ ├── delivery.go │ ├── dispatch.go │ ├── order.go │ └── services.go ├── shared/ │ └── types.go └── utils/ ├── geo/ │ └── geo.go ├── jwt/ │ └── jwt.go ├── secure/ │ └── secure.go ├── utils.go └── validator/ └── validator.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ Dockerfile docker-compose.yml ./postmates ./postmates-app ================================================ FILE: .gitignore ================================================ .env node_modules/ main ================================================ FILE: API_DOCS.md ================================================ # Docs ### Courier can initiate connection @ `/courier/realtime/:id` ### Customer can initiate connection @ `/customer/realtime/:id` see `handler/ws.go` and `handler/handler.go` for more details with regards to realtime connections. ### Get Delivery Quote `/v1/get-delivery-cost` **method:** POST **data params:** ``` { origin: { latitude: 5.677474538991623, longitude: -0.24460022375167725 }, destination: { latitude: 5.6796946725653745, longitude: -0.2447180449962616 } } ``` **response:** ``` { data: { estimate: { 1: { productId: 1, price: 5 } }, distance: 2.3166666666666664, duration: 319 }, message: "success" } ``` ### Rate Delivery (Customer) `/v1/customer-rate-trip` **method:** POST **data params:** ``` { message: "Good Service", deliveryId: 1, customerId: 1, rating: 5 } ``` **response:** ``` { message: "success" data: true } ``` ### Rate Delivery (Courier) `/v1/courier-rate-trip` **method:** POST **data params:** ``` { message: "Good Service", deliveryId: 1, courierId: 1 rating: 5 } ``` **response:** ``` { message: "success" data: true } ``` ================================================ FILE: Dockerfile ================================================ FROM golang:1.14-alpine AS main-env # install gcc for uber/h3-go. see https://github.com/uber/h3/issues/354 RUN apk add build-base RUN mkdir /app ARG PORT=8080 ENV PORT=${PORT} ADD . /app/ WORKDIR /app RUN cd /app # Attempt to cache the module retrieval RUN go mod download RUN go build -o postmates-app FROM alpine WORKDIR /app COPY --from=main-env /app/postmates-app /app COPY --from=main-env /app/database /app/database COPY .env /app/.env EXPOSE $PORT CMD ["/app/postmates-app"] ================================================ FILE: Dockerfile.wait ================================================ FROM alpine # Add docker-compose-wait tool ------------------- ENV WAIT_VERSION 2.7.3 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait RUN chmod +x /wait CMD ["/wait"] ================================================ FILE: README.md ================================================ # Postmates This is the heart of a delivery service. Features include geo-indexing, order-dispatch, proximity-searching, ETA, trip estimates, etc
We use google maps for features such as distance-matrix and directions
Find more documentation [here](https://github.com/gwuah/postmates/blob/master/API_DOCS.md) # Inbuilt Features - [x] geo-indexing - [x] geo-radius search - [x] ETA - [x] order creation - [x] order dispatch - [x] order acceptance - [x] order order rejection - [x] customer login/signup - [x] customer ratings # Requirements - Postgres - Redis - Uber H3 # Architecture - We use websocket connections for realtime communications with courier and customers. The ws connections are store in-memory in a concurrency safe manner. - Couriers are indexed using uber's h3 geo-indexing library and grouped in redis. - When you perform a radius search(closest couriers), we use h3 to calculate all indices 2 levels at resolution 8, see image below. Then we query our courier index, powered by redis to find all the couriers in those locations. Then, we make a request using their lng/lats and the customer's lng/lat to google maps to get the distance and duration from the customer, then we sort that result, and then dispatch the order to these couriers in order of those closest the origin of the request. (See [image](https://github.com/gwuah/postmates/blob/master/img/radius.png) - The couriers send location updates every 3 seconds. This allows us to know their locations in almost realtime. - The dispatch logic gives a courier 5 seconds to accept an order, after which it is sent to the next closest/available courier. If none of the available couriers accept the request, the process starts all over again, till someone finally accepts it. ## Project Setup 1. Clone the repo and make a copy of .env.sample as .env & update the env vars. ```bash git clone https://github.com/gwuah/postmates.git cp .env.sample .env ``` 2. Run the app using either : ```bash go run main.go ``` ```bash go build main.go ./main ``` # Demo - `cd demo` and run `yarn` to install all required dependencies. - run `node electrons.js` to initiate 3 couriers instances that are constantly sending location updates every 3 seconds - run `node customer__delivery_request.js` to instantiate a customer that will create a delivery request. - pay attention to the logs. ================================================ FILE: database/couriers.yml ================================================ - Griffith Awuah - Andy Osei - Yaw Manu ================================================ FILE: database/customers.yml ================================================ - Alicia Keys ================================================ FILE: database/models/courier.go ================================================ package models type Courier struct { Model FirstName string `json:"firstName"` LastName string `json:"lastName"` MiddleName string `json:"middleName"` Longitude float64 `json:"longitude"` Latitude float64 `json:"latitude"` State State `json:"state"` Vehicle *Vehicle `json:"vehicle,omitempty"` // Deliveries []Delivery `json:"deliveries"` PhotoUrl string `json:"photoUrl"` Rating int `json:"rating"` } ================================================ FILE: database/models/customer.go ================================================ package models import ( "time" ) type Model struct { ID uint `gorm:"primary_key" json:"id"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt *time.Time `sql:"index" json:"deletedAt"` } type Customer struct { Model State State `json:"state"` Phone string `gorm:"not null;unique" json:"phone"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Email string `json:"email"` Longitude float64 `json:"longitude"` Latitude float64 `json:"latitude"` Code int `json:"code"` Active bool `json:"active" gorm:"default=false"` Token string `json:"-"` Rating int `json:"rating"` } ================================================ FILE: database/models/delivery.go ================================================ package models type State string const ( // delivery state types Pending State = "pending" PendingPickup = "pending_pickup" NearingPickup = "nearing_pickup" AtPickup = "at_pickup" DeliveryOngoing = "delivery_ongoing" NearingDropoff = "nearing_dropoff" AtDropOff = "at_dropoff" Completed = "completed" Cancelled = "cancelled" // courier state types AwaitingDispatch = "awaiting_dispatch" Dispatched = "dispatched" OnTrip = "on_trip" Offline = "offline" // vehicle state Inactive = "inactive" // customer state Searching = "searching" AtRest = "atRest" ) type Delivery struct { Model State State `json:"state"` OriginLongitude float64 `json:"originLongitude"` OriginLatitude float64 `json:"originLatitude"` DestinationLongitude float64 `json:"destinationLongitude"` DestinationLatitude float64 `json:"destinationLatitude"` FinalCost float64 `json:"finalCost"` InitialCost float64 `json:"initialCost"` Completed bool `json:"completed"` Notes string `json:"notes"` CustomerID uint `json:"customerId"` Customer Customer `json:"customer"` ProductID uint `json:"productId"` Product Product `json:"product"` CourierID *uint `json:"courierId,omitempty"` Courier *Courier `json:"courier,omitempty"` TripPoints []*TripPoint `json:"tripPoints,omitempty"` CourierRating float64 `json:"courierRating"` CourierRatingMessage string `json:"courierRatingMessage"` CustomerRating float64 `json:"customerRating"` CustomerRatingMessage string `json:"customerRatingMessage"` } ================================================ FILE: database/models/order.go ================================================ package models type Order struct { Model // Deliveries []Delivery `json:"deliveries"` CourierID uint `json:"courierId"` Courier Courier `json:"courier"` Completed bool `json:"completed"` State State `json:"state"` } ================================================ FILE: database/models/product.go ================================================ package models type Product struct { Model Name string `json:"name"` } ================================================ FILE: database/models/tripPoint.go ================================================ package models type TripPoint struct { Model DeliveryID uint `json:"deliveryId"` Delivery *Delivery `json:"delivery"` Longitude float64 `json:"longitude"` Latitude float64 `json:"latitude"` State State `json:"state"` } ================================================ FILE: database/models/vehicle.go ================================================ package models type VehicleType string const ( Motor VehicleType = "motor" Car = "car" ) type Vehicle struct { Model RegNumber string `gorm:"not null;unique" json:"regNumber"` VehicleModel string `json:"vehicleModel"` Type VehicleType `json:"vehicleType"` CourierID uint `json:"courierId"` State State `json:"state"` Active bool `json:"active" gorm:"default=false"` } ================================================ FILE: database/postgres/postgres.go ================================================ package postgres import ( "fmt" "os" "gorm.io/driver/postgres" "gorm.io/gorm" ) type Config struct { Host string Port string Password string User string DBName string SSLMode string DBurl string } func SetupDatabase(db *gorm.DB, models ...interface{}) error { err := db.AutoMigrate(models...) return err } func New(config *Config) (*gorm.DB, error) { var ( db *gorm.DB err error ) dsn := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode, ) if os.Getenv("ENV") == "staging" || os.Getenv("ENV") == "production" { db, err = gorm.Open(postgres.Open(config.DBurl), &gorm.Config{}) } else { db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) } if err != nil { return nil, err } return db, nil } ================================================ FILE: database/products.yml ================================================ - Express - Pool ================================================ FILE: database/redis/redis.go ================================================ package redis import ( "net/url" "os" "github.com/go-redis/redis" ) type Config struct { Addr string Password string DB int DBurl string } func New(config *Config) *redis.Client { if os.Getenv("ENV") == "staging" || os.Getenv("ENV") == "production" { parsedURL, _ := url.Parse(config.DBurl) password, _ := parsedURL.User.Password() return redis.NewClient(&redis.Options{ Addr: parsedURL.Host, Password: password, }) } return redis.NewClient(&redis.Options{ Addr: config.Addr, Password: config.Password, DB: config.DB, }) } ================================================ FILE: database/seeds.go ================================================ package database import ( "fmt" "log" "os" "strings" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/utils" "github.com/kylelemons/go-gypsy/yaml" "gorm.io/gorm" ) type SeedFn func(db *gorm.DB, path string) func RunSeeds(db *gorm.DB, seeds []SeedFn) { path, err := os.Getwd() if err != nil { panic(err) } for _, seed := range seeds { seed(db, path) } } func SeedProducts(DB *gorm.DB, path string) { config, err := yaml.ReadFile(path + "/database/products.yml") if err != nil { panic(err) } productList, ok := config.Root.(yaml.List) if !ok { panic("failed to parse product.yml") } for i := 0; i < productList.Len(); i++ { productName := strings.ToLower(fmt.Sprintf("%s", productList.Item(i))) var product models.Product if err := DB.Where("name = ?", productName).First(&product).Error; err != nil { if err == gorm.ErrRecordNotFound { DB.Create(&models.Product{Name: productName}) } else { log.Printf("Product [ %s ] lookup failed", productName) log.Println(err) } } } } func SeedCouriers(DB *gorm.DB, path string) { config, err := yaml.ReadFile(path + "/database/couriers.yml") if err != nil { panic(err) } courierList, ok := config.Root.(yaml.List) if !ok { panic("failed to parse product.yml") } for i := 0; i < courierList.Len(); i++ { name := strings.ToLower(fmt.Sprintf("%s", courierList.Item(i))) firstName := strings.Split(name, " ")[0] lastName := strings.Split(name, " ")[1] var courier models.Courier if err := DB.Where("first_name = ? AND last_name = ?", firstName, lastName).First(&courier).Error; err != nil { if err == gorm.ErrRecordNotFound { DB.Create(&models.Courier{FirstName: firstName, LastName: lastName}) } else { log.Printf("Courier [ %s ] lookup failed", name) log.Println(err) } } } } func SeedCustomers(DB *gorm.DB, path string) { config, err := yaml.ReadFile(path + "/database/customers.yml") if err != nil { panic(err) } customerList, ok := config.Root.(yaml.List) if !ok { panic("failed to parse product.yml") } for i := 0; i < customerList.Len(); i++ { name := strings.ToLower(fmt.Sprintf("%s", customerList.Item(i))) firstName := strings.Split(name, " ")[0] lastName := strings.Split(name, " ")[1] var customer models.Customer if err := DB.Where("first_name = ? AND last_name = ?", firstName, lastName).First(&customer).Error; err != nil { if err == gorm.ErrRecordNotFound { DB.Create(&models.Customer{FirstName: firstName, LastName: lastName, Active: true}) } else { log.Printf("Customer [ %s ] lookup failed", name) log.Println(err) } } } } func SeedVehicles(DB *gorm.DB, path string) { config, err := yaml.ReadFile(path + "/database/vehicles.yml") if err != nil { panic(err) } c, ok := config.Root.(yaml.List) if !ok { panic("failed to parse vehicles.yml") } cd, ok := c.Item(0).(yaml.Map) if !ok { panic("failed to parse vehicles.yml") } l, ok := cd["data"].(yaml.List) if !ok { panic("failed to parse vehicles.yml") } for _, v := range l { value, ok := v.(yaml.Map) if !ok { panic("failed to parse vehicles.yml") } courierId := fmt.Sprintf("%v", value["courierId"]) vehicleModel := fmt.Sprintf("%v", value["vehicleModel"]) regNumber := fmt.Sprintf("%v", value["regNumber"]) Type := fmt.Sprintf("%v", value["type"]) vehicle := models.Vehicle{ CourierID: uint(utils.ConvertToUint64(courierId)), VehicleModel: vehicleModel, RegNumber: regNumber, Type: utils.ConvertToVehicleType(Type), Active: false, } if err := DB.Where("reg_number = ?", vehicle.RegNumber).First(&models.Vehicle{}).Error; err != nil { if err == gorm.ErrRecordNotFound { DB.Create(&vehicle) } else { log.Printf("Vehicle [ %s ] lookup failed", vehicle.RegNumber) log.Println(err) } } } } ================================================ FILE: database/vehicles.yml ================================================ - data: - vehicleModel: yahama regNumber: GX 4888-10 type: motor courierId: 1 - vehicleModel: yahama regNumber: GM 44-10 type: motor courierId: 2 - vehicleModel: kia morning regNumber: GE 4993-10 type: Car courierId: 3 ================================================ FILE: demo/customer__closest_couriers.js ================================================ const WebSocket = require("ws"); function connect(id) { console.log(`Customer ${id} initating a connection ... `); let ws = new WebSocket(`ws://localhost:8080/v1/customer/realtime/${id}`); ws.on("open", (e) => { console.log("connection successful"); setInterval(() => { ws.send( JSON.stringify({ meta: { type: "GetClosestCouriers", }, id: id, origin: { latitude: 5.6796946725653745, longitude: -0.2447180449962616, }, }) ); }, 2000); }); ws.on("message", function (data) { console.log(data); }); ws.on("error", function (data) { console.log("Error connecting"); }); } function parseMessage(message) {} function main() { connect(process.argv[2]); } main(); ================================================ FILE: demo/customer__delivery_request.js ================================================ const WebSocket = require("ws"); function connect(id) { console.log(`Customer ${id} initating a connection ... `); let ws = new WebSocket(`ws://localhost:8080/v1/customer/realtime/${id}`); ws.on("open", (e) => { console.log("connection successful"); setTimeout(() => { ws.send( JSON.stringify({ meta: { type: "DeliveryRequest", }, productId: 1, customerID: 1, notes: "Handle it carefully", origin: { latitude: 5.6796946725653745, longitude: -0.2447180449962616, }, destination: { longitude: 2.4345545, latitude: 4.054594095, }, }) ); }, 1000); }); ws.on("message", function (data) { let parsed = JSON.parse(data); console.log(JSON.stringify(parsed, null, 4)); }); ws.on("error", function (data) { console.log("Error connecting"); }); } function main() { connect("PostMaster"); } main(); ================================================ FILE: demo/electrons.js ================================================ const WebSocket = require("ws"); const outsideScope = { latitude: 5.698188535023582, longitude: -0.239341780857103, }; const defaultCabPositions = [ { longitude: -0.2475990969444747, latitude: 5.684136332305188, color: "blue", }, { longitude: -0.2397266058667604, latitude: 5.683835847589247, color: "blue", }, { longitude: -0.24460022375167725, latitude: 5.677474538991623, color: "blue", }, ]; class Courier { constructor(id, coord) { this.appState = { id, coord, state: "awaiting_dispatch", courier: null, current_delivery: null, }; } _initialization() { this.ws = new WebSocket( `ws://localhost:8080/v1/courier/realtime/${this.appState.id}` ); this.ws.on("message", (msg) => { console.log("new message"); this.handleMessage(msg); }); this.ws.on("error", (data) => { console.log("Error connecting", data); }); console.log(`Courier ${this.appState.id} has been instantiated.`); this._sendLocationUpdate(); } _handleNewDelivery(parsed) { console.log(`NewDeliveryRequest Recieved ${this.appState.id} `); if (this.appState.id == "2") { console.log( `ID(${this.appState.id}) >>> `, JSON.stringify(parsed, null, 4) ); this.appState.current_delivery = parsed.delivery; this.ws.send( JSON.stringify({ meta: { type: "AcceptDelivery", }, deliveryId: parsed.delivery.id, }) ); } } _sendLocationUpdate() { const deliveryId = this.appState.current_delivery ? this.appState.current_delivery.id : null; setInterval(() => { this.ws.send( JSON.stringify({ meta: { type: "LocationUpdate", }, id: this.appState.id, latitude: this.appState.coord.latitude, longitude: this.appState.coord.longitude, state: this.appState.state, deliveryId, }) ); }, 3000); } handleMessage(message) { let parsed = JSON.parse(message); switch (parsed.meta.type) { case "NewDelivery": this._handleNewDelivery(parsed); break; } } } function main() { var c1 = new Courier("1", defaultCabPositions[0]); c1._initialization(); var c2 = new Courier("2", defaultCabPositions[1]); c2._initialization(); var c3 = new Courier("3", defaultCabPositions[2]); c3._initialization(); } main(); ================================================ FILE: demo/package.json ================================================ { "name": "demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "ws": "^7.3.1" } } ================================================ FILE: docker-compose.yml ================================================ version: '3.7' services: postgres: image: "postgres:13.1" hostname: postgres container_name: postmates-postgres env_file: .env environment: - POSTGRES_PASSWORD=${DB_PASS} - POSTGRES_DB=${DB_NAME} - POSTGRES_USER=${DB_USER} ports: - "5432:5432" redis: image: redis:5.0.10-alpine container_name: postmates-redis ports: - "6379:6379" postmates: build: dockerfile: ./Dockerfile context: . args: PORT: ${PORT} container_name: postmates-app ports: - "9000:${PORT}" env_file: .env depends_on: - postgres - redis - waiter waiter: build: dockerfile: ./Dockerfile.wait context: . container_name: postmates-waiter depends_on: - postgres - redis environment: - WAIT_HOSTS=postgres:5432, redis:6379 - WAIT_HOSTS_TIMEOUT=300 - WAIT_SLEEP_INTERVAL=30 - WAIT_HOST_CONNECT_TIMEOUT=30 - WAIT_AFTER_HOSTS=0 ================================================ FILE: go.mod ================================================ module github.com/gwuah/postmates go 1.14 // +heroku goVersion go1.14.2 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-gonic/gin v1.6.3 github.com/go-playground/validator/v10 v10.3.0 github.com/go-redis/redis v6.15.9+incompatible github.com/gorilla/websocket v1.4.2 github.com/joho/godotenv v1.3.0 github.com/json-iterator/go v1.1.10 // indirect github.com/kylelemons/go-gypsy v1.0.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d github.com/onsi/ginkgo v1.14.2 // indirect github.com/onsi/gomega v1.10.3 // indirect github.com/uber/h3-go v3.0.1+incompatible golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 google.golang.org/protobuf v1.25.0 // indirect googlemaps.github.io/maps v1.2.3 gorm.io/driver/postgres v1.0.0 gorm.io/gorm v1.20.2 ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8= github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY= github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY= github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw= github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/go-gypsy v1.0.0 h1:7/wQ7A3UL1bnqRMnZ6T8cwCOArfZCxFmb1iTxaOOo1s= github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/uber/h3-go v3.0.1+incompatible h1:RVpBm8qd7mM94YuIhNQfCXpVj6mPY6gNsVstDg1FvjY= github.com/uber/h3-go v3.0.1+incompatible/go.mod h1:66a2M4rQlf+dtkTWj3bHoLFgDT/Rt4kLT8dMuEQVQvw= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= googlemaps.github.io/maps v1.2.3 h1:zChNy7zFReU4ovIw5btSPks47imSE/OhAtn9Rn8T1wg= googlemaps.github.io/maps v1.2.3/go.mod h1:cCq0JKYAnnCRSdiaBi7Ex9CW15uxIAk7oPi8V/xEh6s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM= gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to= gorm.io/gorm v1.9.19 h1:NMrwpxOZIHWJEFzZ0MM8PdYlcXyKLaXTHWfpDEDdBNg= gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro= gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= ================================================ FILE: handler/auth.go ================================================ package handler import ( "net/http" "github.com/gin-gonic/gin" "github.com/gwuah/postmates/database/models" ) func (h *Handler) Refresh(c *gin.Context) { token := c.Param("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{ "message": "token not found", }) return } customer := new(models.Customer) result := h.DB.Where("token = ?", token).First(customer) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed to fetch customer", }) return } newToken, err := h.JWT.GenerateToken(customer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed to generate token", }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "token": newToken, }) return } ================================================ FILE: handler/base.go ================================================ package handler import ( "log" "net/http" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/gwuah/postmates/shared" myValidator "github.com/gwuah/postmates/utils/validator" ) func (h *Handler) handleCustomerRating(c *gin.Context) { var data shared.CustomerRatingRequest if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err, }) return } response, err := h.Services.RateDelivery(shared.RatingRequest{ IsCustomerRating: true, CustomerRating: data, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "data": response, }) } func (h *Handler) handleCourierRating(c *gin.Context) { var data shared.CourierRatingRequest if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err, }) return } response, err := h.Services.RateDelivery(shared.RatingRequest{ IsCustomerRating: false, CourierRating: data, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err, }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "data": response, }) } func (h *Handler) GetDeliveryCost(c *gin.Context) { var quoteRequest shared.GetDeliveryCostRequest if err := c.ShouldBindJSON("eRequest); err != nil { for _, fieldErr := range err.(validator.ValidationErrors) { c.JSON(http.StatusBadRequest, gin.H{ "message": myValidator.FieldError{Err: fieldErr}.String(), }) return } } response, err := h.Services.GetDeliveryCost(quoteRequest) if err != nil { log.Println(err) c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err, }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "data": response, }) } ================================================ FILE: handler/courier.go ================================================ package handler import ( "net/http" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/gwuah/postmates/shared" myValidator "github.com/gwuah/postmates/utils/validator" ) type closestCourierResponse struct { Couriers []string `json:"couriers"` } func (h *Handler) GetClosestCouriers(c *gin.Context) { var data shared.GetClosestCouriersRequest if err := c.ShouldBindJSON(&data); err != nil { for _, fieldErr := range err.(validator.ValidationErrors) { c.JSON(http.StatusBadRequest, gin.H{ "message": myValidator.FieldError{Err: fieldErr}.String(), }) return } } couriersWithEta, err := h.Services.GetClosestCouriers(data.Origin, 2) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failure", "err": err, }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "data": couriersWithEta, }) } ================================================ FILE: handler/customer.go ================================================ package handler import ( "fmt" "log" "net/http" "github.com/gin-gonic/gin" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/lib/sms" "github.com/gwuah/postmates/utils" "gorm.io/gorm" ) type CreateCustomerRequest struct { Phone string `json:"phone" validate:"required"` } type LoginRequest struct { Phone string `json:"phone" validate:"required"` Code int `json:"code" validate:"required"` } func (h *Handler) ListCustomers(c *gin.Context) { var customers []models.Customer if err := h.DB.Find(&customers).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed to retrieve customers", }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "customers": customers, }) } func (h *Handler) ViewCustomer(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusNotFound, gin.H{ "message": "customer id not found", }) } customer := new(models.Customer) result := h.DB.First(customer, id) if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed To retrieve customer", }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "customer": customer, }) return } func (h *Handler) sendSMS(customer models.Customer, token string) { response, err := h.SMS.SendTextMessage(sms.Message{ To: utils.GeneratePhoneNumber(customer.Phone), Sms: fmt.Sprintf("Your electra code: %s", token), }) if err != nil { log.Println(err) return } fmt.Println(response) } func (h *Handler) SignupCustomer(c *gin.Context) { var data CreateCustomerRequest if err := c.ShouldBindJSON(&data); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "failed to parse request", }) } existingCustomer, err := h.Repo.FindCustomerByPhone(data.Phone) if err != nil && err != gorm.ErrRecordNotFound { log.Println(err) c.JSON(http.StatusInternalServerError, gin.H{ "message": "Request failed", }) return } code := utils.GenerateOTP() if existingCustomer != nil { _, err = h.Repo.UpdateCustomer(existingCustomer.ID, map[string]interface{}{ "Code": code, }) go h.sendSMS(*existingCustomer, code) c.JSON(http.StatusOK, gin.H{ "message": "customer already exists, token has been sent", }) } else { record, err := h.Repo.CreateCustomerWithPhoneAndCode(data.Phone, utils.ConvertToInt(code)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "Customer Creation failed", }) return } go h.sendSMS(*record, code) c.JSON(http.StatusOK, gin.H{ "message": "new customer", "data": gin.H{ "customer": record, }, }) } } func (h *Handler) LoginCustomer(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": "failed to parse request", }) return } query := fmt.Sprintf("phone = '%s' AND code = '%d'", req.Phone, req.Code) customer, err := h.Repo.FindCustomerByQuery(query) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "message": "wrong token or phone number", }) return } token, err := h.JWT.GenerateToken(customer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed to generate token", }) return } refreshToken := h.Sec.Token(token) _, err = h.Repo.UpdateCustomer(customer.ID, map[string]interface{}{ "Token": token, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed to store refresh token", }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "token": token, "refreshToken": refreshToken, }) return } ================================================ FILE: handler/delivery.go ================================================ package handler import ( "encoding/json" "log" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/shared" ) type CourierWithEta struct { Courier *shared.User Duration float64 } func (h *Handler) acceptDelivery(message []byte, ws *ws.WSConnection) { var data shared.AcceptDelivery err := json.Unmarshal(message, &data) if err != nil { log.Println("failed to parse message", err) return } err = h.Services.AcceptDelivery(data, ws) if err != nil { log.Println("failed to accept delivery", err) return } } func (h *Handler) processDeliveryRequest(message []byte, ws *ws.WSConnection) { var data shared.DeliveryRequest err := json.Unmarshal(message, &data) if err != nil { log.Println("failed to parse message", err) return } _, err = h.Repo.UpdateCustomer(data.CustomerID, map[string]interface{}{ "State": models.Searching, }) if err != nil { log.Println("failed to update customer", err) return } product, err := h.Repo.FindProduct(data.ProductId) if err != nil { log.Printf("failed to find product with id (%d)", data.ProductId) log.Println(err) return } if product.Name == "express" { delivery, err := h.Services.CreateDelivery(data) if err != nil { log.Println("failed to create delivery", err) return } err = h.Services.DispatchDelivery(data, delivery, ws) if err != nil { log.Println("failed to dispatch delivery", err) return } } else { } } func (h *Handler) handleDeliveryCancellation(message []byte, ws *ws.WSConnection) { var data shared.CancelDeliveryRequest err := json.Unmarshal(message, &data) if err != nil { log.Println("failed to parse message", err) return } ws.SendMessage([]byte("Delivery Cancelled")) } ================================================ FILE: handler/handler.go ================================================ package handler import ( "os" "github.com/gin-gonic/gin" "github.com/go-redis/redis" "github.com/gwuah/postmates/lib/billing" "github.com/gwuah/postmates/lib/eta" "github.com/gwuah/postmates/lib/sms" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/repository" "github.com/gwuah/postmates/services" "github.com/gwuah/postmates/utils/jwt" "github.com/gwuah/postmates/utils/secure" "gorm.io/gorm" ) type Handler struct { DB *gorm.DB Repo *repository.Repository JWT jwt.Service Sec *secure.Service Services *services.Services maxMessageTypeLength int Hub *ws.Hub RedisDB *redis.Client SMS *sms.SMS Eta *eta.Eta } func New(DB *gorm.DB, jwt jwt.Service, sec *secure.Service, redisDB *redis.Client) *Handler { SMS := sms.New(os.Getenv("TERMII_API_KEY")) eta := eta.New(os.Getenv("GMAPS_TOKEN")) billing := billing.New() hub := ws.NewHub() go hub.Run() repo := repository.New(DB, redisDB) services := services.New(repo, eta, hub, billing) return &Handler{ DB: DB, Repo: repo, JWT: jwt, Services: services, maxMessageTypeLength: 30, Hub: hub, RedisDB: redisDB, SMS: SMS, Eta: eta, Sec: sec, } } func (h *Handler) Register(v1 *gin.RouterGroup) { v1.GET("/customer/realtime/:id", h.handleConnection("customer")) v1.GET("/courier/realtime/:id", h.handleConnection("courier")) v1.POST("/get-closest-couriers", h.GetClosestCouriers) v1.POST("/get-delivery-cost", h.GetDeliveryCost) v1.POST("/customer-rate-trip", h.handleCustomerRating) v1.POST("/courier-rate-trip", h.handleCourierRating) v1.GET("/refresh/:token", h.Refresh) // middleware.JWT(h.JWT) customers := v1.Group("/customers") customers.GET("/", h.ListCustomers) customers.GET("/:id", h.ViewCustomer) customers.POST("/signup", h.SignupCustomer) customers.POST("/login", h.LoginCustomer) } ================================================ FILE: handler/order.go ================================================ package handler import ( "net/http" "github.com/gin-gonic/gin" "github.com/gwuah/postmates/utils" ) func (h *Handler) GetOrder(c *gin.Context) { id := c.Param("id") if id == "" { c.JSON(http.StatusNotFound, gin.H{ "message": "Order ID not found", }) } id64 := utils.ConvertToUint64(id) order, err := h.Repo.FindDelivery(uint(id64), true) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "failed To Retrieve Order", }) return } c.JSON(http.StatusOK, gin.H{ "message": "success", "order": order, }) return } ================================================ FILE: handler/ws.go ================================================ package handler import ( "encoding/json" "log" "net/http" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/shared" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } var MESSAGE_TYPES = map[string]string{ "DeliveryRequest": "DeliveryRequest", "CancelDelivery": "CancelDelivery", "GetEstimate": "GetEstimate", "LocationUpdate": "LocationUpdate", "AcceptDelivery": "AcceptDelivery", } func (h *Handler) handleConnection(entity string) func(c *gin.Context) { return func(c *gin.Context) { id := c.Param("id") // this is unsafe, in future we have to set a static list of accepted origins upgrader.CheckOrigin = func(r *http.Request) bool { return true } conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Println("failed to setup websocket conn ..", err) return } wsConnection := &ws.WSConnection{ Hub: h.Hub, Send: make(chan []byte), Conn: conn, Id: id, Entity: entity, ProcessMessage: h.processIncomingMessage, IsActive: true, DeliveryAcceptanceAck: make(chan bool), } h.Hub.Register <- wsConnection go wsConnection.ReadPump() go wsConnection.WritePump() } } func (h *Handler) processIncomingMessage(message []byte, ws *ws.WSConnection) { switch string(h.getTypeOfMessage(message)) { case MESSAGE_TYPES["DeliveryRequest"]: h.processDeliveryRequest(message, ws) case MESSAGE_TYPES["CancelDelivery"]: h.handleDeliveryCancellation(message, ws) case MESSAGE_TYPES["LocationUpdate"]: h.handleLocationUpdate(message, ws) case MESSAGE_TYPES["AcceptDelivery"]: h.acceptDelivery(message, ws) default: log.Printf("No handler available for request %s", h.getTypeOfMessage(message)) } } func (h *Handler) getTypeOfMessage(message []byte) []byte { // this method pre-parses the message and extracts the type of message from the payload // this is done to speedup parsing and reduce size of marshalled/unmarshalled payload // custom algorithm, ask @gwuah for explanation start := 16 end := start + h.maxMessageTypeLength + 2 payload := message[start:end] numberOfQuotesSeen := 0 head := []byte{} for i := 0; i < len(payload); i++ { value := payload[i] if numberOfQuotesSeen == 2 { break } if value == byte('"') { numberOfQuotesSeen++ } head = append(head, value) } return head[1 : len(head)-1] } func (h *Handler) handleLocationUpdate(message []byte, ws *ws.WSConnection) { var data shared.UserLocationUpdate err := json.Unmarshal(message, &data) if err != nil { log.Println("failed to parse message", err) return } err = h.Services.HandleLocationUpdate(data) if err != nil { log.Println("failed to handle location update", err) return } } ================================================ FILE: lib/billing/billing.go ================================================ package billing import ( "math" "github.com/gwuah/postmates/utils/geo" ) const ( BASE_PRICE = 5 ) type Billing struct { } func New() *Billing { return &Billing{} } func (b *Billing) GetDeliveryCost(distance float64) float64 { fare := (13 * geo.ConvertMetresToKM(distance)) / 12.5 if fare < BASE_PRICE { return BASE_PRICE } return math.Ceil(fare) } ================================================ FILE: lib/eta/eta.go ================================================ package eta import ( "context" "fmt" "log" "github.com/gwuah/postmates/shared" "googlemaps.github.io/maps" ) type Eta struct { token string gmaps *maps.Client } type DistanceFromOrigin float64 type DurationFromOrigin float64 func New(googleAPIKey string) *Eta { if googleAPIKey == "" { log.Fatal("gmaps token required") } gmapsClient, err := maps.NewClient(maps.WithAPIKey(googleAPIKey)) if err != nil { log.Fatal("failed to initialize gmaps", err) } return &Eta{ token: googleAPIKey, gmaps: gmapsClient, } } func (eta *Eta) GMAPS__distanceMatrixBase(origins []shared.Coord, destinations []shared.Coord) (*maps.DistanceMatrixResponse, error) { modifiedOrigins := []string{} modifiedDestinations := []string{} for _, coord := range origins { modifiedOrigins = append(modifiedOrigins, fmt.Sprintf("%f,%f", coord.Latitude, coord.Longitude)) } for _, coord := range destinations { modifiedDestinations = append(modifiedDestinations, fmt.Sprintf("%f,%f", coord.Latitude, coord.Longitude)) } r := &maps.DistanceMatrixRequest{ Origins: modifiedOrigins, Destinations: modifiedDestinations, } resp, err := eta.gmaps.DistanceMatrix(context.Background(), r) if err != nil { return nil, err } return resp, nil } func (eta *Eta) GMAPS__getDistanceAndDuration1to1(origin shared.Coord, destination shared.Coord) (int, float64, error) { resp, err := eta.GMAPS__distanceMatrixBase([]shared.Coord{origin}, []shared.Coord{destination}) if err != nil { return 0, 0, err } distance := resp.Rows[0].Elements[0].Distance.Meters duration := resp.Rows[0].Elements[0].Duration.Minutes() return distance, duration, nil } func (eta *Eta) GMAPS__getDistanceAndDurationManyTo1(origins []shared.Coord, destination shared.Coord) (*maps.DistanceMatrixResponse, error) { resp, err := eta.GMAPS__distanceMatrixBase(origins, []shared.Coord{destination}) if err != nil { return nil, err } return resp, nil } ================================================ FILE: lib/sms/sms.go ================================================ package sms import ( "bytes" "encoding/json" "io/ioutil" "log" "net/http" "os" ) type SMS struct { key string senderID string } type Message struct { To string `json:"to"` From string `json:"from"` Sms string `json:"sms"` Type string `json:"type"` Channel string `json:"channel"` ApiKey string `json:"api_key"` } type Response struct { MessageId string `json:"message_id"` Message string `json:"message"` Balance float64 `json:"balance"` User string `json:"user"` } const API_ENDPOINT = "https://termii.com/api/sms/send" func New(apiKey string) *SMS { if apiKey == "" { log.Fatal("termii api key required") } return &SMS{apiKey, os.Getenv("TERMII_SENDER_ID")} } func (s *SMS) SendTextMessage(msg Message) (*Response, error) { msg.ApiKey = s.key msg.Type = "plain" msg.Channel = "generic" msg.From = s.senderID body, err := json.Marshal(msg) if err != nil { return nil, err } req, err := http.NewRequest("POST", API_ENDPOINT, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() payload, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var response Response err = json.Unmarshal(payload, &response) if err != nil { return nil, err } return &response, nil } ================================================ FILE: lib/ws/connection.go ================================================ package ws import ( "fmt" "log" "time" "github.com/gorilla/websocket" ) const ( writeWait = 10 * time.Second pongWait = 60 * time.Second pingPeriod = (pongWait * 9) / 10 maxMessageSize = 1024 ) type WSConnection struct { Id string Hub *Hub Room string Conn *websocket.Conn Send chan []byte ProcessMessage func(msg []byte, ws *WSConnection) Entity string IsActive bool DeliveryAcceptanceAck chan bool } func (w *WSConnection) Deactivate() { close(w.Send) w.IsActive = false } func (w *WSConnection) ReadPump() { defer func() { log.Println("Unregistering", w.Id) w.Hub.unregister <- w w.Conn.Close() }() w.Conn.SetReadLimit(maxMessageSize) w.Conn.SetReadDeadline(time.Now().Add(pongWait)) w.Conn.SetPongHandler(func(string) error { w.Conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { _, message, err := w.Conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("error: %v", err) } break } go func() { w.ProcessMessage(message, w) }() } } func (w *WSConnection) WritePump() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() w.Conn.Close() }() for { select { case message, ok := <-w.Send: w.Conn.SetWriteDeadline(time.Now().Add(writeWait)) if !ok { // The Hub closed the channel. w.Conn.WriteMessage(websocket.CloseMessage, []byte{}) return } err := w.Conn.WriteMessage(websocket.TextMessage, message) if err != nil { log.Println("failed to Send message to client", err) } case <-ticker.C: w.Conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := w.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } } func (w *WSConnection) GetIdBasedOnType() string { if w.Entity == "courier" { return fmt.Sprintf("courier_%s", w.Id) } else { return fmt.Sprintf("customer_%s", w.Id) } } func (w *WSConnection) JoinRoom(name string) { w.Hub.joinRoomQueue <- RoomRequest{name: name, w: w} } func (w *WSConnection) LeaveRoom(name string) { w.Hub.leaveRoomQueue <- RoomRequest{name: name, w: w} } func (w *WSConnection) SendMessage(message []byte) { if w.IsActive { w.Send <- message } else { log.Println("Can't send message to closed socket conn", w.Id, w.Entity) } } func (w *WSConnection) AckDeliveryAcceptance(status bool) { if w.IsActive { w.DeliveryAcceptanceAck <- status } else { log.Println("Can't send message to closed socket conn", w.Id, w.Entity) } } ================================================ FILE: lib/ws/hub.go ================================================ package ws import ( "fmt" "sync" ) type Hub struct { customers map[string]*WSConnection couriers map[string]*WSConnection broadcast chan []byte Register chan *WSConnection unregister chan *WSConnection rooms map[string]*Room createRoomQueue chan RoomRequest joinRoomQueue chan RoomRequest leaveRoomQueue chan RoomRequest gil sync.Mutex } func NewHub() *Hub { return &Hub{ broadcast: make(chan []byte), Register: make(chan *WSConnection), unregister: make(chan *WSConnection), customers: make(map[string]*WSConnection), couriers: make(map[string]*WSConnection), rooms: make(map[string]*Room), createRoomQueue: make(chan RoomRequest), joinRoomQueue: make(chan RoomRequest), leaveRoomQueue: make(chan RoomRequest), gil: sync.Mutex{}, } } func (h *Hub) GetSize(entities string) int { h.gil.Lock() defer h.gil.Unlock() if entities == "couriers" { return len(h.couriers) } else { return len(h.customers) } } func (h *Hub) GetCourier(id string) *WSConnection { // in future, we can refactor this so every entity has their own mutex h.gil.Lock() defer h.gil.Unlock() return h.couriers[id] } func (h *Hub) GetCustomer(id uint) *WSConnection { // in future, we can refactor this so every entity has their own mutex h.gil.Lock() defer h.gil.Unlock() return h.customers[fmt.Sprintf("%d", id)] } func (h *Hub) createRoom(name string) { if _, roomExists := h.rooms[name]; roomExists { return } h.rooms[name] = NewRoom(name) } func (h *Hub) Run() { for { select { case conn := <-h.Register: h.gil.Lock() if conn.Entity == "courier" { h.couriers[conn.Id] = conn } else { h.customers[conn.Id] = conn } h.gil.Unlock() case conn := <-h.unregister: h.gil.Lock() if conn.Entity == "courier" { if _, ok := h.couriers[conn.Id]; ok { delete(h.couriers, conn.Id) conn.Deactivate() } } else { if _, ok := h.customers[conn.Id]; ok { delete(h.customers, conn.Id) conn.Deactivate() } } h.gil.Unlock() case request := <-h.createRoomQueue: h.createRoom(request.name) case request := <-h.joinRoomQueue: room := h.rooms[request.name] room.joinQueue <- request case request := <-h.leaveRoomQueue: room := h.rooms[request.name] room.leaveQueue <- request } } } ================================================ FILE: lib/ws/room.go ================================================ package ws type RoomRequest struct { w *WSConnection name string } type Room struct { name string broadcast chan []byte members map[string]*WSConnection joinQueue chan RoomRequest leaveQueue chan RoomRequest done chan bool } func NewRoom(name string) *Room { room := &Room{ name: name, broadcast: make(chan []byte), leaveQueue: make(chan RoomRequest), members: make(map[string]*WSConnection), joinQueue: make(chan RoomRequest), done: make(chan bool), } go room.run() return room } func (room *Room) run() { for { select { case request := <-room.joinQueue: room.members[request.w.GetIdBasedOnType()] = request.w case request := <-room.leaveQueue: if _, ok := room.members[request.w.GetIdBasedOnType()]; ok { delete(room.members, request.w.GetIdBasedOnType()) close(request.w.Send) } case message := <-room.broadcast: for id, client := range room.members { select { case client.Send <- message: default: close(client.Send) delete(room.members, id) } } case <-room.done: return } } } func (room *Room) close() { room.done <- true } ================================================ FILE: main.go ================================================ package main import ( "crypto/sha1" "fmt" "log" "os" "github.com/gwuah/postmates/database" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/database/postgres" "github.com/gwuah/postmates/database/redis" "github.com/gwuah/postmates/handler" "github.com/gwuah/postmates/server" "github.com/gwuah/postmates/utils/jwt" "github.com/gwuah/postmates/utils/secure" "github.com/joho/godotenv" ) func main() { ENV := os.Getenv("ENV") if ENV == "" { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file", err) } } db, err := postgres.New(&postgres.Config{ User: os.Getenv("DB_USER"), Password: os.Getenv("DB_PASS"), DBName: os.Getenv("DB_NAME"), SSLMode: os.Getenv("DB_SSLMODE"), Host: os.Getenv("DB_HOST"), Port: os.Getenv("DB_PORT"), DBurl: os.Getenv("DATABASE_URL"), }) if err != nil { log.Fatal("failed To Connect To Postgresql database", err) } err = postgres.SetupDatabase(db, &models.Customer{}, &models.Delivery{}, &models.Courier{}, &models.Order{}, &models.Vehicle{}, &models.TripPoint{}, ) if err != nil { log.Fatal("failed To Setup Tables", err) } database.RunSeeds(db, []database.SeedFn{ database.SeedProducts, database.SeedCouriers, database.SeedCustomers, database.SeedVehicles, }) sec := secure.New(1, sha1.New()) jwt, err := jwt.New("HS256", os.Getenv("JWT_SECRET"), 15, 64) if err != nil { log.Fatal(err) } redisDB := redis.New(&redis.Config{ Addr: os.Getenv("REDIS_ADDRESS"), Password: os.Getenv("REDIS_PASSWORD"), }) s := server.New() h := handler.New(db, jwt, sec, redisDB) routes := s.Group("/v1") h.Register(routes) server.Start(&s, &server.Config{ Port: fmt.Sprintf(":%s", os.Getenv("PORT")), }) } ================================================ FILE: middleware/cors.go ================================================ package middleware import "github.com/gin-gonic/gin" func CORS() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } ================================================ FILE: middleware/jwt.go ================================================ package middleware import ( "net/http" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" ) type TokenParser interface { ParseToken(string) (*jwt.Token, error) } func JWT(tokenParser TokenParser) gin.HandlerFunc { return func(c *gin.Context) { token, err := tokenParser.ParseToken(c.Request.Header.Get("Authorization")) if err != nil || !token.Valid { c.AbortWithStatus(http.StatusUnauthorized) return } claims := token.Claims.(jwt.MapClaims) phone := claims["phone"].(string) email := claims["email"].(string) c.Set("phone", phone) c.Set("email", email) c.Next() } } ================================================ FILE: plg/handy.go ================================================ package plg import ( "encoding/json" "fmt" "log" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" "gorm.io/gorm" ) func S(db *gorm.DB) { var data shared.GetDeliveryCostRequest err := json.Unmarshal([]byte( `{ "origin": { "latitude": 5.677474538991623, "longitude": -0.24460022375167725 }, "destination": { "latitude": 5.6796946725653745, "longitude": -0.2447180449962616 } }`, ), &data) if err != nil { log.Fatal("error", err) } for i := 0; i < 10; i++ { cId := uint(1) delivery := models.Delivery{ OriginLatitude: data.Origin.Latitude, OriginLongitude: data.Origin.Longitude, DestinationLatitude: data.Destination.Latitude, DestinationLongitude: data.Destination.Longitude, Notes: "Hello", CustomerID: 1, State: models.Pending, CourierID: &cId, CustomerRating: 1, ProductID: 1, } if err := db.Create(&delivery).Error; err != nil { log.Fatal("error", err) } } fmt.Println("seed complete") } func C(db *gorm.DB) { var data shared.GetDeliveryCostRequest err := json.Unmarshal([]byte( `{ "origin": { "latitude": 5.677474538991623, "longitude": -0.24460022375167725 }, "destination": { "latitude": 5.6796946725653745, "longitude": -0.2447180449962616 } }`, ), &data) if err != nil { log.Fatal("error", err) } for i := 0; i < 10; i++ { cId := uint(1) delivery := models.Delivery{ OriginLatitude: data.Origin.Latitude, OriginLongitude: data.Origin.Longitude, DestinationLatitude: data.Destination.Latitude, DestinationLongitude: data.Destination.Longitude, Notes: "Hello", CustomerID: 1, State: models.Pending, CourierID: &cId, CourierRating: 1, ProductID: 1, } if err := db.Create(&delivery).Error; err != nil { log.Fatal("error", err) } } fmt.Println("seed complete") } ================================================ FILE: repository/courier.go ================================================ package repository import ( "encoding/json" "fmt" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" "github.com/uber/h3-go" "gorm.io/gorm/clause" ) func (r *Repository) FindCourier(id uint) (*models.Courier, error) { courier := models.Courier{} if err := r.DB.Preload(clause.Associations).First(&courier, id).Error; err != nil { return nil, err } return &courier, nil } func (r *Repository) UpdateCourier(id uint, data map[string]interface{}) (*models.Courier, error) { courier := models.Courier{} if err := r.DB.Model(&courier).Where("id = ?", id).Updates(data).Error; err != nil { return nil, err } return &courier, nil } func (r *Repository) GetCourierFromRedis(id string) (*shared.User, error) { var user shared.User key := fmt.Sprintf("courier_%s", id) result, err := r.RedisDB.Get(key).Result() if err != nil { return nil, err } err = json.Unmarshal([]byte(result), &user) if err != nil { return nil, err } return &user, nil } func (r *Repository) InsertCourierIntoRedis(user *shared.User) error { stringifiedUser, err := json.Marshal(user) if err != nil { return err } key := fmt.Sprintf("courier_%s", user.Id) _, err = r.RedisDB.Set(key, stringifiedUser, 0).Result() if err != nil { return err } return nil } func (r *Repository) RemoveCourierFromIndex(index h3.H3Index, user *shared.User) error { key := fmt.Sprintf("courier_index_%d", index) _, err := r.RedisDB.LRem(key, 0, user.Id).Result() if err != nil { return err } return nil } func (r *Repository) InsertCourierIntoIndex(index h3.H3Index, user *shared.User) error { key := fmt.Sprintf("courier_index_%d", index) _, err := r.RedisDB.LPush(key, user.Id).Result() if err != nil { return err } return nil } func (r *Repository) GetCouriersInIndex(index h3.H3Index) ([]string, error) { key := fmt.Sprintf("courier_index_%d", index) couriersIds, err := r.RedisDB.LRange(key, 0, -1).Result() if err != nil { return nil, err } return couriersIds, nil } func (r *Repository) GetAllCouriers(ids []string) ([]*shared.User, error) { couriers := []*shared.User{} for _, id := range ids { courier, _ := r.GetCourierFromRedis(id) couriers = append(couriers, courier) } return couriers, nil } ================================================ FILE: repository/customer.go ================================================ package repository import ( "github.com/gwuah/postmates/database/models" ) func (r *Repository) FindCustomerByQuery(query string) (*models.Customer, error) { var customer models.Customer if err := r.DB.Where(query).First(&customer).Error; err != nil { return nil, err } return &customer, nil } func (r *Repository) FindCustomerByPhone(phone string) (*models.Customer, error) { customer := models.Customer{} err := r.DB.Where("phone = ?", phone).First(&customer).Error if err != nil { return nil, err } return &customer, nil } func (r *Repository) CreateCustomerWithPhoneAndCode(phone string, code int) (*models.Customer, error) { customer := models.Customer{Phone: phone, Code: code} if err := r.DB.Create(&customer).Error; err != nil { return nil, err } return &customer, nil } func (r *Repository) UpdateCustomer(id uint, data map[string]interface{}) (*models.Customer, error) { var customer models.Customer if err := r.DB.Model(&customer).Where("id = ?", id).Updates(data).Error; err != nil { return nil, err } return &customer, nil } ================================================ FILE: repository/delivery.go ================================================ package repository import ( "fmt" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" "gorm.io/gorm/clause" ) func (r *Repository) CreateDelivery(data shared.DeliveryRequest) (*models.Delivery, error) { delivery := models.Delivery{ OriginLatitude: data.Origin.Latitude, OriginLongitude: data.Origin.Longitude, DestinationLatitude: data.Destination.Latitude, DestinationLongitude: data.Destination.Longitude, Notes: data.Notes, ProductID: data.ProductId, CustomerID: data.CustomerID, State: models.Pending, } if err := r.DB.Create(&delivery).Error; err != nil { return nil, err } return &delivery, nil } func (r *Repository) FindDelivery(id uint, loadAssociations bool) (*models.Delivery, error) { var delivery models.Delivery if err := r.DB.First(&delivery, id).Error; err != nil { return nil, err } if loadAssociations { r.DB.Preload(clause.Associations).Find(&delivery) } return &delivery, nil } func (r *Repository) UpdateDelivery(id uint, data map[string]interface{}) (*models.Delivery, error) { var delivery models.Delivery if err := r.DB.Model(&delivery).Where("id = ?", id).Updates(data).Error; err != nil { return nil, err } return &delivery, nil } func (r *Repository) DeliveryCount(condition string) (int64, error) { var count int64 if err := r.DB.Model(&models.Delivery{}).Where(condition).Count(&count).Error; err != nil { return 0, err } return count, nil } func (r *Repository) DeliverySum(condition string, field string) (int64, error) { var count int64 response := r.DB.Model(&models.Delivery{}).Where(condition).Select(fmt.Sprintf("sum(%s)", field)) if err := response.Error; err != nil { return 0, err } response.Scan(&count) return count, nil } ================================================ FILE: repository/order.go ================================================ package repository import ( "errors" "github.com/gwuah/postmates/database/models" "gorm.io/gorm/clause" ) type CreateOrderSchema struct { Phone string `json:"phone" validate:"required"` } func (r *Repository) CreateOrder() (*models.Order, error) { order := models.Order{} if err := r.DB.Create(&order).Error; err != nil { return nil, err } return &order, nil } func (r *Repository) FindOrder(id uint) (*models.Order, error) { order := models.Order{Courier: models.Courier{}} if err := r.DB.First(&order, id).Error; err != nil { return nil, err } if order.ID == 0 { return nil, errors.New("order doesn't exist") } r.DB.Preload(clause.Associations).Find(&order) return &order, nil } func (r *Repository) UpdateOrder(id uint, data map[string]interface{}) (*models.Order, error) { order := models.Order{} if err := r.DB.Model(&order).Where("id = ?", id).Updates(data).Error; err != nil { return nil, err } return &order, nil } ================================================ FILE: repository/product.go ================================================ package repository import ( "errors" "github.com/gwuah/postmates/database/models" ) func (r *Repository) FindProduct(id uint) (*models.Product, error) { product := models.Product{} if err := r.DB.First(&product, id).Error; err != nil { return nil, err } if product.ID == 0 { return nil, errors.New("Product Doesn't Exist") } return &product, nil } func (r *Repository) FindAllProducts() ([]models.Product, error) { products := []models.Product{} if err := r.DB.Find(&products).Error; err != nil { return nil, err } return products, nil } ================================================ FILE: repository/repository.go ================================================ // This is basically our data layer. package repository import ( "github.com/go-redis/redis" "gorm.io/gorm" ) type Repository struct { DB *gorm.DB RedisDB *redis.Client } func New(db *gorm.DB, redisDB *redis.Client) *Repository { return &Repository{db, redisDB} } ================================================ FILE: repository/tripPoint.go ================================================ package repository import ( "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" ) func (r *Repository) CreateTripPoint(data shared.UserLocationUpdate) (*models.TripPoint, error) { tripPoint := models.TripPoint{ Latitude: data.Latitude, Longitude: data.Longitude, DeliveryID: data.DeliveryId, State: data.State, } if err := r.DB.Create(&tripPoint).Error; err != nil { return nil, err } return &tripPoint, nil } ================================================ FILE: server/server.go ================================================ package server import ( "log" "net/http" "os" "os/signal" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/gwuah/postmates/middleware" "github.com/gwuah/postmates/utils/validator" ) type Config struct { Port string Debug bool } type Server struct { *gin.Engine } func healthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "OK", }) } func New() Server { binding.Validator = new(validator.DefaultValidator) server := gin.Default() server.Use(middleware.CORS()) server.GET("/", healthCheck) return Server{server} } func Start(e *Server, cfg *Config) { s := &http.Server{ Addr: cfg.Port, Handler: e.Engine, } quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt) go func() { <-quit if err := s.Close(); err != nil { log.Println("failed To ShutDown Server", err) } log.Println("Shut Down Server") }() if err := s.ListenAndServe(); err != nil { if err == http.ErrServerClosed { log.Println("Server Closed After Interruption") } else { log.Println("Unexpected Server Shutdown. err:", err) } } } ================================================ FILE: services/base.go ================================================ package services import ( "errors" "fmt" "strings" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" ) type PricePerProduct struct { ProductId uint `json:"productId"` Price float64 `json:"price"` } type GetDeliveryCostResponse struct { Estimates map[uint]PricePerProduct `json:"estimate"` Distance float64 `json:"distance"` Duration float64 `json:"duration"` } func (s *Services) RateDelivery(data shared.RatingRequest) (bool, error) { if data.IsCustomerRating { delivery, err := s.repo.UpdateDelivery(data.CustomerRating.DeliveryId, map[string]interface{}{ "CustomerRating": data.CustomerRating.Rating, "CustomerRatingMessage": data.CustomerRating.Message, }) if err != nil { return false, err } delivery, err = s.repo.FindDelivery(data.CustomerRating.DeliveryId, false) if err != nil { return false, err } if delivery.State != models.Completed { return false, errors.New("delivery not completed") } condition := fmt.Sprintf("courier_id = %d AND state = %s", *delivery.CourierID, models.Completed) totalTrips, err := s.repo.DeliveryCount(condition) if err != nil { return false, err } totalRatings, err := s.repo.DeliverySum(condition, "customer_rating") if err != nil { return false, err } averageRating := totalRatings / totalTrips _, err = s.repo.UpdateCourier(*delivery.CourierID, map[string]interface{}{ "Rating": averageRating, }) if err != nil { return false, err } } else { delivery, err := s.repo.UpdateDelivery(data.CourierRating.DeliveryId, map[string]interface{}{ "CourierRating": data.CourierRating.Rating, "CourierRatingMessage": data.CourierRating.Message, }) if err != nil { return false, err } delivery, err = s.repo.FindDelivery(data.CourierRating.DeliveryId, false) if err != nil { return false, err } condition := fmt.Sprintf("customer_id = %d", delivery.CustomerID) totalTrips, err := s.repo.DeliveryCount(condition) if err != nil { return false, err } totalRatings, err := s.repo.DeliverySum(condition, "courier_rating") if err != nil { return false, err } averageRating := totalRatings / totalTrips _, err = s.repo.UpdateCustomer(delivery.CustomerID, map[string]interface{}{ "Rating": averageRating, }) if err != nil { return false, err } } return true, nil } func (s *Services) GetDeliveryCost(data shared.GetDeliveryCostRequest) (*GetDeliveryCostResponse, error) { products, err := s.repo.FindAllProducts() if err != nil { return nil, errors.New("failed to load products") } duration, distance, err := s.eta.GMAPS__getDistanceAndDuration1to1(data.Origin, data.Destination) if err != nil { return nil, err } response := GetDeliveryCostResponse{ Estimates: make(map[uint]PricePerProduct), Duration: float64(duration), Distance: float64(distance), } for _, product := range products { switch strings.ToLower(product.Name) { case "express": response.Estimates[product.ID] = PricePerProduct{ ProductId: product.ID, Price: s.billing.GetDeliveryCost(float64(distance)), } break } } return &response, nil } ================================================ FILE: services/courier.go ================================================ package services ================================================ FILE: services/delivery.go ================================================ package services import ( "encoding/json" "log" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/shared" "github.com/gwuah/postmates/utils" ) func (s *Services) CreateDelivery(data shared.DeliveryRequest) (*models.Delivery, error) { delivery, err := s.repo.CreateDelivery(data) if err != nil { return nil, err } return delivery, nil } func (s *Services) AcceptDelivery(data shared.AcceptDelivery, courierWS *ws.WSConnection) error { courierFromRedis, err := s.repo.GetCourierFromRedis(courierWS.Id) if err != nil { log.Printf("failed to retrieve courier %s from redis", courierWS.Id) return nil } delivery, err := s.repo.FindDelivery(data.DeliveryId, false) if err != nil { return err } duration, distance, err := s.eta.GMAPS__getDistanceAndDuration1to1(shared.Coord{ Latitude: courierFromRedis.Latitude, Longitude: courierFromRedis.Longitude, }, shared.Coord{ Latitude: delivery.OriginLatitude, Longitude: delivery.OriginLongitude, }) if err != nil { log.Printf("failed to get courier %s ETA", courierWS.Id) return nil } _, err = s.repo.UpdateDelivery(data.DeliveryId, map[string]interface{}{ "State": models.PendingPickup, "CourierID": courierWS.Id, }) if err != nil { return err } _, err = s.repo.UpdateCourier(uint(utils.ConvertToUint64(courierWS.Id)), map[string]interface{}{ "State": models.Dispatched, }) if err != nil { return err } go func() { courierWS.AckDeliveryAcceptance(true) }() delivery, err = s.repo.FindDelivery(data.DeliveryId, true) if err != nil { return err } courier, err := s.repo.FindCourier(*delivery.CourierID) if err != nil { return err } customer := s.hub.GetCustomer(delivery.CustomerID) if customer != nil { go func() { courier.Latitude = courierFromRedis.Latitude courier.Longitude = courierFromRedis.Longitude acceptanceDataStruct := shared.DeliveryAcceptedPayload{ Meta: shared.Meta{ Type: "DeliveryAccepted", }, Courier: *courier, Delivery: *delivery, DistanceToPickup: float64(distance), DurationToPickup: float64(duration), } acceptanceData, err := json.Marshal(acceptanceDataStruct) if err != nil { return } customer.SendMessage(acceptanceData) }() } return nil } func (s *Services) CancelDelivery(data shared.CancelDeliveryRequest) bool { return true } ================================================ FILE: services/dispatch.go ================================================ package services import ( "encoding/json" "errors" "log" "sort" "time" "github.com/go-redis/redis" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/shared" "github.com/gwuah/postmates/utils/geo" ) func (s *Services) HandleLocationUpdate(params shared.UserLocationUpdate) error { switch params.State { case models.AwaitingDispatch: _, err := s.indexCourierLocation(params) return err case models.Dispatched, models.OnTrip: _, err := s.indexCourierLocation(params) if err != nil { return err } _, err = s.repo.CreateTripPoint(params) if err != nil { return err } err = s.relayCoordsToCustomer(params) if err != nil { return err } } return nil } func (s *Services) relayCoordsToCustomer(params shared.UserLocationUpdate) error { delivery, err := s.repo.FindDelivery(params.DeliveryId, false) if err != nil { return err } redisCourier, err := s.repo.GetCourierFromRedis(string(params.Id)) if err != nil { return err } duration, distance, err := s.eta.GMAPS__getDistanceAndDuration1to1(shared.Coord{ Latitude: redisCourier.Latitude, Longitude: redisCourier.Longitude, }, shared.Coord{ Latitude: delivery.OriginLatitude, Longitude: delivery.OriginLongitude, }) if err != nil { return nil } data, err := json.Marshal(shared.CourierLocation{ Meta: shared.Meta{ Type: "CourierLocationUpdate", }, Coord: redisCourier.Coord, DistanceToPickup: float64(distance), DurationToPickup: float64(duration), }) if err != nil { return nil } if customerConn := s.hub.GetCustomer(delivery.CustomerID); customerConn != nil { customerConn.SendMessage(data) } return nil } func (s *Services) indexCourierLocation(param shared.UserLocationUpdate) (*shared.User, error) { newIndex := geo.CoordToIndex(param.Coord) courier, err := s.repo.GetCourierFromRedis(param.Id) if err == redis.Nil { courier = &shared.User{ Id: param.Id, } } if err != redis.Nil && err != nil { return nil, err } oldIndex := courier.LastKnownIndex courier.Coord = param.Coord courier.LastKnownIndex = newIndex err = s.repo.InsertCourierIntoRedis(courier) if err != nil { return nil, err } if oldIndex != newIndex { err = s.repo.RemoveCourierFromIndex(oldIndex, courier) if err != nil { return nil, err } err = s.repo.InsertCourierIntoIndex(newIndex, courier) if err != nil { return nil, err } } return courier, nil } func (s *Services) GetClosestCouriers(destination shared.Coord, steps int) ([]shared.CourierWithEta, error) { var e []shared.CourierWithEta rings := geo.GetRingsFromOrigin(destination, steps) couriersIds := []string{} for _, index := range rings { ids, err := s.repo.GetCouriersInIndex(index) if err != nil { log.Printf("failed to load couriers in courier_index %d", index) continue } if len(ids) > 0 { couriersIds = append(couriersIds, ids...) } } if len(couriersIds) == 0 { return e, errors.New("no couriers available") } couriers, err := s.repo.GetAllCouriers(couriersIds) if err != nil { return e, err } origins := []shared.Coord{} for _, courier := range couriers { origins = append(origins, shared.Coord{ Latitude: courier.Latitude, Longitude: courier.Longitude, }) } response, err := s.eta.GMAPS__getDistanceAndDurationManyTo1(origins, destination) if err != nil { return e, err } for key, dt := range response.Rows { courier := couriers[key] e = append(e, shared.CourierWithEta{ Courier: courier, Duration: dt.Elements[0].Duration.Minutes(), Distance: float64(dt.Elements[0].Distance.Meters), }) } sort.Slice(e, func(i, j int) bool { return e[i].Duration < e[j].Duration }) return e, nil } func (s *Services) DispatchDelivery(data shared.DeliveryRequest, delivery *models.Delivery, ws *ws.WSConnection) error { foundCourierForOrder := false if s.hub.GetSize("couriers") == 0 { res, err := json.Marshal(shared.NoCourierAvailable{ Meta: shared.Meta{ Type: "NoCourierAvailable", }, Message: "there are no couriers available", }) if err != nil { return err } ws.SendMessage(res) return nil } dispatchLogic: e, err := s.GetClosestCouriers(data.Origin, 2) if err != nil { return nil } delivery, err = s.repo.FindDelivery(delivery.ID, true) if err != nil { return nil } ticker := time.NewTicker(5 * time.Second) courierLoop: for _, courier := range e { conn := s.hub.GetCourier(courier.Courier.Id) if conn == nil { continue } convertedDeliveryRequest, err := json.Marshal(shared.NewDelivery{ Meta: shared.Meta{ Type: "NewDelivery", }, Delivery: delivery, DistanceToPickup: courier.Distance, DurationToPickup: courier.Duration, }) if err != nil { return nil } conn.SendMessage(convertedDeliveryRequest) select { case <-ticker.C: // move to next courier in queue case <-conn.DeliveryAcceptanceAck: // delivery has been accepted, exit ticker.Stop() foundCourierForOrder = true break courierLoop } } if !foundCourierForOrder { goto dispatchLogic } return nil } ================================================ FILE: services/order.go ================================================ package services ================================================ FILE: services/services.go ================================================ package services import ( "github.com/gwuah/postmates/lib/billing" "github.com/gwuah/postmates/lib/eta" "github.com/gwuah/postmates/lib/ws" "github.com/gwuah/postmates/repository" ) type Services struct { repo *repository.Repository eta *eta.Eta hub *ws.Hub billing *billing.Billing } func New(repo *repository.Repository, eta *eta.Eta, hub *ws.Hub, billing *billing.Billing) *Services { return &Services{repo: repo, eta: eta, hub: hub, billing: billing} } ================================================ FILE: shared/types.go ================================================ package shared import ( "errors" "github.com/gwuah/postmates/database/models" "github.com/uber/h3-go" ) var ( MAPBOX_ERROR = errors.New("Mapbox Request failed") ) type Meta struct { Type string `json:"type"` } type Coord struct { Longitude float64 `json:"longitude" validate:"required"` Latitude float64 `json:"latitude" validate:"required"` } type User struct { Id string `json:"id"` LastKnownIndex h3.H3Index `json:"lastKnownIndex"` Coord } type UserLocationUpdate struct { Id string `json:"id"` State models.State `json:"state"` DeliveryId uint `json:"deliveryId"` Coord } type DeliveryRequest struct { Meta Meta `json:"meta"` Origin Coord `json:"origin"` Destination Coord `json:"destination"` ProductId uint `json:"productId"` Notes string `json:"notes"` CustomerID uint `json:"customerId"` } type CancelDeliveryRequest struct { Meta Meta `json:"meta"` TripId uint `json:"tripId"` } type GetClosestCouriersRequest struct { Origin Coord `json:"origin"` } type NewDelivery struct { Meta Meta `json:"meta"` Delivery *models.Delivery `json:"delivery"` DistanceToPickup float64 `json:"distanceToPickup"` DurationToPickup float64 `json:"durationToPickup"` } type AcceptDelivery struct { Meta Meta `json:"meta"` DeliveryId uint `json:"deliveryId"` } type CourierWithEta struct { Courier *User `json:"courier"` Distance float64 `json:"distance"` Duration float64 `json:"duration"` } type DeliveryAcceptedPayload struct { Meta Meta `json:"meta"` Courier models.Courier `json:"courier"` Delivery models.Delivery `json:"delivery"` DistanceToPickup float64 `json:"distanceToPickup"` DurationToPickup float64 `json:"durationToPickup"` } type NoCourierAvailable struct { Meta Meta `json:"meta"` Message string `json:"message"` } type CourierLocation struct { Meta Meta `json:"meta"` Coord DistanceToPickup float64 `json:"distanceToPickup"` DurationToPickup float64 `json:"durationToPickup"` } type PricePerProduct struct { ProductId uint `json:"productId"` Price int `json:"price"` } type GetDeliveryCostRequest struct { Origin Coord `json:"origin" validate:"required"` Destination Coord `json:"destination" validate:"required"` } type BaseRating struct { DeliveryId uint `json:"deliveryId" validate:"required"` Rating int `json:"rating" validate:"required"` Message string `json:"message"` } type CustomerRatingRequest struct { BaseRating CustomerId uint `json:"customerId" validate:"required"` } type CourierRatingRequest struct { BaseRating CourierId uint `json:"courierId" validate:"required"` } type RatingRequest struct { IsCustomerRating bool CustomerRating CustomerRatingRequest CourierRating CourierRatingRequest } ================================================ FILE: utils/geo/geo.go ================================================ package geo import ( "github.com/gwuah/postmates/shared" "github.com/uber/h3-go" ) func CoordToIndex(param shared.Coord) h3.H3Index { return h3.FromGeo(h3.GeoCoord{ Latitude: param.Latitude, Longitude: param.Longitude, }, 8) } func GetRingsFromOrigin(coord shared.Coord, steps int) []h3.H3Index { return h3.KRing(CoordToIndex(coord), steps) } func ConvertMetresToKM(distance float64) float64 { return distance / 1000 } ================================================ FILE: utils/jwt/jwt.go ================================================ package jwt import ( "errors" "fmt" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/gwuah/postmates/database/models" ) var minSecretLen = 128 // New generates new JWT service necessary for auth middleware func New(algo, secret string, ttlMinutes, minSecretLength int) (Service, error) { if minSecretLength > 0 { minSecretLen = minSecretLength } if len(secret) < minSecretLen { return Service{}, fmt.Errorf("jwt secret length is %v, which is less than required %v", len(secret), minSecretLen) } signingMethod := jwt.GetSigningMethod(algo) if signingMethod == nil { return Service{}, fmt.Errorf("invalid jwt signing method: %s", algo) } return Service{ key: []byte(secret), algo: signingMethod, ttl: time.Duration(ttlMinutes) * time.Minute, }, nil } // Service provides a Json-Web-Token authentication implementation type Service struct { // Secret key used for signing. key []byte // Duration for which the jwt token is valid. ttl time.Duration // JWT signing algorithm algo jwt.SigningMethod } // ParseToken parses token from Authorization header func (s Service) ParseToken(authHeader string) (*jwt.Token, error) { parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == "Bearer") { return nil, errors.New("token not passed to authorization header") } return jwt.Parse(parts[1], func(token *jwt.Token) (interface{}, error) { if s.algo != token.Method { return nil, errors.New("failed to parse token") } return s.key, nil }) } // GenerateToken generates new JWT token and populates it with user data func (s Service) GenerateToken(customer *models.Customer) (string, error) { return jwt.NewWithClaims(s.algo, jwt.MapClaims{ "phone": customer.Phone, "exp": time.Now().Add(s.ttl).Unix(), }).SignedString(s.key) } ================================================ FILE: utils/secure/secure.go ================================================ package secure import ( "fmt" "hash" "strconv" "time" "github.com/nbutton23/zxcvbn-go" "golang.org/x/crypto/bcrypt" ) // New initializes security service func New(minPWStr int, h hash.Hash) *Service { return &Service{minPWStr: minPWStr, h: h} } // Service holds security related methods type Service struct { minPWStr int h hash.Hash } // Password checks whether password is secure enough using zxcvbn library func (s *Service) Password(pass string, inputs ...string) bool { pwStrength := zxcvbn.PasswordStrength(pass, inputs) return pwStrength.Score >= s.minPWStr } // Hash hashes the password using bcrypt func (*Service) Hash(password string) string { hashedPW, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(hashedPW) } // HashMatchesPassword matches hash with password. Returns true if hash and password match. func (*Service) HashMatchesPassword(hash, password string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil } // Token generates new unique token func (s *Service) Token(str string) string { s.h.Reset() fmt.Fprintf(s.h, "%s%s", str, strconv.Itoa(time.Now().Nanosecond())) return fmt.Sprintf("%x", s.h.Sum(nil)) } ================================================ FILE: utils/utils.go ================================================ package utils import ( "crypto/rand" "fmt" "io" "strconv" "strings" "github.com/gwuah/postmates/database/models" "github.com/gwuah/postmates/shared" ) var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} func GeneratePhoneNumber(phone string) string { return fmt.Sprintf("233%s", phone[1:]) } func GenerateOTP() string { max := 4 b := make([]byte, max) n, err := io.ReadAtLeast(rand.Reader, b, max) if n != max { panic(err) } for i := 0; i < len(b); i++ { b[i] = table[int(b[i])%len(table)] } return string(b) } func StringifyLngLat(props shared.Coord) string { return fmt.Sprintf("%f,%f", props.Longitude, props.Latitude) } func ConvertToUint64(num string) uint64 { id64, _ := strconv.ParseUint(num, 10, 64) return id64 } func ConvertToInt(num string) int { id64, _ := strconv.ParseInt(num, 10, 64) return int(id64) } func ConvertToVehicleType(id string) models.VehicleType { switch strings.ToLower(id) { case "motor": return models.Motor case "car": return models.Car default: return models.Motor } } ================================================ FILE: utils/validator/validator.go ================================================ package validator import ( "fmt" "reflect" "strings" "sync" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" ) type FieldError struct { Err validator.FieldError } func (q FieldError) String() string { var sb strings.Builder sb.WriteString("validation failed on field '" + q.Err.Field() + "'") sb.WriteString(", condition: " + q.Err.ActualTag()) // Print condition parameters, e.g. oneof=red blue -> { red blue } if q.Err.Param() != "" { sb.WriteString(" { " + q.Err.Param() + " }") } if q.Err.Value() != nil && q.Err.Value() != "" { sb.WriteString(fmt.Sprintf(", actual: %v", q.Err.Value())) } return sb.String() } // DefaultValidator ... type DefaultValidator struct { once sync.Once validate *validator.Validate } var _ binding.StructValidator = &DefaultValidator{} func (v *DefaultValidator) ValidateStruct(obj interface{}) error { if kindOfData(obj) == reflect.Struct { v.lazyinit() if err := v.validate.Struct(obj); err != nil { return err } } return nil } func (v *DefaultValidator) Engine() interface{} { v.lazyinit() return v.validate } func (v *DefaultValidator) lazyinit() { v.once.Do(func() { v.validate = validator.New() v.validate.SetTagName("validate") // add any custom validations etc. here }) } func kindOfData(data interface{}) reflect.Kind { value := reflect.ValueOf(data) valueType := value.Kind() if valueType == reflect.Ptr { valueType = value.Elem().Kind() } return valueType }