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