├── 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 |
17 |
18 | 19 |
20 |
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 | --------------------------------------------------------------------------------