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
}