├── README.md
├── client
├── .gitignore
├── README.md
├── components.json
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ └── vite.svg
├── src
│ ├── @types
│ │ ├── MultiPolygon.ts
│ │ ├── Partner.ts
│ │ ├── Point.ts
│ │ ├── Polygon.ts
│ │ └── Response.ts
│ ├── App.tsx
│ ├── api
│ │ ├── axios.ts
│ │ └── partner-service.ts
│ ├── assets
│ │ ├── img
│ │ │ ├── store.png
│ │ │ └── store.png:Zone.Identifier
│ │ └── react.svg
│ ├── components
│ │ ├── Map
│ │ │ └── index.tsx
│ │ └── MapPartner
│ │ │ └── index.tsx
│ ├── index.css
│ ├── lib
│ │ └── utils.ts
│ ├── main.tsx
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
└── server
├── .env
├── .env.example
├── .gitignore
├── docker-compose.yml
├── package-lock.json
├── package.json
├── src
├── app.ts
├── config
│ └── config.ts
├── database
│ ├── mongo.ts
│ └── redis.ts
├── index.ts
├── modules
│ └── partners
│ │ ├── @types
│ │ ├── CreatePartner.ts
│ │ ├── LatLong.ts
│ │ ├── MultiPolygon.ts
│ │ ├── Point.ts
│ │ └── Polygon.ts
│ │ ├── PartnerModel.ts
│ │ ├── controller.ts
│ │ ├── router.ts
│ │ ├── service.ts
│ │ ├── test
│ │ └── partner.test.ts
│ │ └── validators
│ │ ├── create-partner-validator.ts
│ │ └── point-validator.ts
└── scripts
│ └── populate-database.ts
└── tsconfig.json
/README.md:
--------------------------------------------------------------------------------
1 | # Zé Delivery Challenge
2 |
3 |
4 |
5 | ## Technologies:
6 |
7 | ### Backend
8 | - Node: v20.14.0
9 | - MongoDB No relational DB
10 | - KoaJS Server framework
11 | - Turf.js Library for GeoJSON manipulation
12 | - Docker & Docker compose for containerization
13 |
14 | ### Frontend
15 | - Node: v20.14.0
16 | - React
17 | - Leaflet/React Leaflet: Library for coordinates visualization
18 |
19 | ## How to install
20 |
21 | ### Backend
22 |
23 | - Navigate to server folder
24 | ```
25 | cd server
26 | ```
27 |
28 | - Install dependencies
29 | ```
30 | npm install
31 | ```
32 |
33 | - Run container
34 | ```
35 | npm run compose:up
36 | ```
37 |
38 | - Populate database (Optional)
39 | ```
40 | npm run populate
41 | ```
42 |
43 | - Run server
44 | ```
45 | npm run dev
46 | ```
47 |
48 | ### Frontend
49 | - Navigate to client folder
50 | ```
51 | cd client
52 | ```
53 |
54 | - Install dependencies
55 | ```
56 | npm install
57 | ```
58 |
59 | - Run client
60 | ```
61 | npm run dev
62 | ```
63 |
64 | ## Routes
65 |
66 | - POST /partner
67 | ```
68 | {
69 | "coverageArea": {
70 | "type": "MultiPolygon",
71 | "coordinates": [
72 | [
73 | [
74 | [30,20],
75 | [45,40],
76 | [10,40],
77 | [30,20]
78 | ]
79 | ],
80 | [
81 | [
82 | [15,5],
83 | [40,10],
84 | [10,20],
85 | [5,10],
86 | [15,5]
87 | ]
88 | ]
89 | ]
90 | },
91 | "address": {
92 | "type": "Point",
93 | "coordinates": [
94 | -46.57421,
95 | -21.785741
96 | ]
97 | },
98 | "tradingName": "Adega da Cerveja - Pinheiros",
99 | "ownerName": "Zé da Silva",
100 | "document": "1432132123891/0001",
101 | },
102 | ```
103 |
104 | - GET /partner
105 |
106 | - GET /nearest?lat={x}&long={y}
107 |
108 | - GET /partner:id
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/client/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/client/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@turf/turf": "^7.2.0",
14 | "axios": "^1.7.9",
15 | "class-variance-authority": "^0.7.1",
16 | "clsx": "^2.1.1",
17 | "leaflet": "^1.9.4",
18 | "lucide-react": "^0.474.0",
19 | "react": "^18.3.1",
20 | "react-dom": "^18.3.1",
21 | "react-leaflet": "^4.2.1",
22 | "tailwind-merge": "^3.0.1",
23 | "tailwindcss-animate": "^1.0.7"
24 | },
25 | "devDependencies": {
26 | "@eslint/js": "^9.17.0",
27 | "@types/leaflet": "^1.9.16",
28 | "@types/node": "^22.12.0",
29 | "@types/react": "^18.3.18",
30 | "@types/react-dom": "^18.3.5",
31 | "@vitejs/plugin-react": "^4.3.4",
32 | "autoprefixer": "^10.4.20",
33 | "eslint": "^9.17.0",
34 | "eslint-plugin-react-hooks": "^5.0.0",
35 | "eslint-plugin-react-refresh": "^0.4.16",
36 | "globals": "^15.14.0",
37 | "postcss": "^8.5.1",
38 | "tailwindcss": "^3.4.15",
39 | "typescript": "~5.6.2",
40 | "typescript-eslint": "^8.18.2",
41 | "vite": "^6.0.5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/@types/MultiPolygon.ts:
--------------------------------------------------------------------------------
1 | import { Polygon } from "./Polygon"
2 | export type MultiPolygon = Polygon[]
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/client/src/@types/Partner.ts:
--------------------------------------------------------------------------------
1 | import { MultiPolygon } from "./MultiPolygon"
2 | import { Point } from "./Point"
3 |
4 | export type Partner = {
5 | _id: string,
6 | tradingName: string,
7 | ownerName: string,
8 | document: string,
9 | coverageArea: {
10 | type: "MultiPolygon",
11 | coordinates: MultiPolygon
12 | },
13 | address: {
14 | type: "Point",
15 | coordinates: Point
16 | }
17 | }
--------------------------------------------------------------------------------
/client/src/@types/Point.ts:
--------------------------------------------------------------------------------
1 | export type Point = [number, number]
--------------------------------------------------------------------------------
/client/src/@types/Polygon.ts:
--------------------------------------------------------------------------------
1 | import { Point } from "./Point"
2 |
3 | export type Polygon = Point[][]
4 |
5 |
--------------------------------------------------------------------------------
/client/src/@types/Response.ts:
--------------------------------------------------------------------------------
1 | export type Response = {
2 | data: T,
3 | message: string
4 | }
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { Map } from "./components/Map"
3 | import { Partner } from "./@types/Partner"
4 | import { partnerService } from "./api/partner-service";
5 |
6 | function App() {
7 |
8 | const [partners, setPartners] = useState([]);
9 |
10 | useEffect(() => {
11 | partnerService.findAllPartners()
12 | .then(res => setPartners(res))
13 | }, [])
14 |
15 | return (
16 |
21 | )
22 | }
23 |
24 | export default App
25 |
--------------------------------------------------------------------------------
/client/src/api/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export const api = axios.create({
4 | baseURL: "http://localhost:3001",
5 | withCredentials: false
6 | });
7 |
--------------------------------------------------------------------------------
/client/src/api/partner-service.ts:
--------------------------------------------------------------------------------
1 | import { Partner } from "@/@types/Partner";
2 | import { api } from "./axios";
3 | import { Response } from "@/@types/Response";
4 | import * as turf from "@turf/turf";
5 | import { Point } from "@/@types/Point";
6 |
7 | async function findAllPartners() {
8 | const response = await api.get>("/partner");
9 |
10 | const partners = response.data.data;
11 |
12 | partners.forEach(partner => {
13 | turf.coordEach(
14 | turf.multiPolygon(partner.coverageArea.coordinates),
15 | (coord) => [coord[0], coord[1]] = [coord[1], coord[0]])
16 | })
17 |
18 |
19 | return partners.map(partner => ({
20 | ...partner,
21 | address: {
22 | ...partner.address,
23 | coordinates: [partner.address.coordinates[1], partner.address.coordinates[0]] as Point,
24 | }
25 |
26 | }))
27 |
28 | }
29 |
30 | export const partnerService = {
31 | findAllPartners
32 | }
--------------------------------------------------------------------------------
/client/src/assets/img/store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaviGGA/ze-backend-challenge/34242ad130a2837087c9f0f1b1a6a4fd63d4f42a/client/src/assets/img/store.png
--------------------------------------------------------------------------------
/client/src/assets/img/store.png:Zone.Identifier:
--------------------------------------------------------------------------------
1 | [ZoneTransfer]
2 | ZoneId=3
3 | HostUrl=https://www.flaticon.com/
4 |
--------------------------------------------------------------------------------
/client/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/components/Map/index.tsx:
--------------------------------------------------------------------------------
1 | import { Partner } from "@/@types/Partner";
2 | import { MapContainer, TileLayer, useMapEvent } from "react-leaflet";
3 | import { MapPartner } from "../MapPartner";
4 | import { Point } from "@/@types/Point";
5 | import { useState } from "react";
6 | import { LatLng } from "leaflet";
7 |
8 |
9 | type Props = {
10 | partners: Partner[]
11 | }
12 |
13 | const CENTER_OF_BRAZIL: Point = [-14, -53];
14 |
15 | const colors = [
16 | {color: "yellow"},
17 | {color: "green"},
18 | {color: "orange"}
19 | ];
20 |
21 | export function Map({partners}: Props) {
22 |
23 | const [latLong, setLatLong] = useState<{lat: number, long: number}>({lat: 0, long: 0});
24 |
25 | function MouseTracker() {
26 | useMapEvent("mousemove", e =>
27 | setLatLong({lat: e.latlng.lat, long: e.latlng.lng})
28 | )
29 | return null
30 | }
31 |
32 |
33 |
34 | return (
35 |
36 |
Cursor position - Lat: {latLong.lat.toFixed(2)} Long: {latLong.long.toFixed(2)}
37 |
38 |
42 |
43 | {
44 | partners.map((partner, idx) =>
45 |
50 | )
51 | }
52 |
53 |
54 | )
55 | }
--------------------------------------------------------------------------------
/client/src/components/MapPartner/index.tsx:
--------------------------------------------------------------------------------
1 | import { Marker, Polygon, Popup } from "react-leaflet";
2 | import storeIcon from "@/assets/img/store.png";
3 | import L from "leaflet";
4 | import { Partner } from "@/@types/Partner";
5 |
6 | const customIcon = L.icon({
7 | iconUrl: storeIcon,
8 | iconSize: [32, 32],
9 | iconAnchor: [12, 41],
10 | popupAnchor: [0, -41],
11 | })
12 |
13 | type Props = {
14 | partner: Partner,
15 | color: {color: string}
16 | }
17 |
18 | export function MapPartner({partner, color}: Props) {
19 |
20 | return (
21 | <>
22 |
23 |
24 | Estabelecimento: {partner.tradingName}
25 |
26 | Dono: {partner.ownerName}
27 |
28 |
29 |
30 | >
31 | )
32 | }
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | #root {
6 | width: 100%;
7 | }
8 |
9 | :root {
10 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
11 | line-height: 1.5;
12 | font-weight: 400;
13 |
14 | color-scheme: light dark;
15 | color: rgba(255, 255, 255, 0.87);
16 | background-color: #242424;
17 |
18 | font-synthesis: none;
19 | text-rendering: optimizeLegibility;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | }
23 |
24 | a {
25 | font-weight: 500;
26 | color: #646cff;
27 | text-decoration: inherit;
28 | }
29 | a:hover {
30 | color: #535bf2;
31 | }
32 |
33 | body {
34 | margin: 0;
35 | display: flex;
36 | place-items: center;
37 | min-width: 320px;
38 | min-height: 100vh;
39 | }
40 |
41 | h1 {
42 | font-size: 3.2em;
43 | line-height: 1.1;
44 | }
45 |
46 | button {
47 | border-radius: 8px;
48 | border: 1px solid transparent;
49 | padding: 0.6em 1.2em;
50 | font-size: 1em;
51 | font-weight: 500;
52 | font-family: inherit;
53 | background-color: #1a1a1a;
54 | cursor: pointer;
55 | transition: border-color 0.25s;
56 | }
57 | button:hover {
58 | border-color: #646cff;
59 | }
60 | button:focus,
61 | button:focus-visible {
62 | outline: 4px auto -webkit-focus-ring-color;
63 | }
64 |
65 | @media (prefers-color-scheme: light) {
66 | :root {
67 | color: #213547;
68 | background-color: #ffffff;
69 | }
70 | a:hover {
71 | color: #747bff;
72 | }
73 | button {
74 | background-color: #f9f9f9;
75 | }
76 | }
77 |
78 | @layer base {
79 | :root {
80 | --background: 0 0% 100%;
81 | --foreground: 0 0% 3.9%;
82 | --card: 0 0% 100%;
83 | --card-foreground: 0 0% 3.9%;
84 | --popover: 0 0% 100%;
85 | --popover-foreground: 0 0% 3.9%;
86 | --primary: 0 0% 9%;
87 | --primary-foreground: 0 0% 98%;
88 | --secondary: 0 0% 96.1%;
89 | --secondary-foreground: 0 0% 9%;
90 | --muted: 0 0% 96.1%;
91 | --muted-foreground: 0 0% 45.1%;
92 | --accent: 0 0% 96.1%;
93 | --accent-foreground: 0 0% 9%;
94 | --destructive: 0 84.2% 60.2%;
95 | --destructive-foreground: 0 0% 98%;
96 | --border: 0 0% 89.8%;
97 | --input: 0 0% 89.8%;
98 | --ring: 0 0% 3.9%;
99 | --chart-1: 12 76% 61%;
100 | --chart-2: 173 58% 39%;
101 | --chart-3: 197 37% 24%;
102 | --chart-4: 43 74% 66%;
103 | --chart-5: 27 87% 67%;
104 | --radius: 0.5rem;
105 | }
106 | .dark {
107 | --background: 0 0% 3.9%;
108 | --foreground: 0 0% 98%;
109 | --card: 0 0% 3.9%;
110 | --card-foreground: 0 0% 98%;
111 | --popover: 0 0% 3.9%;
112 | --popover-foreground: 0 0% 98%;
113 | --primary: 0 0% 98%;
114 | --primary-foreground: 0 0% 9%;
115 | --secondary: 0 0% 14.9%;
116 | --secondary-foreground: 0 0% 98%;
117 | --muted: 0 0% 14.9%;
118 | --muted-foreground: 0 0% 63.9%;
119 | --accent: 0 0% 14.9%;
120 | --accent-foreground: 0 0% 98%;
121 | --destructive: 0 62.8% 30.6%;
122 | --destructive-foreground: 0 0% 98%;
123 | --border: 0 0% 14.9%;
124 | --input: 0 0% 14.9%;
125 | --ring: 0 0% 83.1%;
126 | --chart-1: 220 70% 50%;
127 | --chart-2: 160 60% 45%;
128 | --chart-3: 30 80% 55%;
129 | --chart-4: 280 65% 60%;
130 | --chart-5: 340 75% 55%;
131 | }
132 | }
133 |
134 | @layer base {
135 | * {
136 | @apply border-border;
137 | }
138 | body {
139 | @apply bg-background text-foreground;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/client/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import "leaflet/dist/leaflet.css";
5 | import App from './App.tsx'
6 |
7 | createRoot(document.getElementById('root')!).render(
8 |
9 |
10 | ,
11 | )
12 |
--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{ts,tsx,js,jsx}"
7 | ],
8 | theme: {
9 | extend: {
10 | borderRadius: {
11 | lg: 'var(--radius)',
12 | md: 'calc(var(--radius) - 2px)',
13 | sm: 'calc(var(--radius) - 4px)'
14 | },
15 | colors: {
16 | background: 'hsl(var(--background))',
17 | foreground: 'hsl(var(--foreground))',
18 | card: {
19 | DEFAULT: 'hsl(var(--card))',
20 | foreground: 'hsl(var(--card-foreground))'
21 | },
22 | popover: {
23 | DEFAULT: 'hsl(var(--popover))',
24 | foreground: 'hsl(var(--popover-foreground))'
25 | },
26 | primary: {
27 | DEFAULT: 'hsl(var(--primary))',
28 | foreground: 'hsl(var(--primary-foreground))'
29 | },
30 | secondary: {
31 | DEFAULT: 'hsl(var(--secondary))',
32 | foreground: 'hsl(var(--secondary-foreground))'
33 | },
34 | muted: {
35 | DEFAULT: 'hsl(var(--muted))',
36 | foreground: 'hsl(var(--muted-foreground))'
37 | },
38 | accent: {
39 | DEFAULT: 'hsl(var(--accent))',
40 | foreground: 'hsl(var(--accent-foreground))'
41 | },
42 | destructive: {
43 | DEFAULT: 'hsl(var(--destructive))',
44 | foreground: 'hsl(var(--destructive-foreground))'
45 | },
46 | border: 'hsl(var(--border))',
47 | input: 'hsl(var(--input))',
48 | ring: 'hsl(var(--ring))',
49 | chart: {
50 | '1': 'hsl(var(--chart-1))',
51 | '2': 'hsl(var(--chart-2))',
52 | '3': 'hsl(var(--chart-3))',
53 | '4': 'hsl(var(--chart-4))',
54 | '5': 'hsl(var(--chart-5))'
55 | }
56 | }
57 | }
58 | },
59 | plugins: [require("tailwindcss-animate")],
60 | }
61 |
--------------------------------------------------------------------------------
/client/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": ["./src/*"]
27 | }
28 |
29 | },
30 | "include": ["src"]
31 | }
32 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path"
2 | import react from "@vitejs/plugin-react"
3 | import { defineConfig } from "vite"
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | MONGO_URI=mongodb://localhost/ze-delivery
2 | PORT=3001
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | MONGO_URI=
2 | PORT=
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | mongodb:
5 | image: mongo
6 | container_name: ze_mongodb
7 | ports:
8 | - "27017:27017"
9 | environment:
10 | - MONGO_INITDB_DATABASE=ze-delivery
11 | restart: always
12 | redis:
13 | image: redis
14 | container_name: ze_redis
15 | ports:
16 | - "6379:6379"
17 | restart: always
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ze-backend-challenge",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "dev": "npx nodemon --exec ts-node src/index.ts",
7 | "compose:up": "docker-compose -f docker-compose.yml up -d",
8 | "populate": "npx ts-node ./src/scripts/populate-database.ts",
9 | "test": "vitest"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "description": "",
15 | "dependencies": {
16 | "@koa/cors": "^5.0.0",
17 | "@koa/router": "^13.1.0",
18 | "@turf/turf": "^7.2.0",
19 | "dotenv-safe": "^9.1.0",
20 | "koa": "^2.15.3",
21 | "koa-bodyparser": "^4.4.1",
22 | "mongodb-memory-server": "^10.1.3",
23 | "mongoose": "^8.9.5",
24 | "nodemon": "^3.1.9",
25 | "redis": "^4.7.0",
26 | "supertest": "^7.0.0",
27 | "ts-node": "^10.9.2",
28 | "typescript": "^5.7.3",
29 | "vitest": "^3.0.4",
30 | "zod": "^3.24.1"
31 | },
32 | "devDependencies": {
33 | "@types/dotenv-safe": "^8.1.6",
34 | "@types/koa": "^2.15.0",
35 | "@types/koa__cors": "^5.0.0",
36 | "@types/koa__router": "^12.0.4",
37 | "@types/koa-bodyparser": "^4.3.12",
38 | "@types/supertest": "^6.0.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/app.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import bodyParser from "koa-bodyparser";
3 | import { partnerRouter } from "./modules/partners/router";
4 | import cors from "@koa/cors";
5 |
6 | const app = new Koa();
7 |
8 | app.use(bodyParser());
9 | app.use(cors())
10 |
11 | app.use(partnerRouter.routes());
12 |
13 | export { app }
14 |
15 |
16 |
--------------------------------------------------------------------------------
/server/src/config/config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import dotenvSafe from 'dotenv-safe';
3 |
4 | const cwd = process.cwd();
5 |
6 | const root = path.join.bind(cwd);
7 |
8 | dotenvSafe.config({
9 | path: root('.env'),
10 | sample: root('.env.example'),
11 | });
12 |
13 | const ENV = process.env;
14 |
15 | const config = {
16 | PORT: ENV.PORT ?? 4000,
17 | MONGO_URI: ENV.MONGO_URI ?? '',
18 | };
19 |
20 |
21 |
22 | export { config };
23 |
--------------------------------------------------------------------------------
/server/src/database/mongo.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { config } from "../config/config";
3 | import { MongoMemoryServer } from "mongodb-memory-server"
4 |
5 | export async function connectDatabase() {
6 | mongoose.connection.on("close", () => {
7 | console.log("Mongoose connection closed.")
8 | })
9 |
10 | mongoose.connection.on("error", error => {
11 | console.log("Couldn't connect to Mongo database: ", error)
12 | })
13 |
14 | mongoose.connection.on('connected', () => {
15 | console.log("Successfully conected to your Mongo database.")
16 | });
17 |
18 | await mongoose.connect(config.MONGO_URI)
19 | }
20 |
21 | export async function disconnectDatabase() {
22 | await mongoose.disconnect();
23 | }
24 |
25 | export async function connectMemoryDatabase() {
26 | const mongoServer = await MongoMemoryServer.create();
27 | await mongoose.connect(
28 | mongoServer.getUri(),
29 | {dbName: "ze-bank-test"}
30 | )
31 | }
--------------------------------------------------------------------------------
/server/src/database/redis.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from 'redis';
2 |
3 |
4 | export const redisClient = createClient();
5 |
6 | export async function connectToRedis() {
7 | return redisClient
8 | .on("connect", () => console.log("Redis server connected"))
9 | .on("error", err => console.log(err))
10 | .connect();
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import { app } from "./app";
2 | import { config } from "./config/config";
3 | import { connectDatabase } from "./database/mongo";
4 | import { connectToRedis } from "./database/redis";
5 |
6 | app.listen(config.PORT, async () => {
7 | console.log(`Server open on port ${config.PORT}`)
8 | await connectDatabase();
9 | await connectToRedis();
10 | })
11 |
12 |
--------------------------------------------------------------------------------
/server/src/modules/partners/@types/CreatePartner.ts:
--------------------------------------------------------------------------------
1 | import { MultiPolygon } from "./MultiPolygon"
2 | import { Point } from "./Point"
3 |
4 | export type CreatePartner = {
5 | tradingName: string,
6 | ownerName: string,
7 | document: string,
8 | coverageArea: {
9 | coordinates: MultiPolygon
10 | },
11 | address: {
12 | coordinates: Point
13 | }
14 | }
--------------------------------------------------------------------------------
/server/src/modules/partners/@types/LatLong.ts:
--------------------------------------------------------------------------------
1 | export type LatLong = {
2 | lat: number,
3 | long: number
4 | }
--------------------------------------------------------------------------------
/server/src/modules/partners/@types/MultiPolygon.ts:
--------------------------------------------------------------------------------
1 | import { Polygon } from "./Polygon"
2 | export type MultiPolygon = Polygon[]
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/src/modules/partners/@types/Point.ts:
--------------------------------------------------------------------------------
1 | export type Point = [number, number]
--------------------------------------------------------------------------------
/server/src/modules/partners/@types/Polygon.ts:
--------------------------------------------------------------------------------
1 | import { Point } from "./Point"
2 |
3 | export type Polygon = Point[][]
4 |
5 |
--------------------------------------------------------------------------------
/server/src/modules/partners/PartnerModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Model } from "mongoose";
2 | import { MultiPolygon } from "./@types/MultiPolygon";
3 | import { Point } from "./@types/Point";
4 |
5 | export type IPartner = {
6 | tradingName: string,
7 | ownerName: string,
8 | document: string,
9 | coverageArea: {
10 | type: "MultiPolygon",
11 | coordinates: MultiPolygon
12 | },
13 | address: {
14 | type: "Point",
15 | coordinates: Point
16 | }
17 | } & Document
18 |
19 | const Schema = new mongoose.Schema(
20 | {
21 | tradingName: {
22 | type: String,
23 | required: true,
24 | },
25 | ownerName: {
26 | type: String,
27 | required: true,
28 | },
29 | document: {
30 | type: String,
31 | required: true,
32 | unique: true
33 | },
34 | coverageArea: {
35 | type: {
36 | type: String,
37 | default: "MultiPolygon"
38 | },
39 | coordinates: {
40 | type: [[[[Number, Number]]]]
41 | }
42 | },
43 | address: {
44 | type: {
45 | type: String,
46 | default: "Point"
47 | },
48 | coordinates: {
49 | type: [Number, Number]
50 | }
51 | }
52 | },
53 | {
54 | collection: "Partner",
55 | timestamps: true
56 | }
57 | )
58 |
59 | export const Partner: Model = mongoose.model('Partner', Schema);
60 |
61 |
--------------------------------------------------------------------------------
/server/src/modules/partners/controller.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "koa";
2 | import { validatePartner } from "./validators/create-partner-validator";
3 | import { service } from "./service";
4 | import { Point } from "./@types/Point";
5 |
6 | async function createPartner(ctx: Context) {
7 | try {
8 | const partnerBody = validatePartner(ctx.request.body);
9 | const createdPartner = await service.createPartner(partnerBody)
10 |
11 | ctx.status = 201;
12 | ctx.body = {
13 | data: createdPartner,
14 | message: "Partner successfully created."
15 | }
16 | } catch(error) {
17 | throw error
18 | }
19 | }
20 |
21 | async function findPartnerById(ctx: Context) {
22 | try {
23 | const { id } = ctx.params;
24 | const foundPartner = await service.findPartnerById(id);
25 |
26 | ctx.status = 200;
27 | ctx.body = {
28 | data: foundPartner,
29 | message: "Partner successfully found."
30 | }
31 | } catch(error) {
32 | throw error
33 | }
34 | }
35 |
36 | async function findNearestPartner(ctx: Context) {
37 | try {
38 |
39 | const {lat, long} = ctx.request.query as {long: string, lat: string};
40 | const numberLong = parseFloat(long);
41 | const numberLat = parseFloat(lat);
42 | const point: Point = [numberLong, numberLat]
43 |
44 | const nearestPartner = await service.findNearestPartner(point)
45 |
46 | ctx.status = 200;
47 | ctx.body = {
48 | data: nearestPartner,
49 | message: "Nearest partner successfully found."
50 | }
51 | } catch(error) {
52 | throw error
53 | }
54 | }
55 |
56 | async function findAllPartners(ctx: Context) {
57 | try {
58 | const partners = await service.findAllPartners();
59 |
60 | ctx.status = 200
61 | ctx.body = {
62 | data: partners,
63 | message: "Partners successfully found."
64 | }
65 | } catch(error) {
66 | throw error
67 | }
68 | }
69 |
70 | export const controller = {
71 | createPartner,
72 | findPartnerById,
73 | findNearestPartner,
74 | findAllPartners
75 | }
--------------------------------------------------------------------------------
/server/src/modules/partners/router.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import { controller } from "./controller";
3 |
4 | const partnerRouter = new Router({prefix: "/partner"});
5 |
6 | partnerRouter.post("/", controller.createPartner);
7 | partnerRouter.get("/", controller.findAllPartners);
8 | partnerRouter.get("/nearest", controller.findNearestPartner);
9 | partnerRouter.get("/:id", controller.findPartnerById);
10 |
11 | export { partnerRouter }
--------------------------------------------------------------------------------
/server/src/modules/partners/service.ts:
--------------------------------------------------------------------------------
1 | import { map } from "zod";
2 | import { redisClient } from "../../database/redis";
3 | import { CreatePartner } from "./@types/CreatePartner";
4 | import { Point } from "./@types/Point";
5 | import { IPartner, Partner } from "./PartnerModel";
6 | import * as turf from "@turf/turf";
7 |
8 | async function createPartner(partnerBody: CreatePartner): Promise {
9 |
10 | const partnerExists = await Partner.exists({
11 | document: partnerBody.document
12 | })
13 |
14 | if(partnerExists) {
15 | throw new Error("Partner alrealdy exists.")
16 | }
17 |
18 | return await Partner.create(partnerBody);
19 | }
20 |
21 | async function findPartnerById(id: string): Promise {
22 | const foundPartner = await Partner.findById(id);
23 |
24 | if(!foundPartner) {
25 | throw new Error("Partner not found");
26 | }
27 |
28 | return foundPartner;
29 | }
30 |
31 | async function findNearestPartner(point: Point): Promise {
32 |
33 | const pointCacheId = `long:${point[0]};lat:${point[1]}`
34 | const foundCache = await redisClient.get(pointCacheId);
35 |
36 | if (foundCache) {
37 | return JSON.parse(foundCache)
38 | }
39 |
40 | const partners = await Partner.find();
41 |
42 | const sortByLeastDistance = (a: IPartner, b: IPartner) =>
43 | partnerDistanceTo(a, point) - partnerDistanceTo(b, point)
44 |
45 | const nearestPartner = partners
46 | .concat()
47 | .sort(sortByLeastDistance)
48 | .find(partner => pointInCoverage(partner, point))
49 |
50 |
51 |
52 | if(!nearestPartner) {
53 | throw new Error("No partner was found. Possibly, this point isn't inside the coverage.")
54 | }
55 |
56 |
57 | redisClient.setEx(
58 | pointCacheId,
59 | 120,
60 | JSON.stringify(nearestPartner)
61 | )
62 |
63 | return nearestPartner;
64 |
65 | }
66 |
67 | async function findAllPartners() {
68 | return await Partner.find();
69 | }
70 |
71 | const partnerDistanceTo = ({address}: IPartner, point: Point) =>
72 | turf.distance(address.coordinates, point, {units: "meters"})
73 |
74 | const pointInCoverage = (partner: IPartner, point: Point) =>
75 | turf.booleanPointInPolygon(
76 | turf.point(point),
77 | turf.multiPolygon(partner.coverageArea.coordinates)
78 | )
79 |
80 | export const service = {
81 | createPartner,
82 | findPartnerById,
83 | findNearestPartner,
84 | findAllPartners
85 | }
--------------------------------------------------------------------------------
/server/src/modules/partners/test/partner.test.ts:
--------------------------------------------------------------------------------
1 | import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
2 | import { connectMemoryDatabase, disconnectDatabase } from "../../../database/mongo";
3 | import http from "http";
4 | import request from "supertest";
5 | import { app } from "../../../app";
6 | import mongoose from "mongoose";
7 | import { connectToRedis } from "../../../database/redis";
8 |
9 | let server: http.Server
10 |
11 | beforeAll(async () => {
12 | await connectMemoryDatabase();
13 | await connectToRedis();
14 | server = http.createServer(app.callback());
15 | server.listen();
16 | })
17 |
18 | afterAll(async () => {
19 | server.close();
20 | await disconnectDatabase();
21 | })
22 |
23 | beforeEach(async () => {
24 | await mongoose.connection.dropDatabase()
25 | })
26 |
27 | describe("POST /partner", () => {
28 |
29 | it("Create successfully", async () => {
30 |
31 | const partner = {
32 | "tradingName": "Adega da Cerveja - Pinheiros",
33 | "ownerName": "Zé da Silva",
34 | "document": "1432132123891/0001",
35 | "coverageArea": {
36 | "type": "MultiPolygon",
37 | "coordinates": [
38 | [[[30, 20], [45, 40], [10, 40], [30, 20]]],
39 | [[[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]]
40 | ]
41 | },
42 | "address": {
43 | "type": "Point",
44 | "coordinates": [-46.57421, -21.785741]
45 | }
46 | }
47 |
48 | const response = await request(server)
49 | .post("/partner")
50 | .send(partner)
51 | .expect(201)
52 |
53 | console.log(response)
54 |
55 | expect(response.body.data.tradingName).toBe(partner.tradingName)
56 | expect(response.body.data.ownerName).toBe(partner.ownerName)
57 | expect(response.body.data.coverageArea).toStrictEqual(partner.coverageArea)
58 | expect(response.body.data.address).toStrictEqual(partner.address)
59 |
60 | })
61 |
62 | it("When coverageArea is invalid should fail", async () => {
63 | const partner = {
64 | "tradingName": "Adega da Cerveja - Pinheiros",
65 | "ownerName": "Zé da Silva",
66 | "document": "1432132123891/0001",
67 | "coverageArea": {
68 | "type": "MultiPolygon",
69 | "coordinates": [
70 | [[30, 20], [45, 40], [10, 40], [30, 20]]
71 | ],
72 | },
73 | "address": {
74 | "type": "Point",
75 | "coordinates": [-46.57421, -21.785741]
76 | }
77 | }
78 |
79 | await request(server)
80 | .post("/partner")
81 | .send(partner)
82 | .expect(500)
83 | })
84 |
85 | it("When address is invalid should fail", async () => {
86 | const partner = {
87 | "tradingName": "Adega da Cerveja - Pinheiros",
88 | "ownerName": "Zé da Silva",
89 | "document": "1432132123891/0001",
90 | "coverageArea": {
91 | "type": "MultiPolygon",
92 | "coordinates": [
93 | [[30, 20], [45, 40], [10, 40], [30, 20]]
94 | ],
95 | },
96 | "address": {
97 | "type": "Point",
98 | "coordinates": [-21.785741]
99 | }
100 | }
101 |
102 | await request(server)
103 | .post("/partner")
104 | .send(partner)
105 | .expect(500)
106 | })
107 |
108 | })
109 |
110 | describe("GET /partner/:id", () => {
111 |
112 | it("Find partner by id successfully", async () => {
113 |
114 | const partner = {
115 | "tradingName": "Adega da Cerveja - Pinheiros",
116 | "ownerName": "Zé da Silva",
117 | "document": "1432132123891/0001",
118 | "coverageArea": {
119 | "type": "MultiPolygon",
120 | "coordinates": [
121 | [[[30, 20], [45, 40], [10, 40], [30, 20]]],
122 | [[[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]]
123 | ]
124 | },
125 | "address": {
126 | "type": "Point",
127 | "coordinates": [-46.57421, -21.785741]
128 | }
129 | }
130 |
131 | const partnerResponse = await request(server)
132 | .post("/partner")
133 | .send(partner)
134 | .expect(201)
135 |
136 | const createdPartner = partnerResponse.body.data;
137 | const partnerId = partnerResponse.body.data._id;
138 |
139 | const response = await request(server)
140 | .get(`/partner/${partnerId}`)
141 | .expect(200)
142 |
143 | expect(response.body.data).toMatchObject(createdPartner);
144 |
145 | })
146 |
147 | it("When no partner found should fail.", async () => {
148 | await request(server)
149 | .get(`/partner/1234`)
150 | .expect(500)
151 | })
152 |
153 | })
154 |
155 | describe("GET partner/nearest", () => {
156 |
157 | it("Find the nearest partner", async () => {
158 | const partners = [
159 | {
160 | "tradingName": "Loja A",
161 | "ownerName": "John A",
162 | "document": "1432132123891/0001",
163 | "coverageArea": {
164 | "type": "MultiPolygon",
165 | "coordinates": [
166 | [
167 | [[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]]
168 | ]
169 | ],
170 | },
171 | "address": {
172 | "type": "Point",
173 | "coordinates": [1, 1]
174 | }
175 | },
176 | {
177 | "tradingName": "Loja B",
178 | "ownerName": "John B",
179 | "document": "1432132123891/0002",
180 | "coverageArea": {
181 | "type": "MultiPolygon",
182 | "coordinates": [
183 | [
184 | [[10, 10], [14, 10], [14, 14], [10, 14], [10, 10]]
185 | ]
186 | ],
187 | },
188 | "address": {
189 | "type": "Point",
190 | "coordinates": [9, 9]
191 | }
192 | },
193 | {
194 | "tradingName": "Loja C",
195 | "ownerName": "John C",
196 | "document": "1432132123891/0003",
197 | "coverageArea": {
198 | "type": "MultiPolygon",
199 | "coordinates": [
200 | [
201 | [[5, 5], [9, 5], [9, 9], [5, 9], [5, 5]]
202 | ]
203 | ],
204 | },
205 | "address": {
206 | "type": "Point",
207 | "coordinates": [6, 6]
208 | }
209 | },
210 |
211 | ]
212 |
213 | const partnerAPromise = request(server)
214 | .post("/partner")
215 | .send(partners[0])
216 | .expect(201)
217 |
218 | const partnerBPromise = request(server)
219 | .post("/partner")
220 | .send(partners[1])
221 | .expect(201)
222 |
223 | const partnerCPromise = request(server)
224 | .post("/partner")
225 | .send(partners[2])
226 | .expect(201)
227 |
228 | await Promise.all([
229 | partnerAPromise,
230 | partnerBPromise,
231 | partnerCPromise
232 | ])
233 |
234 | const response = await request(server)
235 | .get("/partner/nearest?long=8&lat=8")
236 | .expect(200)
237 |
238 | expect(response.body.data).toMatchObject(partners[2])
239 |
240 | })
241 |
242 | })
--------------------------------------------------------------------------------
/server/src/modules/partners/validators/create-partner-validator.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | import { CreatePartner } from "../@types/CreatePartner";
3 |
4 | const coverageAreaObject = z.object({
5 | coordinates: z.array(z.array(z.array(z.tuple([z.number(), z.number()]))))
6 | })
7 |
8 | const addressObject = z.object({
9 | coordinates: z.tuple([z.number(), z.number()])
10 | })
11 |
12 | const createPartnerSchema = z.object({
13 | tradingName: z.string().min(1, "tradingName is required"),
14 | ownerName: z.string().min(1, "tradingName is required"),
15 | document: z.string().min(1, "document is required"),
16 | coverageArea: coverageAreaObject,
17 | address: addressObject
18 | });
19 |
20 | export function validatePartner(partner: unknown): CreatePartner {
21 | return createPartnerSchema.parse(partner);
22 | }
--------------------------------------------------------------------------------
/server/src/modules/partners/validators/point-validator.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | import { LatLong } from "../@types/LatLong";
3 |
4 | const latLongSchema = z.object({
5 | lat: z.number(),
6 | long: z.number()
7 | });
8 |
9 | export function validateLatLong(latLong: unknown): LatLong {
10 | return latLongSchema.parse(latLong);
11 | }
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
44 | // "resolveJsonModule": true, /* Enable importing .json files. */
45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
47 |
48 | /* JavaScript Support */
49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
52 |
53 | /* Emit */
54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "noEmit": true, /* Disable emitting files from a compilation. */
60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
61 | // "outDir": "./", /* Specify an output folder for all emitted files. */
62 | // "removeComments": true, /* Disable emitting comments. */
63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
69 | // "newLine": "crlf", /* Set the newline character for emitting files. */
70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
79 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
80 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
81 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
83 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
84 |
85 | /* Type Checking */
86 | "strict": true, /* Enable all strict type-checking options. */
87 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
88 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
90 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
92 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
106 |
107 | /* Completeness */
108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
110 | }
111 | }
112 |
--------------------------------------------------------------------------------