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 <br/>
We use google maps for features such as distance-matrix and directions <br/>
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
}
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
SYMBOL INDEX (214 symbols across 49 files)
FILE: database/models/courier.go
type Courier (line 3) | type Courier struct
FILE: database/models/customer.go
type Model (line 7) | type Model struct
type Customer (line 14) | type Customer struct
FILE: database/models/delivery.go
type State (line 3) | type State
constant Pending (line 7) | Pending State = "pending"
constant PendingPickup (line 8) | PendingPickup = "pending_pickup"
constant NearingPickup (line 9) | NearingPickup = "nearing_pickup"
constant AtPickup (line 10) | AtPickup = "at_pickup"
constant DeliveryOngoing (line 11) | DeliveryOngoing = "delivery_ongoing"
constant NearingDropoff (line 12) | NearingDropoff = "nearing_dropoff"
constant AtDropOff (line 13) | AtDropOff = "at_dropoff"
constant Completed (line 14) | Completed = "completed"
constant Cancelled (line 15) | Cancelled = "cancelled"
constant AwaitingDispatch (line 18) | AwaitingDispatch = "awaiting_dispatch"
constant Dispatched (line 19) | Dispatched = "dispatched"
constant OnTrip (line 20) | OnTrip = "on_trip"
constant Offline (line 21) | Offline = "offline"
constant Inactive (line 24) | Inactive = "inactive"
constant Searching (line 27) | Searching = "searching"
constant AtRest (line 28) | AtRest = "atRest"
type Delivery (line 31) | type Delivery struct
FILE: database/models/order.go
type Order (line 3) | type Order struct
FILE: database/models/product.go
type Product (line 3) | type Product struct
FILE: database/models/tripPoint.go
type TripPoint (line 3) | type TripPoint struct
FILE: database/models/vehicle.go
type VehicleType (line 3) | type VehicleType
constant Motor (line 6) | Motor VehicleType = "motor"
constant Car (line 7) | Car = "car"
type Vehicle (line 10) | type Vehicle struct
FILE: database/postgres/postgres.go
type Config (line 12) | type Config struct
function SetupDatabase (line 22) | func SetupDatabase(db *gorm.DB, models ...interface{}) error {
function New (line 27) | func New(config *Config) (*gorm.DB, error) {
FILE: database/redis/redis.go
type Config (line 10) | type Config struct
function New (line 17) | func New(config *Config) *redis.Client {
FILE: database/seeds.go
type SeedFn (line 15) | type SeedFn
function RunSeeds (line 17) | func RunSeeds(db *gorm.DB, seeds []SeedFn) {
function SeedProducts (line 28) | func SeedProducts(DB *gorm.DB, path string) {
function SeedCouriers (line 54) | func SeedCouriers(DB *gorm.DB, path string) {
function SeedCustomers (line 83) | func SeedCustomers(DB *gorm.DB, path string) {
function SeedVehicles (line 112) | func SeedVehicles(DB *gorm.DB, path string) {
FILE: demo/customer__closest_couriers.js
function connect (line 3) | function connect(id) {
function parseMessage (line 35) | function parseMessage(message) {}
function main (line 37) | function main() {
FILE: demo/customer__delivery_request.js
function connect (line 3) | function connect(id) {
function main (line 42) | function main() {
FILE: demo/electrons.js
class Courier (line 26) | class Courier {
method constructor (line 27) | constructor(id, coord) {
method _initialization (line 37) | _initialization() {
method _handleNewDelivery (line 56) | _handleNewDelivery(parsed) {
method _sendLocationUpdate (line 75) | _sendLocationUpdate() {
method handleMessage (line 96) | handleMessage(message) {
function main (line 106) | function main() {
FILE: handler/auth.go
method Refresh (line 10) | func (h *Handler) Refresh(c *gin.Context) {
FILE: handler/base.go
method handleCustomerRating (line 13) | func (h *Handler) handleCustomerRating(c *gin.Context) {
method handleCourierRating (line 43) | func (h *Handler) handleCourierRating(c *gin.Context) {
method GetDeliveryCost (line 73) | func (h *Handler) GetDeliveryCost(c *gin.Context) {
FILE: handler/courier.go
type closestCourierResponse (line 12) | type closestCourierResponse struct
method GetClosestCouriers (line 16) | func (h *Handler) GetClosestCouriers(c *gin.Context) {
FILE: handler/customer.go
type CreateCustomerRequest (line 15) | type CreateCustomerRequest struct
type LoginRequest (line 19) | type LoginRequest struct
method ListCustomers (line 24) | func (h *Handler) ListCustomers(c *gin.Context) {
method ViewCustomer (line 40) | func (h *Handler) ViewCustomer(c *gin.Context) {
method sendSMS (line 64) | func (h *Handler) sendSMS(customer models.Customer, token string) {
method SignupCustomer (line 78) | func (h *Handler) SignupCustomer(c *gin.Context) {
method LoginCustomer (line 133) | func (h *Handler) LoginCustomer(c *gin.Context) {
FILE: handler/delivery.go
type CourierWithEta (line 12) | type CourierWithEta struct
method acceptDelivery (line 17) | func (h *Handler) acceptDelivery(message []byte, ws *ws.WSConnection) {
method processDeliveryRequest (line 35) | func (h *Handler) processDeliveryRequest(message []byte, ws *ws.WSConnec...
method handleDeliveryCancellation (line 79) | func (h *Handler) handleDeliveryCancellation(message []byte, ws *ws.WSCo...
FILE: handler/handler.go
type Handler (line 19) | type Handler struct
method Register (line 55) | func (h *Handler) Register(v1 *gin.RouterGroup) {
function New (line 32) | func New(DB *gorm.DB, jwt jwt.Service, sec *secure.Service, redisDB *red...
FILE: handler/order.go
method GetOrder (line 10) | func (h *Handler) GetOrder(c *gin.Context) {
FILE: handler/ws.go
method handleConnection (line 27) | func (h *Handler) handleConnection(entity string) func(c *gin.Context) {
method processIncomingMessage (line 58) | func (h *Handler) processIncomingMessage(message []byte, ws *ws.WSConnec...
method getTypeOfMessage (line 73) | func (h *Handler) getTypeOfMessage(message []byte) []byte {
method handleLocationUpdate (line 102) | func (h *Handler) handleLocationUpdate(message []byte, ws *ws.WSConnecti...
FILE: lib/billing/billing.go
constant BASE_PRICE (line 10) | BASE_PRICE = 5
type Billing (line 13) | type Billing struct
method GetDeliveryCost (line 20) | func (b *Billing) GetDeliveryCost(distance float64) float64 {
function New (line 16) | func New() *Billing {
FILE: lib/eta/eta.go
type Eta (line 13) | type Eta struct
method GMAPS__distanceMatrixBase (line 38) | func (eta *Eta) GMAPS__distanceMatrixBase(origins []shared.Coord, dest...
method GMAPS__getDistanceAndDuration1to1 (line 66) | func (eta *Eta) GMAPS__getDistanceAndDuration1to1(origin shared.Coord,...
method GMAPS__getDistanceAndDurationManyTo1 (line 81) | func (eta *Eta) GMAPS__getDistanceAndDurationManyTo1(origins []shared....
type DistanceFromOrigin (line 18) | type DistanceFromOrigin
type DurationFromOrigin (line 19) | type DurationFromOrigin
function New (line 21) | func New(googleAPIKey string) *Eta {
FILE: lib/sms/sms.go
type SMS (line 12) | type SMS struct
method SendTextMessage (line 42) | func (s *SMS) SendTextMessage(msg Message) (*Response, error) {
type Message (line 17) | type Message struct
type Response (line 26) | type Response struct
constant API_ENDPOINT (line 33) | API_ENDPOINT = "https://termii.com/api/sms/send"
function New (line 35) | func New(apiKey string) *SMS {
FILE: lib/ws/connection.go
constant writeWait (line 12) | writeWait = 10 * time.Second
constant pongWait (line 13) | pongWait = 60 * time.Second
constant pingPeriod (line 14) | pingPeriod = (pongWait * 9) / 10
constant maxMessageSize (line 15) | maxMessageSize = 1024
type WSConnection (line 18) | type WSConnection struct
method Deactivate (line 30) | func (w *WSConnection) Deactivate() {
method ReadPump (line 35) | func (w *WSConnection) ReadPump() {
method WritePump (line 61) | func (w *WSConnection) WritePump() {
method GetIdBasedOnType (line 92) | func (w *WSConnection) GetIdBasedOnType() string {
method JoinRoom (line 100) | func (w *WSConnection) JoinRoom(name string) {
method LeaveRoom (line 104) | func (w *WSConnection) LeaveRoom(name string) {
method SendMessage (line 108) | func (w *WSConnection) SendMessage(message []byte) {
method AckDeliveryAcceptance (line 116) | func (w *WSConnection) AckDeliveryAcceptance(status bool) {
FILE: lib/ws/hub.go
type Hub (line 8) | type Hub struct
method GetSize (line 39) | func (h *Hub) GetSize(entities string) int {
method GetCourier (line 50) | func (h *Hub) GetCourier(id string) *WSConnection {
method GetCustomer (line 57) | func (h *Hub) GetCustomer(id uint) *WSConnection {
method createRoom (line 64) | func (h *Hub) createRoom(name string) {
method Run (line 72) | func (h *Hub) Run() {
function NewHub (line 21) | func NewHub() *Hub {
FILE: lib/ws/room.go
type RoomRequest (line 3) | type RoomRequest struct
type Room (line 8) | type Room struct
method run (line 32) | func (room *Room) run() {
method close (line 59) | func (room *Room) close() {
function NewRoom (line 17) | func NewRoom(name string) *Room {
FILE: main.go
function main (line 20) | func main() {
FILE: middleware/cors.go
function CORS (line 5) | func CORS() gin.HandlerFunc {
FILE: middleware/jwt.go
type TokenParser (line 10) | type TokenParser interface
function JWT (line 14) | func JWT(tokenParser TokenParser) gin.HandlerFunc {
FILE: plg/handy.go
function S (line 13) | func S(db *gorm.DB) {
function C (line 57) | func C(db *gorm.DB) {
FILE: repository/courier.go
method FindCourier (line 13) | func (r *Repository) FindCourier(id uint) (*models.Courier, error) {
method UpdateCourier (line 23) | func (r *Repository) UpdateCourier(id uint, data map[string]interface{})...
method GetCourierFromRedis (line 33) | func (r *Repository) GetCourierFromRedis(id string) (*shared.User, error) {
method InsertCourierIntoRedis (line 53) | func (r *Repository) InsertCourierIntoRedis(user *shared.User) error {
method RemoveCourierFromIndex (line 69) | func (r *Repository) RemoveCourierFromIndex(index h3.H3Index, user *shar...
method InsertCourierIntoIndex (line 79) | func (r *Repository) InsertCourierIntoIndex(index h3.H3Index, user *shar...
method GetCouriersInIndex (line 89) | func (r *Repository) GetCouriersInIndex(index h3.H3Index) ([]string, err...
method GetAllCouriers (line 99) | func (r *Repository) GetAllCouriers(ids []string) ([]*shared.User, error) {
FILE: repository/customer.go
method FindCustomerByQuery (line 7) | func (r *Repository) FindCustomerByQuery(query string) (*models.Customer...
method FindCustomerByPhone (line 18) | func (r *Repository) FindCustomerByPhone(phone string) (*models.Customer...
method CreateCustomerWithPhoneAndCode (line 30) | func (r *Repository) CreateCustomerWithPhoneAndCode(phone string, code i...
method UpdateCustomer (line 41) | func (r *Repository) UpdateCustomer(id uint, data map[string]interface{}...
FILE: repository/delivery.go
method CreateDelivery (line 11) | func (r *Repository) CreateDelivery(data shared.DeliveryRequest) (*model...
method FindDelivery (line 30) | func (r *Repository) FindDelivery(id uint, loadAssociations bool) (*mode...
method UpdateDelivery (line 45) | func (r *Repository) UpdateDelivery(id uint, data map[string]interface{}...
method DeliveryCount (line 55) | func (r *Repository) DeliveryCount(condition string) (int64, error) {
method DeliverySum (line 63) | func (r *Repository) DeliverySum(condition string, field string) (int64,...
FILE: repository/order.go
type CreateOrderSchema (line 10) | type CreateOrderSchema struct
method CreateOrder (line 14) | func (r *Repository) CreateOrder() (*models.Order, error) {
method FindOrder (line 25) | func (r *Repository) FindOrder(id uint) (*models.Order, error) {
method UpdateOrder (line 42) | func (r *Repository) UpdateOrder(id uint, data map[string]interface{}) (...
FILE: repository/product.go
method FindProduct (line 9) | func (r *Repository) FindProduct(id uint) (*models.Product, error) {
method FindAllProducts (line 24) | func (r *Repository) FindAllProducts() ([]models.Product, error) {
FILE: repository/repository.go
type Repository (line 9) | type Repository struct
function New (line 14) | func New(db *gorm.DB, redisDB *redis.Client) *Repository {
FILE: repository/tripPoint.go
method CreateTripPoint (line 8) | func (r *Repository) CreateTripPoint(data shared.UserLocationUpdate) (*m...
FILE: server/server.go
type Config (line 15) | type Config struct
type Server (line 20) | type Server struct
function healthCheck (line 24) | func healthCheck(c *gin.Context) {
function New (line 30) | func New() Server {
function Start (line 38) | func Start(e *Server, cfg *Config) {
FILE: services/base.go
type PricePerProduct (line 12) | type PricePerProduct struct
type GetDeliveryCostResponse (line 17) | type GetDeliveryCostResponse struct
method RateDelivery (line 23) | func (s *Services) RateDelivery(data shared.RatingRequest) (bool, error) {
method GetDeliveryCost (line 108) | func (s *Services) GetDeliveryCost(data shared.GetDeliveryCostRequest) (...
FILE: services/delivery.go
method CreateDelivery (line 13) | func (s *Services) CreateDelivery(data shared.DeliveryRequest) (*models....
method AcceptDelivery (line 21) | func (s *Services) AcceptDelivery(data shared.AcceptDelivery, courierWS ...
method CancelDelivery (line 101) | func (s *Services) CancelDelivery(data shared.CancelDeliveryRequest) bool {
FILE: services/dispatch.go
method HandleLocationUpdate (line 17) | func (s *Services) HandleLocationUpdate(params shared.UserLocationUpdate...
method relayCoordsToCustomer (line 41) | func (s *Services) relayCoordsToCustomer(params shared.UserLocationUpdat...
method indexCourierLocation (line 84) | func (s *Services) indexCourierLocation(param shared.UserLocationUpdate)...
method GetClosestCouriers (line 126) | func (s *Services) GetClosestCouriers(destination shared.Coord, steps in...
method DispatchDelivery (line 188) | func (s *Services) DispatchDelivery(data shared.DeliveryRequest, deliver...
FILE: services/services.go
type Services (line 10) | type Services struct
function New (line 17) | func New(repo *repository.Repository, eta *eta.Eta, hub *ws.Hub, billing...
FILE: shared/types.go
type Meta (line 14) | type Meta struct
type Coord (line 18) | type Coord struct
type User (line 23) | type User struct
type UserLocationUpdate (line 29) | type UserLocationUpdate struct
type DeliveryRequest (line 36) | type DeliveryRequest struct
type CancelDeliveryRequest (line 45) | type CancelDeliveryRequest struct
type GetClosestCouriersRequest (line 50) | type GetClosestCouriersRequest struct
type NewDelivery (line 54) | type NewDelivery struct
type AcceptDelivery (line 61) | type AcceptDelivery struct
type CourierWithEta (line 66) | type CourierWithEta struct
type DeliveryAcceptedPayload (line 72) | type DeliveryAcceptedPayload struct
type NoCourierAvailable (line 80) | type NoCourierAvailable struct
type CourierLocation (line 85) | type CourierLocation struct
type PricePerProduct (line 92) | type PricePerProduct struct
type GetDeliveryCostRequest (line 97) | type GetDeliveryCostRequest struct
type BaseRating (line 102) | type BaseRating struct
type CustomerRatingRequest (line 108) | type CustomerRatingRequest struct
type CourierRatingRequest (line 113) | type CourierRatingRequest struct
type RatingRequest (line 118) | type RatingRequest struct
FILE: utils/geo/geo.go
function CoordToIndex (line 8) | func CoordToIndex(param shared.Coord) h3.H3Index {
function GetRingsFromOrigin (line 15) | func GetRingsFromOrigin(coord shared.Coord, steps int) []h3.H3Index {
function ConvertMetresToKM (line 19) | func ConvertMetresToKM(distance float64) float64 {
FILE: utils/jwt/jwt.go
function New (line 16) | func New(algo, secret string, ttlMinutes, minSecretLength int) (Service,...
type Service (line 36) | type Service struct
method ParseToken (line 48) | func (s Service) ParseToken(authHeader string) (*jwt.Token, error) {
method GenerateToken (line 64) | func (s Service) GenerateToken(customer *models.Customer) (string, err...
FILE: utils/secure/secure.go
function New (line 14) | func New(minPWStr int, h hash.Hash) *Service {
type Service (line 19) | type Service struct
method Password (line 25) | func (s *Service) Password(pass string, inputs ...string) bool {
method Hash (line 31) | func (*Service) Hash(password string) string {
method HashMatchesPassword (line 37) | func (*Service) HashMatchesPassword(hash, password string) bool {
method Token (line 42) | func (s *Service) Token(str string) string {
FILE: utils/utils.go
function GeneratePhoneNumber (line 16) | func GeneratePhoneNumber(phone string) string {
function GenerateOTP (line 20) | func GenerateOTP() string {
function StringifyLngLat (line 33) | func StringifyLngLat(props shared.Coord) string {
function ConvertToUint64 (line 37) | func ConvertToUint64(num string) uint64 {
function ConvertToInt (line 42) | func ConvertToInt(num string) int {
function ConvertToVehicleType (line 47) | func ConvertToVehicleType(id string) models.VehicleType {
FILE: utils/validator/validator.go
type FieldError (line 13) | type FieldError struct
method String (line 17) | func (q FieldError) String() string {
type DefaultValidator (line 36) | type DefaultValidator struct
method ValidateStruct (line 43) | func (v *DefaultValidator) ValidateStruct(obj interface{}) error {
method Engine (line 57) | func (v *DefaultValidator) Engine() interface{} {
method lazyinit (line 62) | func (v *DefaultValidator) lazyinit() {
function kindOfData (line 71) | func kindOfData(data interface{}) reflect.Kind {
Condensed preview — 65 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".dockerignore",
"chars": 58,
"preview": "Dockerfile\ndocker-compose.yml\n./postmates\n./postmates-app\n"
},
{
"path": ".gitignore",
"chars": 23,
"preview": ".env\nnode_modules/\nmain"
},
{
"path": "API_DOCS.md",
"chars": 1283,
"preview": "# Docs\n\n### Courier can initiate connection @\n\n`/courier/realtime/:id`\n\n### Customer can initiate connection @\n\n`/custom"
},
{
"path": "Dockerfile",
"chars": 490,
"preview": "FROM golang:1.14-alpine AS main-env\n# install gcc for uber/h3-go. see https://github.com/uber/h3/issues/354\nRUN apk add "
},
{
"path": "Dockerfile.wait",
"chars": 216,
"preview": "FROM alpine\n\n\n# Add docker-compose-wait tool -------------------\nENV WAIT_VERSION 2.7.3\nADD https://github.com/ufoscout/"
},
{
"path": "README.md",
"chars": 2377,
"preview": "# Postmates\n\nThis is the heart of a delivery service. Features include geo-indexing, order-dispatch, proximity-searching"
},
{
"path": "database/couriers.yml",
"chars": 40,
"preview": "- Griffith Awuah\n- Andy Osei\n- Yaw Manu\n"
},
{
"path": "database/customers.yml",
"chars": 14,
"preview": "- Alicia Keys\n"
},
{
"path": "database/models/courier.go",
"chars": 444,
"preview": "package models\n\ntype Courier struct {\n\tModel\n\tFirstName string `json:\"firstName\"`\n\tLastName string `json:\"lastNam"
},
{
"path": "database/models/customer.go",
"chars": 708,
"preview": "package models\n\nimport (\n\t\"time\"\n)\n\ntype Model struct {\n\tID uint `gorm:\"primary_key\" json:\"id\"`\n\tCreatedAt "
},
{
"path": "database/models/delivery.go",
"chars": 1910,
"preview": "package models\n\ntype State string\n\nconst (\n\t// delivery state types\n\tPending State = \"pending\"\n\tPendingPickup "
},
{
"path": "database/models/order.go",
"chars": 237,
"preview": "package models\n\ntype Order struct {\n\tModel\n\t// Deliveries []Delivery `json:\"deliveries\"`\n\tCourierID uint `json:\"couri"
},
{
"path": "database/models/product.go",
"chars": 74,
"preview": "package models\n\ntype Product struct {\n\tModel\n\tName string `json:\"name\"`\n}\n"
},
{
"path": "database/models/tripPoint.go",
"chars": 249,
"preview": "package models\n\ntype TripPoint struct {\n\tModel\n\tDeliveryID uint `json:\"deliveryId\"`\n\tDelivery *Delivery `json:\"de"
},
{
"path": "database/models/vehicle.go",
"chars": 429,
"preview": "package models\n\ntype VehicleType string\n\nconst (\n\tMotor VehicleType = \"motor\"\n\tCar = \"car\"\n)\n\ntype Vehicle"
},
{
"path": "database/postgres/postgres.go",
"chars": 870,
"preview": "package postgres\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"gorm.io/driver/postgres\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype Config struct {\n\tHost s"
},
{
"path": "database/products.yml",
"chars": 17,
"preview": "- Express\n- Pool\n"
},
{
"path": "database/redis/redis.go",
"chars": 586,
"preview": "package redis\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/go-redis/redis\"\n)\n\ntype Config struct {\n\tAddr string\n\tPasswor"
},
{
"path": "database/seeds.go",
"chars": 3897,
"preview": "package database\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gw"
},
{
"path": "database/vehicles.yml",
"chars": 285,
"preview": "- data:\n - vehicleModel: yahama\n regNumber: GX 4888-10\n type: motor\n courierId: 1\n - vehicleModel: "
},
{
"path": "demo/customer__closest_couriers.js",
"chars": 816,
"preview": "const WebSocket = require(\"ws\");\n\nfunction connect(id) {\n console.log(`Customer ${id} initating a connection ... `);\n "
},
{
"path": "demo/customer__delivery_request.js",
"chars": 1014,
"preview": "const WebSocket = require(\"ws\");\n\nfunction connect(id) {\n console.log(`Customer ${id} initating a connection ... `);\n "
},
{
"path": "demo/electrons.js",
"chars": 2515,
"preview": "const WebSocket = require(\"ws\");\n\nconst outsideScope = {\n latitude: 5.698188535023582,\n longitude: -0.239341780857103,"
},
{
"path": "demo/package.json",
"chars": 244,
"preview": "{\n \"name\": \"demo\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"test\": \"echo \\\""
},
{
"path": "docker-compose.yml",
"chars": 1006,
"preview": "version: '3.7'\nservices:\n postgres:\n image: \"postgres:13.1\"\n hostname: postgres\n container_name: postmates-pos"
},
{
"path": "go.mod",
"chars": 943,
"preview": "module github.com/gwuah/postmates\n\ngo 1.14\n\n// +heroku goVersion go1.14.2\n\nrequire (\n\tgithub.com/dgrijalva/jwt-go v3.2.0"
},
{
"path": "go.sum",
"chars": 33038,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go."
},
{
"path": "handler/auth.go",
"chars": 785,
"preview": "package handler\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gwuah/postmates/database/models\"\n)\n\nfunc"
},
{
"path": "handler/base.go",
"chars": 2032,
"preview": "package handler\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"gi"
},
{
"path": "handler/courier.go",
"chars": 910,
"preview": "package handler\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.co"
},
{
"path": "handler/customer.go",
"chars": 3713,
"preview": "package handler\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gwuah/postmates/database/m"
},
{
"path": "handler/delivery.go",
"chars": 1786,
"preview": "package handler\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/post"
},
{
"path": "handler/handler.go",
"chars": 2114,
"preview": "package handler\n\nimport (\n\t\"os\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-redis/redis\"\n\t\"github.com/gwuah/postmates/l"
},
{
"path": "handler/order.go",
"chars": 574,
"preview": "package handler\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gwuah/postmates/utils\"\n)\n\nfunc (h *Handl"
},
{
"path": "handler/ws.go",
"chars": 2919,
"preview": "package handler\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gorilla/websocke"
},
{
"path": "lib/billing/billing.go",
"chars": 364,
"preview": "package billing\n\nimport (\n\t\"math\"\n\n\t\"github.com/gwuah/postmates/utils/geo\"\n)\n\nconst (\n\tBASE_PRICE = 5\n)\n\ntype Billing st"
},
{
"path": "lib/eta/eta.go",
"chars": 1962,
"preview": "package eta\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/gwuah/postmates/shared\"\n\n\t\"googlemaps.github.io/maps\"\n)\n\nty"
},
{
"path": "lib/sms/sms.go",
"chars": 1411,
"preview": "package sms\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n)\n\ntype SMS struct {\n\tkey str"
},
{
"path": "lib/ws/connection.go",
"chars": 2695,
"preview": "package ws\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\nconst (\n\twriteWait = 10 * time.Secon"
},
{
"path": "lib/ws/hub.go",
"chars": 2381,
"preview": "package ws\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n)\n\ntype Hub struct {\n\tcustomers map[string]*WSConnection\n\tcouriers map["
},
{
"path": "lib/ws/room.go",
"chars": 1166,
"preview": "package ws\n\ntype RoomRequest struct {\n\tw *WSConnection\n\tname string\n}\n\ntype Room struct {\n\tname string\n\tbroadca"
},
{
"path": "main.go",
"chars": 1788,
"preview": "package main\n\nimport (\n\t\"crypto/sha1\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/gwuah/postmates/database\"\n\t\"github.com/gwuah/pos"
},
{
"path": "middleware/cors.go",
"chars": 605,
"preview": "package middleware\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc CORS() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc."
},
{
"path": "middleware/jwt.go",
"chars": 611,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/dgrijalva/jwt-go\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype TokenParser"
},
{
"path": "plg/handy.go",
"chars": 2051,
"preview": "package plg\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/p"
},
{
"path": "repository/courier.go",
"chars": 2276,
"preview": "package repository\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/p"
},
{
"path": "repository/customer.go",
"chars": 1080,
"preview": "package repository\n\nimport (\n\t\"github.com/gwuah/postmates/database/models\"\n)\n\nfunc (r *Repository) FindCustomerByQuery(q"
},
{
"path": "repository/delivery.go",
"chars": 1830,
"preview": "package repository\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/postmates/shared\"\n"
},
{
"path": "repository/order.go",
"chars": 966,
"preview": "package repository\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"gorm.io/gorm/clause\"\n)\n\ntype Cre"
},
{
"path": "repository/product.go",
"chars": 567,
"preview": "package repository\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n)\n\nfunc (r *Repository) FindProduc"
},
{
"path": "repository/repository.go",
"chars": 277,
"preview": "// This is basically our data layer.\npackage repository\n\nimport (\n\t\"github.com/go-redis/redis\"\n\t\"gorm.io/gorm\"\n)\n\ntype R"
},
{
"path": "repository/tripPoint.go",
"chars": 468,
"preview": "package repository\n\nimport (\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/postmates/shared\"\n)\n\nfunc "
},
{
"path": "server/server.go",
"chars": 1107,
"preview": "package server\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gin-gonic/gin/b"
},
{
"path": "services/base.go",
"chars": 3244,
"preview": "package services\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah"
},
{
"path": "services/courier.go",
"chars": 17,
"preview": "package services\n"
},
{
"path": "services/delivery.go",
"chars": 2431,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/gwuah/pos"
},
{
"path": "services/dispatch.go",
"chars": 5188,
"preview": "package services\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis\"\n\t\"github.com/"
},
{
"path": "services/order.go",
"chars": 17,
"preview": "package services\n"
},
{
"path": "services/services.go",
"chars": 480,
"preview": "package services\n\nimport (\n\t\"github.com/gwuah/postmates/lib/billing\"\n\t\"github.com/gwuah/postmates/lib/eta\"\n\t\"github.com/"
},
{
"path": "shared/types.go",
"chars": 2917,
"preview": "package shared\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gwuah/postmates/database/models\"\n\t\"github.com/uber/h3-go\"\n)\n\nvar (\n\tMAP"
},
{
"path": "utils/geo/geo.go",
"chars": 434,
"preview": "package geo\n\nimport (\n\t\"github.com/gwuah/postmates/shared\"\n\t\"github.com/uber/h3-go\"\n)\n\nfunc CoordToIndex(param shared.Co"
},
{
"path": "utils/jwt/jwt.go",
"chars": 1818,
"preview": "package jwt\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dgrijalva/jwt-go\"\n\t\"github.com/gwuah/postmates/d"
},
{
"path": "utils/secure/secure.go",
"chars": 1233,
"preview": "package secure\n\nimport (\n\t\"fmt\"\n\t\"hash\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/nbutton23/zxcvbn-go\"\n\t\"golang.org/x/crypto/bcry"
},
{
"path": "utils/utils.go",
"chars": 1067,
"preview": "package utils\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gwuah/postmates/database/models\""
},
{
"path": "utils/validator/validator.go",
"chars": 1504,
"preview": "package validator\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gin-gonic/gin/binding\"\n\t\"github.com/go-pl"
}
]
About this extraction
This page contains the full source code of the gwuah/postmates GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 65 files (109.9 KB), approximately 41.4k tokens, and a symbol index with 214 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.