├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .env.template ├── .gitignore ├── .medusa │ └── types │ │ ├── index.d.ts │ │ └── remote-query-entry-points.d.ts ├── .yarnrc.yml ├── README.md ├── data │ └── medusa-eats-seed-data.json ├── jest.config.js ├── medusa-config.ts ├── package.json ├── src │ ├── admin │ │ ├── components │ │ │ ├── actions-menu.tsx │ │ │ ├── delivery-actions-menu.tsx │ │ │ ├── delivery-items.tsx │ │ │ ├── delivery-row.tsx │ │ │ ├── driver-actions-menu.tsx │ │ │ └── icons.tsx │ │ ├── hooks │ │ │ └── index.tsx │ │ ├── routes │ │ │ ├── deliveries │ │ │ │ └── page.tsx │ │ │ ├── drivers │ │ │ │ └── page.tsx │ │ │ └── restaurants │ │ │ │ └── page.tsx │ │ └── tsconfig.json │ ├── api │ │ ├── README.md │ │ ├── middlewares.ts │ │ ├── store │ │ │ ├── deliveries │ │ │ │ ├── [id] │ │ │ │ │ ├── accept │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── claim │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── complete │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── decline │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── pass │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── pick-up │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── prepare │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── ready │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── subscribe │ │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── subscribe │ │ │ │ │ └── route.ts │ │ │ ├── drivers │ │ │ │ ├── [id] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── restaurants │ │ │ │ ├── [id] │ │ │ │ │ ├── admins │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── products │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── status │ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── users │ │ │ │ ├── me │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ └── types.ts │ ├── jobs │ │ └── README.md │ ├── links │ │ ├── delivery-cart.ts │ │ ├── delivery-order.ts │ │ ├── restaurant-deliveries.ts │ │ └── restaurant-products.ts │ ├── loaders │ │ └── README.md │ ├── modules │ │ ├── delivery │ │ │ ├── index.ts │ │ │ ├── migrations │ │ │ │ ├── .snapshot-medusa-TGe-.json │ │ │ │ └── Migration20240822141125.ts │ │ │ ├── models │ │ │ │ ├── delivery-driver.ts │ │ │ │ ├── delivery.ts │ │ │ │ ├── driver.ts │ │ │ │ └── index.ts │ │ │ ├── service.ts │ │ │ └── types │ │ │ │ ├── common.ts │ │ │ │ └── mutations.ts │ │ └── restaurant │ │ │ ├── index.ts │ │ │ ├── migrations │ │ │ ├── .snapshot-medusa-eats.json │ │ │ └── Migration20240925143313.ts │ │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── restaurant-admin.ts │ │ │ └── restaurant.ts │ │ │ ├── service.ts │ │ │ └── types │ │ │ ├── common.ts │ │ │ └── mutations.ts │ ├── scripts │ │ └── seed.ts │ ├── subscribers │ │ └── README.md │ ├── utils │ │ ├── create-variant-price-set.ts │ │ └── index.ts │ └── workflows │ │ ├── delivery │ │ ├── steps │ │ │ ├── await-delivery.ts │ │ │ ├── await-pick-up.ts │ │ │ ├── await-preparation.ts │ │ │ ├── await-start-preparation.ts │ │ │ ├── create-delivery.ts │ │ │ ├── create-fulfillment.ts │ │ │ ├── create-order.ts │ │ │ ├── delete-delivery-drivers.ts │ │ │ ├── find-driver.ts │ │ │ ├── index.ts │ │ │ ├── notify-restaurant.ts │ │ │ ├── set-transaction-id.ts │ │ │ └── update-delivery.ts │ │ └── workflows │ │ │ ├── claim-delivery.ts │ │ │ ├── create-delivery.ts │ │ │ ├── handle-delivery.ts │ │ │ ├── index.ts │ │ │ ├── pass-delivery.ts │ │ │ └── update-delivery.ts │ │ ├── restaurant │ │ ├── steps │ │ │ ├── create-restaurant.ts │ │ │ └── index.ts │ │ └── workflows │ │ │ ├── create-restaurant-products.ts │ │ │ ├── create-restaurant.ts │ │ │ └── index.ts │ │ ├── user │ │ ├── steps │ │ │ ├── create-user.ts │ │ │ ├── index.ts │ │ │ └── update-user.ts │ │ └── workflows │ │ │ ├── create-user.ts │ │ │ └── index.ts │ │ └── util │ │ └── steps │ │ ├── index.ts │ │ ├── set-step-failed.ts │ │ └── set-step-success.ts ├── tsconfig.json └── yarn.lock └── frontend ├── .env.template ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public ├── _1ed747de-53f1-4915-aaba-92c046039beb.jpeg ├── _35841975-1e89-4c07-adb8-208cc8c59e17.jpeg ├── _411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg ├── _4476ce30-b852-402d-b65f-e3db54165e4c.jpeg ├── _7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg ├── _ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg ├── _de4759b3-096f-47f6-936a-ea291df53f9f.jpeg ├── medusa-logo.svg ├── next.svg ├── notification.mp3 ├── pizza-loading.gif └── vercel.svg ├── src ├── app │ ├── (checkout) │ │ ├── checkout │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── your-order │ │ │ └── page.tsx │ ├── (store) │ │ ├── layout.tsx │ │ ├── login │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── restaurant │ │ │ └── [handle] │ │ │ │ └── page.tsx │ │ └── signup │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── api │ │ └── subscribe │ │ │ └── route.ts │ ├── dashboard │ │ ├── driver │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── restaurant │ │ │ ├── menu │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── components │ ├── common │ │ ├── demo-modal.tsx │ │ ├── footer.tsx │ │ ├── icons.tsx │ │ └── profile-badge.tsx │ ├── dashboard │ │ ├── account-badge.tsx │ │ ├── completed-grid.tsx │ │ ├── delivery-card.tsx │ │ ├── delivery-column.tsx │ │ ├── driver │ │ │ ├── create-category-drawer.tsx │ │ │ ├── delivery-buttons.tsx │ │ │ └── delivery-status-badge.tsx │ │ ├── login-form.tsx │ │ ├── menu │ │ │ ├── create-category-drawer.tsx │ │ │ ├── create-category-form.tsx │ │ │ ├── create-product-drawer.tsx │ │ │ ├── create-product-form.tsx │ │ │ ├── menu-actions.tsx │ │ │ └── menu-product-actions.tsx │ │ ├── realtime-client.tsx │ │ ├── restaurant │ │ │ ├── delivery-buttons.tsx │ │ │ ├── delivery-status-badge.tsx │ │ │ └── restaurant-status.tsx │ │ └── signup-form.tsx │ └── store │ │ ├── cart │ │ ├── cart-button.tsx │ │ ├── cart-counter.tsx │ │ ├── cart-modal.tsx │ │ └── nav-cart.tsx │ │ ├── checkout │ │ ├── checkout-form.tsx │ │ └── order-summary.tsx │ │ ├── order │ │ ├── lottie-player.tsx │ │ ├── order-status-content-tab.tsx │ │ └── order-status.tsx │ │ └── restaurant │ │ ├── dish-card.tsx │ │ ├── restaurant-categories.tsx │ │ └── restaurant-category.tsx ├── lib │ ├── actions │ │ ├── carts.ts │ │ ├── checkout.ts │ │ ├── deliveries.ts │ │ ├── index.ts │ │ ├── restaurants.ts │ │ └── users.ts │ ├── config.ts │ ├── data │ │ ├── carts.ts │ │ ├── categories.ts │ │ ├── cookies.ts │ │ ├── deliveries.ts │ │ ├── drivers.ts │ │ ├── index.ts │ │ ├── restaurants.ts │ │ ├── sessions.ts │ │ └── users.ts │ ├── types.ts │ └── util │ │ ├── constants.ts │ │ └── get-numeric-status.ts └── middleware.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/dump.rdb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Medusajs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/.env.template: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://postgres@localhost/medusa-eats 2 | FRONTEND_URL=http://localhost:3000 3 | JWT_SECRET=supersecret -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | .env 3 | .DS_Store 4 | /uploads 5 | /node_modules 6 | yarn-error.log 7 | 8 | .idea 9 | 10 | coverage 11 | 12 | !src/** 13 | 14 | ./tsconfig.tsbuildinfo 15 | package-lock.json 16 | medusa-db.sql 17 | build 18 | .cache 19 | gh-private-key.pem 20 | 21 | .yarn/* 22 | !.yarn/patches 23 | !.yarn/plugins 24 | !.yarn/releases 25 | !.yarn/sdks 26 | !.yarn/versions 27 | 28 | .medusa/* 29 | .medusa/ -------------------------------------------------------------------------------- /backend/.medusa/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as RemoteQueryEntryPointsTypes from './remote-query-entry-points' -------------------------------------------------------------------------------- /backend/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | const { loadEnv } = require('@medusajs/utils') 2 | loadEnv('test', process.cwd()) 3 | 4 | module.exports = { 5 | transform: { 6 | "^.+\\.[jt]s$": [ 7 | "@swc/jest", 8 | { 9 | jsc: { 10 | parser: { syntax: "typescript", decorators: true }, 11 | }, 12 | }, 13 | ], 14 | }, 15 | testEnvironment: "node", 16 | moduleFileExtensions: ["js", "ts", "json"], 17 | modulePathIgnorePatterns: ["dist/"], 18 | } 19 | 20 | if (process.env.TEST_TYPE === "integration:http") { 21 | module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] 22 | } else if (process.env.TEST_TYPE === "integration:modules") { 23 | module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] 24 | } else if (process.env.TEST_TYPE === "unit") { 25 | module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] 26 | } -------------------------------------------------------------------------------- /backend/medusa-config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv, defineConfig, Modules } from "@medusajs/framework/utils"; 2 | 3 | loadEnv(process.env.NODE_ENV || "development", process.cwd()); 4 | 5 | module.exports = defineConfig({ 6 | admin: { 7 | backendUrl: "https://medusa-eats.medusajs.app", 8 | }, 9 | projectConfig: { 10 | databaseUrl: process.env.DATABASE_URL, 11 | http: { 12 | storeCors: process.env.STORE_CORS, 13 | adminCors: process.env.ADMIN_CORS, 14 | authCors: process.env.AUTH_CORS, 15 | jwtSecret: process.env.JWT_SECRET || "supersecret", 16 | cookieSecret: process.env.COOKIE_SECRET || "supersecret", 17 | }, 18 | }, 19 | modules: { 20 | restaurantModuleService: { 21 | resolve: "./modules/restaurant", 22 | }, 23 | deliveryModuleService: { 24 | resolve: "./modules/delivery", 25 | }, 26 | [Modules.FULFILLMENT]: { 27 | options: { 28 | providers: [ 29 | { 30 | resolve: "@medusajs/fulfillment-manual", 31 | id: "manual-provider", 32 | }, 33 | ], 34 | }, 35 | }, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-eats-backend", 3 | "version": "0.0.1", 4 | "description": "A Medusa backend for Medusa Eats.", 5 | "author": "Medusa (https://medusajs.com)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "sqlite", 9 | "postgres", 10 | "typescript", 11 | "ecommerce", 12 | "headless", 13 | "medusa", 14 | "medusa-eats" 15 | ], 16 | "scripts": { 17 | "build": "medusa build", 18 | "seed": "medusa exec ./src/scripts/seed.ts", 19 | "start": "medusa start", 20 | "dev": "medusa develop", 21 | "setup-db": "npx medusa build && npx medusa migrations run && npx medusa links sync && medusa exec ./src/scripts/seed.ts && npx medusa user -e admin@email.com -p supersecret", 22 | "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", 23 | "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", 24 | "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" 25 | }, 26 | "dependencies": { 27 | "@medusajs/admin-sdk": "2.0.0", 28 | "@medusajs/cli": "2.0.0", 29 | "@medusajs/framework": "2.0.0", 30 | "@medusajs/medusa": "2.0.0", 31 | "@mikro-orm/core": "5.9.7", 32 | "@mikro-orm/knex": "5.9.7", 33 | "@mikro-orm/migrations": "5.9.7", 34 | "@mikro-orm/postgresql": "5.9.7", 35 | "awilix": "^8.0.1", 36 | "cors": "^2.8.5", 37 | "dotenv": "16.3.1", 38 | "express": "^4.17.2", 39 | "jsonwebtoken": "^9.0.2", 40 | "lucide-react": "^0.379.0", 41 | "pg": "^8.13.0", 42 | "prism-react-renderer": "^2.0.4", 43 | "prop-types": "^15.8.1" 44 | }, 45 | "devDependencies": { 46 | "@medusajs/test-utils": "2.0.0", 47 | "@mikro-orm/cli": "5.9.7", 48 | "@mikro-orm/core": "5.9.7", 49 | "@mikro-orm/migrations": "5.9.7", 50 | "@mikro-orm/postgresql": "5.9.7", 51 | "@stdlib/number-float64-base-normalize": "0.0.8", 52 | "@swc/core": "1.5.7", 53 | "@swc/jest": "^0.2.36", 54 | "@types/express": "^4.17.13", 55 | "@types/jest": "^29.5.13", 56 | "@types/jsonwebtoken": "^9.0.6", 57 | "@types/mime": "1.3.5", 58 | "@types/node": "^20.0.0", 59 | "@types/react": "^18.3.2", 60 | "@types/react-dom": "^18.2.25", 61 | "jest": "^29.7.0", 62 | "prop-types": "^15.8.1", 63 | "react": "^18.2.0", 64 | "react-dom": "^18.2.0", 65 | "ts-node": "^10.9.2", 66 | "typescript": "^5.6.2", 67 | "vite": "^5.2.11" 68 | }, 69 | "engines": { 70 | "node": ">=20" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backend/src/admin/components/actions-menu.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"; 2 | import { DropdownMenu, IconButton } from "@medusajs/ui"; 3 | 4 | const ActionsMenu = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Edit 16 | 17 | 18 | 19 | 20 | Delete 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default ActionsMenu; 28 | -------------------------------------------------------------------------------- /backend/src/admin/components/delivery-actions-menu.tsx: -------------------------------------------------------------------------------- 1 | import { ArrrowRight, EllipsisHorizontal, Trash } from "@medusajs/icons"; 2 | import { DropdownMenu, IconButton } from "@medusajs/ui"; 3 | import { DeliveryDTO } from "src/types/delivery/common"; 4 | 5 | const DeliveryActionsMenu = ({ delivery }: { delivery: DeliveryDTO }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | View customer 17 | 18 | {delivery.driver_id && ( 19 | 20 | 21 | 22 | View driver 23 | 24 | 25 | )} 26 | {delivery.order?.id && ( 27 | 28 | 29 | 30 | View order 31 | 32 | 33 | )} 34 | {delivery.restaurant.id && ( 35 | 36 | 37 | 38 | View restaurant 39 | 40 | 41 | )} 42 | 43 | 44 | 45 | Cancel order 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default DeliveryActionsMenu; 53 | -------------------------------------------------------------------------------- /backend/src/admin/components/delivery-items.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Text, Container } from "@medusajs/ui"; 2 | import { DeliveryDTO } from "../../modules/delivery/types/common"; 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | const DeliveryItems = ({ delivery }: { delivery: DeliveryDTO }) => { 6 | const [hovered, setHovered] = useState(false); 7 | 8 | // on hovering over the items count, set hovered to true 9 | const handleMouseEnter = () => { 10 | setHovered(true); 11 | }; 12 | 13 | // on leaving the items count, set hovered to false 14 | const handleMouseLeave = () => { 15 | setHovered(false); 16 | }; 17 | 18 | return ( 19 | <> 20 |
25 | 26 | {delivery.items.length} 27 | 28 |
29 | {hovered && ( 30 | 31 | {delivery.items.map((item) => ( 32 | 33 | ))} 34 | 35 | )} 36 | 37 | ); 38 | }; 39 | 40 | const DeliveryItem = ({ item }: { item: Record }) => { 41 | const thumbnail = item.thumbnail; 42 | 43 | return ( 44 |
45 | {item.title} 52 |
53 |
54 | 55 | {item.title} 56 | 57 | 58 | {item.quantity} x ${item.unit_price} 59 | 60 |
61 |
62 |
63 | ); 64 | }; 65 | 66 | export default DeliveryItems; 67 | -------------------------------------------------------------------------------- /backend/src/admin/components/delivery-row.tsx: -------------------------------------------------------------------------------- 1 | import { Table, Badge } from "@medusajs/ui"; 2 | import { DeliveryDTO } from "../../modules/delivery/types/common"; 3 | import { useRestaurants } from "../hooks"; 4 | import DeliveryActionsMenu from "./delivery-actions-menu"; 5 | import DeliveryItems from "./delivery-items"; 6 | 7 | const DeliveryRow = ({ delivery }: { delivery: DeliveryDTO }) => { 8 | const { data, loading } = useRestaurants({ 9 | id: delivery.restaurant.id, 10 | }); 11 | 12 | if (loading) { 13 | return ; 14 | } 15 | 16 | const restaurant = data?.restaurants[0]; 17 | 18 | return ( 19 | 20 | {delivery.id.slice(-4)} 21 | 22 | {new Date(delivery.created_at).toLocaleString(undefined, { 23 | hour: "numeric", 24 | minute: "numeric", 25 | year: "numeric", 26 | month: "long", 27 | day: "numeric", 28 | })} 29 | 30 | 31 | 32 | {delivery.delivery_status} 33 | 34 | 35 | 36 | {!delivery.delivered_at ? "ETA " : ""} 37 | {new Date(delivery.delivered_at || delivery.eta).toLocaleTimeString( 38 | undefined, 39 | { 40 | hour: "numeric", 41 | minute: "numeric", 42 | } 43 | )} 44 | 45 | {restaurant.name} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default DeliveryRow; 57 | -------------------------------------------------------------------------------- /backend/src/admin/components/driver-actions-menu.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | EllipsisHorizontal, 3 | PencilSquare, 4 | Trash, 5 | XCircleSolid, 6 | } from "@medusajs/icons"; 7 | import { DropdownMenu, IconButton } from "@medusajs/ui"; 8 | import { DriverDTO } from "src/types/delivery/common"; 9 | 10 | const DriverActionsMenu = ({ driver }: { driver: DriverDTO }) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Edit 22 | 23 | 24 | 25 | 26 | Fire {driver.first_name} 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default DriverActionsMenu; 34 | -------------------------------------------------------------------------------- /backend/src/admin/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { Bike, Pizza, Store } from "lucide-react"; 2 | 3 | import React, { 4 | ForwardRefExoticComponent, 5 | PropsWithChildren, 6 | RefAttributes, 7 | } from "react"; 8 | 9 | export const PizzaIcon: ForwardRefExoticComponent< 10 | RefAttributes 11 | > = React.forwardRef((props: PropsWithChildren, ref) => ( 12 | 13 | )); 14 | 15 | export const BikeIcon: ForwardRefExoticComponent> = 16 | React.forwardRef((props: PropsWithChildren, ref) => ( 17 | 18 | )); 19 | 20 | export const StoreIcon: ForwardRefExoticComponent< 21 | RefAttributes 22 | > = React.forwardRef((props: PropsWithChildren, ref) => ( 23 | 24 | )); 25 | -------------------------------------------------------------------------------- /backend/src/admin/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { DeliveryDTO, DriverDTO } from "../../modules/delivery/types/common"; 3 | import { RestaurantDTO } from "../../modules/restaurant/types/common"; 4 | 5 | export const useDrivers = ( 6 | query?: Record 7 | ): { 8 | data: { drivers: DriverDTO[] } | null; 9 | loading: boolean; 10 | } => { 11 | const [data, setData] = useState(null); 12 | const [loading, setLoading] = useState(true); 13 | const filterQuery = new URLSearchParams(query).toString(); 14 | 15 | useEffect(() => { 16 | const fetchDrivers = async () => { 17 | try { 18 | const response = await fetch( 19 | "/admin/drivers" + (query ? `?${filterQuery}` : "") 20 | ); 21 | const result = await response.json(); 22 | setData(result); 23 | } catch (error) { 24 | console.error("Error fetching the data", error); 25 | } finally { 26 | setLoading(false); 27 | } 28 | }; 29 | 30 | fetchDrivers(); 31 | }, []); 32 | 33 | return { data, loading }; 34 | }; 35 | 36 | export const useDeliveries = ( 37 | query?: Record 38 | ): { 39 | data: { deliveries: DeliveryDTO[] } | null; 40 | loading: boolean; 41 | } => { 42 | const [data, setData] = useState(null); 43 | const [loading, setLoading] = useState(true); 44 | 45 | const filterQuery = new URLSearchParams(query).toString(); 46 | 47 | useEffect(() => { 48 | const fetchDeliveries = async () => { 49 | try { 50 | const response = await fetch( 51 | "/admin/deliveries" + (query ? `?${filterQuery}` : "") 52 | ); 53 | const result = await response.json(); 54 | setData(result); 55 | } catch (error) { 56 | console.error("Error fetching the data", error); 57 | } finally { 58 | setLoading(false); 59 | } 60 | }; 61 | 62 | fetchDeliveries(); 63 | }, []); 64 | 65 | return { data, loading }; 66 | }; 67 | 68 | export const useRestaurants = ( 69 | query?: Record 70 | ): { 71 | data: { restaurants: RestaurantDTO[] } | null; 72 | loading: boolean; 73 | } => { 74 | const [data, setData] = useState(null); 75 | const [loading, setLoading] = useState(true); 76 | const filterQuery = new URLSearchParams(query).toString(); 77 | 78 | useEffect(() => { 79 | const fetchRestaurants = async () => { 80 | try { 81 | const response = await fetch( 82 | "/admin/restaurants" + (query ? `?${filterQuery}` : "") 83 | ); 84 | const result = await response.json(); 85 | setData(result); 86 | } catch (error) { 87 | console.error("Error fetching the data", error); 88 | } finally { 89 | setLoading(false); 90 | } 91 | }; 92 | 93 | fetchRestaurants(); 94 | }, []); 95 | 96 | return { data, loading }; 97 | }; 98 | -------------------------------------------------------------------------------- /backend/src/admin/routes/deliveries/page.tsx: -------------------------------------------------------------------------------- 1 | import { defineRouteConfig } from "@medusajs/admin-sdk"; 2 | import { Container, Heading, Table, Text } from "@medusajs/ui"; 3 | import DeliveryRow from "../../components/delivery-row"; 4 | import { PizzaIcon } from "../../components/icons"; 5 | import { useDeliveries } from "../../hooks"; 6 | 7 | const Deliveries = () => { 8 | const { data, loading } = useDeliveries(); 9 | 10 | return ( 11 | 12 |
13 | Deliveries 14 |
15 | {loading && Loading...} 16 | {data?.deliveries && ( 17 | 18 | 19 | 20 | ID 21 | Created 22 | Status 23 | Delivery 24 | Restaurant 25 | Items 26 | Action 27 | 28 | 29 | 30 | {data.deliveries.map((delivery) => ( 31 | 32 | ))} 33 | 34 |
35 | )} 36 |
37 |
38 | ); 39 | }; 40 | 41 | export const config = defineRouteConfig({ 42 | label: "Deliveries", 43 | icon: PizzaIcon, 44 | }); 45 | 46 | export default Deliveries; 47 | -------------------------------------------------------------------------------- /backend/src/admin/routes/drivers/page.tsx: -------------------------------------------------------------------------------- 1 | import { defineRouteConfig } from "@medusajs/admin-sdk"; 2 | import { Container, Heading, Table, Text } from "@medusajs/ui"; 3 | import { DriverDTO } from "../../../modules/delivery/types/common"; 4 | import DriverActionsMenu from "../../components/driver-actions-menu"; 5 | import { BikeIcon } from "../../components/icons"; 6 | import { useDeliveries, useDrivers } from "../../hooks"; 7 | 8 | const Drivers = () => { 9 | const { data, loading } = useDrivers(); 10 | 11 | return ( 12 | 13 |
14 | Drivers 15 |
16 | 17 | 18 | 19 | Name 20 | Phone 21 | Email 22 | Completed deliveries 23 | Actions 24 | 25 | 26 | {loading && Loading...} 27 | {data?.drivers && ( 28 | 29 | {data.drivers.map((driver) => { 30 | return ( 31 | 32 | 33 | {driver.avatar_url && ( 34 | 38 | )} 39 | {driver.first_name} {driver.last_name} 40 | 41 | {driver.phone} 42 | {driver.email} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | })} 52 | 53 | )} 54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | const DeliveryCount = ({ driver }: { driver: DriverDTO }) => { 61 | const { data, loading } = useDeliveries({ 62 | driver_id: driver.id, 63 | }); 64 | 65 | return {loading ? "Loading..." : data?.deliveries.length}; 66 | }; 67 | 68 | export const config = defineRouteConfig({ 69 | label: "Drivers", 70 | icon: BikeIcon, 71 | }); 72 | 73 | export default Drivers; 74 | -------------------------------------------------------------------------------- /backend/src/admin/routes/restaurants/page.tsx: -------------------------------------------------------------------------------- 1 | import { defineRouteConfig } from "@medusajs/admin-sdk"; 2 | import { Container, Heading, StatusBadge, Table, Text } from "@medusajs/ui"; 3 | import ActionsMenu from "../../components/actions-menu"; 4 | import { StoreIcon } from "../../components/icons"; 5 | import { useRestaurants } from "../../hooks"; 6 | 7 | const Restaurants = () => { 8 | const { data, loading } = useRestaurants(); 9 | 10 | return ( 11 | 12 |
13 | Restaurants 14 |
15 | {loading && Loading...} 16 | 17 | 18 | 19 | Name 20 | Status 21 | Phone 22 | Email 23 | Actions 24 | 25 | 26 | {data?.restaurants && ( 27 | 28 | {data.restaurants.map((restaurant) => ( 29 | 30 | {restaurant.name} 31 | 32 | 33 | {restaurant.is_open ? "Open" : "Closed"} 34 | 35 | 36 | {restaurant.phone} 37 | {restaurant.email} 38 | 39 | 40 | 41 | 42 | ))} 43 | 44 | )} 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export const config = defineRouteConfig({ 52 | label: "Restaurants", 53 | icon: StoreIcon, 54 | }); 55 | 56 | export default Restaurants; 57 | -------------------------------------------------------------------------------- /backend/src/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "Node16" 5 | }, 6 | "include": ["."], 7 | "exclude": ["**/*.spec.js"] 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/api/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { defineMiddlewares, authenticate } from "@medusajs/framework"; 2 | 3 | const isAllowed = (req, res, next) => { 4 | const { restaurant_id, driver_id } = req.auth_context.app_metadata; 5 | 6 | if (restaurant_id || driver_id) { 7 | const user = { 8 | actor_type: restaurant_id ? "restaurant" : "driver", 9 | user_id: restaurant_id || driver_id, 10 | }; 11 | 12 | req.user = user; 13 | 14 | next(); 15 | } else { 16 | res.status(403).json({ 17 | message: 18 | "Forbidden. Reason: No restaurant_id or driver_id in app_metadata", 19 | }); 20 | } 21 | }; 22 | 23 | export default defineMiddlewares({ 24 | routes: [ 25 | { 26 | method: ["GET"], 27 | matcher: "/store/users/me", 28 | middlewares: [ 29 | authenticate(["driver", "restaurant"], "bearer"), 30 | isAllowed, 31 | ], 32 | }, 33 | { 34 | method: ["POST"], 35 | matcher: "/store/users", 36 | middlewares: [ 37 | authenticate(["driver", "restaurant"], "bearer", { 38 | allowUnregistered: true, 39 | }), 40 | ], 41 | }, 42 | { 43 | method: ["POST", "DELETE"], 44 | matcher: "/store/restaurants/:id/**", 45 | middlewares: [authenticate(["restaurant", "admin"], "bearer")], 46 | }, 47 | { 48 | matcher: "/store/restaurants/:id/admin/**", 49 | middlewares: [authenticate(["restaurant", "admin"], "bearer")], 50 | }, 51 | ], 52 | }); 53 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/accept/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { notifyRestaurantStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | const DEFAULT_PROCESSING_TIME = 30 * 60 * 1000; // 30 min 8 | 9 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 10 | const { id } = req.params; 11 | 12 | const eta = new Date(new Date().getTime() + DEFAULT_PROCESSING_TIME); 13 | 14 | const data = { 15 | id, 16 | delivery_status: DeliveryStatus.RESTAURANT_ACCEPTED, 17 | eta, 18 | }; 19 | 20 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 21 | .run({ 22 | input: { 23 | data, 24 | stepIdToSucceed: notifyRestaurantStepId, 25 | }, 26 | }) 27 | .catch((error) => { 28 | console.log(error); 29 | return MedusaError.Types.UNEXPECTED_STATE; 30 | }); 31 | 32 | return res.status(200).json({ delivery: updatedDelivery }); 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/claim/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import zod from "zod"; 4 | import { claimDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 5 | 6 | const schema = zod.object({ 7 | driver_id: zod.string(), 8 | }); 9 | 10 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 11 | const validatedBody = schema.parse(req.body); 12 | const deliveryId = req.params.id; 13 | 14 | if (!validatedBody.driver_id) { 15 | return MedusaError.Types.INVALID_DATA; 16 | } 17 | 18 | const claimedDelivery = await claimDeliveryWorkflow(req.scope).run({ 19 | input: { 20 | driver_id: validatedBody.driver_id, 21 | delivery_id: deliveryId, 22 | }, 23 | }); 24 | 25 | return res.status(200).json({ delivery: claimedDelivery }); 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/complete/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { awaitDeliveryStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 8 | const { id } = req.params; 9 | 10 | const data = { 11 | id, 12 | delivery_status: DeliveryStatus.DELIVERED, 13 | delivered_at: new Date(), 14 | }; 15 | 16 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 17 | .run({ 18 | input: { 19 | data, 20 | stepIdToSucceed: awaitDeliveryStepId, 21 | }, 22 | }) 23 | .catch((error) => { 24 | return MedusaError.Types.UNEXPECTED_STATE; 25 | }); 26 | 27 | return res.status(200).json({ delivery: updatedDelivery }); 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/decline/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { notifyRestaurantStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 8 | const { id } = req.params; 9 | 10 | const data = { 11 | id, 12 | delivery_status: DeliveryStatus.RESTAURANT_DECLINED, 13 | }; 14 | 15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 16 | .run({ 17 | input: { 18 | data, 19 | stepIdToFail: notifyRestaurantStepId, 20 | }, 21 | }) 22 | .catch((error) => { 23 | return MedusaError.Types.UNEXPECTED_STATE; 24 | }); 25 | 26 | return res.status(200).json({ delivery: updatedDelivery }); 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/pass/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import zod from "zod"; 3 | import { passDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 4 | 5 | const schema = zod.object({ 6 | driver_id: zod.string(), 7 | }); 8 | 9 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) { 10 | const validatedBody = schema.parse(req.body); 11 | 12 | const deliveryId = req.params.id; 13 | 14 | await passDeliveryWorkflow(req.scope).run({ 15 | input: { 16 | driver_id: validatedBody.driver_id, 17 | delivery_id: deliveryId, 18 | }, 19 | }); 20 | 21 | return res.status(200).json({ message: "Driver declined delivery" }); 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/pick-up/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { awaitPickUpStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 8 | const { id } = req.params; 9 | 10 | const data = { 11 | id, 12 | delivery_status: DeliveryStatus.IN_TRANSIT, 13 | }; 14 | 15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 16 | .run({ 17 | input: { 18 | data, 19 | stepIdToSucceed: awaitPickUpStepId, 20 | }, 21 | }) 22 | .catch((error) => { 23 | console.log(error); 24 | return MedusaError.Types.UNEXPECTED_STATE; 25 | }); 26 | 27 | return res.status(200).json({ delivery: updatedDelivery }); 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/prepare/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { awaitStartPreparationStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 8 | const { id } = req.params; 9 | 10 | const data = { 11 | id, 12 | delivery_status: DeliveryStatus.RESTAURANT_PREPARING, 13 | }; 14 | 15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 16 | .run({ 17 | input: { 18 | data, 19 | stepIdToSucceed: awaitStartPreparationStepId, 20 | }, 21 | }) 22 | .catch((error) => { 23 | return MedusaError.Types.UNEXPECTED_STATE; 24 | }); 25 | 26 | return res.status(200).json({ delivery: updatedDelivery }); 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/ready/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { MedusaError } from "@medusajs/utils"; 3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common"; 4 | import { awaitPreparationStepId } from "../../../../../workflows/delivery/steps"; 5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows"; 6 | 7 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 8 | const { id } = req.params; 9 | 10 | const data = { 11 | id, 12 | delivery_status: DeliveryStatus.READY_FOR_PICKUP, 13 | }; 14 | 15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope) 16 | .run({ 17 | input: { 18 | data, 19 | stepIdToSucceed: awaitPreparationStepId, 20 | }, 21 | }) 22 | .catch((error) => { 23 | return MedusaError.Types.UNEXPECTED_STATE; 24 | }); 25 | 26 | return res.status(200).json({ delivery: updatedDelivery }); 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys } from "@medusajs/utils"; 3 | import zod from "zod"; 4 | import { DeliveryStatus } from "../../../../modules/delivery/types/common"; 5 | import { UpdateDeliveryDTO } from "../../../../modules/delivery/types/mutations"; 6 | import { updateDeliveryWorkflow } from "../../../../workflows/delivery/workflows"; 7 | 8 | const schema = zod.object({ 9 | driver_id: zod.string().optional(), 10 | notified_driver_ids: zod.array(zod.string()).optional(), 11 | order_id: zod.string().optional(), 12 | delivery_status: zod.nativeEnum(DeliveryStatus).optional(), 13 | eta: zod.date().optional(), 14 | }); 15 | 16 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 17 | const validatedBody = schema.parse(req.body); 18 | 19 | const deliveryId = req.params.id; 20 | 21 | const updateData: UpdateDeliveryDTO = { 22 | id: deliveryId, 23 | ...validatedBody, 24 | }; 25 | 26 | if (validatedBody.delivery_status === "delivered") { 27 | updateData.delivered_at = new Date(); 28 | } 29 | 30 | try { 31 | const delivery = await updateDeliveryWorkflow(req.scope).run({ 32 | input: { 33 | data: updateData, 34 | }, 35 | }); 36 | 37 | return res.status(200).json({ delivery }); 38 | } catch (error) { 39 | return res.status(500).json({ message: error.message }); 40 | } 41 | } 42 | 43 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 44 | const deliveryId = req.params.id; 45 | 46 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 47 | 48 | const deliveryQuery = { 49 | entity: "delivery", 50 | fields: [ 51 | "*", 52 | "cart.*", 53 | "cart.items.*", 54 | "order.*", 55 | "order.items.*", 56 | "restaurant.*", 57 | ], 58 | filters: { 59 | id: deliveryId, 60 | }, 61 | }; 62 | 63 | const { 64 | data: [delivery], 65 | } = await query.graph(deliveryQuery); 66 | 67 | if (!delivery) { 68 | return res.status(404).json({ message: "Delivery not found" }); 69 | } 70 | 71 | try { 72 | return res.status(200).json({ delivery }); 73 | } catch (error) { 74 | return res.status(500).json({ message: error.message }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/api/store/deliveries/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys } from "@medusajs/utils"; 3 | import zod from "zod"; 4 | import { 5 | createDeliveryWorkflow, 6 | handleDeliveryWorkflow, 7 | } from "../../../workflows/delivery/workflows"; 8 | 9 | const schema = zod.object({ 10 | cart_id: zod.string().startsWith("cart_"), 11 | restaurant_id: zod.string().startsWith("res_"), 12 | }); 13 | 14 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 15 | const validatedBody = schema.parse(req.body); 16 | 17 | const { result: delivery } = await createDeliveryWorkflow(req.scope).run({ 18 | input: { 19 | cart_id: validatedBody.cart_id, 20 | restaurant_id: validatedBody.restaurant_id, 21 | }, 22 | }); 23 | 24 | const { transaction } = await handleDeliveryWorkflow(req.scope).run({ 25 | input: { 26 | delivery_id: delivery.id, 27 | }, 28 | }); 29 | 30 | return res 31 | .status(200) 32 | .json({ message: "Delivery created", delivery, transaction }); 33 | } 34 | 35 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 36 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 37 | 38 | const filters = {} as Record; 39 | let take = parseInt(req.query.take as string) || null; 40 | let skip = parseInt(req.query.skip as string) || 0; 41 | 42 | for (const key in req.query) { 43 | if (["take", "skip"].includes(key)) continue; 44 | 45 | filters[key] = req.query[key]; 46 | } 47 | const deliveryQuery = { 48 | entity: "delivery", 49 | fields: ["*", "cart.*", "cart.items.*", "order.*", "order.items.*"], 50 | filters, 51 | pagination: { 52 | take, 53 | skip, 54 | }, 55 | }; 56 | 57 | const { data: deliveries } = await query.graph(deliveryQuery); 58 | 59 | if (filters.hasOwnProperty("driver_id")) { 60 | const driverQuery = { 61 | entity: "delivery_driver", 62 | fields: ["driver_id", "delivery_id"], 63 | filters: { 64 | deleted_at: null, 65 | driver_id: filters["driver_id"], 66 | }, 67 | }; 68 | 69 | const { data: availableDeliveriesIds } = await query.graph(driverQuery); 70 | 71 | const availableDeliveriesQuery = { 72 | entity: "delivery", 73 | fields: ["*", "cart.*", "cart.items.*", "order.*", "order.items.*"], 74 | filters: { 75 | id: availableDeliveriesIds.map((d) => d.delivery_id), 76 | }, 77 | }; 78 | 79 | const { data: availableDeliveries } = await query.graph( 80 | availableDeliveriesQuery 81 | ); 82 | 83 | deliveries.push(...availableDeliveries); 84 | } 85 | 86 | return res.status(200).json({ deliveries }); 87 | } 88 | -------------------------------------------------------------------------------- /backend/src/api/store/drivers/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"; 3 | import zod from "zod"; 4 | import { DELIVERY_MODULE } from "../../../../modules/delivery"; 5 | 6 | const schema = zod.object({ 7 | first_name: zod.string().optional(), 8 | last_name: zod.string().optional(), 9 | email: zod.string().optional(), 10 | phone: zod.string().optional(), 11 | avatar_url: zod.string().optional(), 12 | }); 13 | 14 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 15 | const validatedBody = schema.parse(req.body); 16 | 17 | if (!validatedBody) { 18 | return MedusaError.Types.INVALID_DATA; 19 | } 20 | 21 | const deliveryModuleService = req.scope.resolve(DELIVERY_MODULE); 22 | 23 | const driverId = req.params.id; 24 | 25 | const data = { 26 | id: driverId, 27 | ...validatedBody, 28 | }; 29 | 30 | if (!driverId) { 31 | return MedusaError.Types.NOT_FOUND; 32 | } 33 | 34 | const driver = await deliveryModuleService.updateDrivers(data); 35 | 36 | return res.status(200).json({ driver }); 37 | } 38 | 39 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 40 | const driverId = req.params.id; 41 | 42 | if (!driverId) { 43 | return MedusaError.Types.INVALID_DATA; 44 | } 45 | 46 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 47 | 48 | const driverQuery = { 49 | entity: "driver", 50 | filters: { 51 | id: driverId, 52 | }, 53 | fields: ["id", "first_name", "last_name", "email", "phone", "avatar_url"], 54 | }; 55 | 56 | const { 57 | data: [driver], 58 | } = await query.graph(driverQuery); 59 | 60 | return res.status(200).json({ driver }); 61 | } 62 | 63 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) { 64 | const driverId = req.params.id; 65 | 66 | if (!driverId) { 67 | return MedusaError.Types.INVALID_DATA; 68 | } 69 | 70 | const deliveryModuleService = req.scope.resolve(DELIVERY_MODULE); 71 | 72 | await deliveryModuleService.deleteDrivers(driverId); 73 | 74 | return res.status(200).json({ message: "Driver deleted" }); 75 | } 76 | -------------------------------------------------------------------------------- /backend/src/api/store/drivers/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys } from "@medusajs/utils"; 3 | 4 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 5 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 6 | 7 | try { 8 | const driverQuery = { 9 | entity: "driver", 10 | fields: ["*"], 11 | pagination: { 12 | take: null, 13 | skip: 0, 14 | }, 15 | }; 16 | 17 | const { data: drivers } = await query.graph(driverQuery); 18 | 19 | return res.status(200).json({ drivers }); 20 | } catch (error) { 21 | return res.status(500).json({ message: error.message }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/api/store/restaurants/[id]/admins/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticatedMedusaRequest, 3 | MedusaRequest, 4 | MedusaResponse, 5 | } from "@medusajs/framework"; 6 | import { 7 | ContainerRegistrationKeys, 8 | MedusaError, 9 | remoteQueryObjectFromString, 10 | } from "@medusajs/utils"; 11 | import zod from "zod"; 12 | import { createUserWorkflow } from "../../../../../workflows/user/workflows/create-user"; 13 | import { RESTAURANT_MODULE } from "../../../../../modules/restaurant"; 14 | 15 | const schema = zod 16 | .object({ 17 | email: zod.string().email(), 18 | first_name: zod.string().optional(), 19 | last_name: zod.string().optional(), 20 | }) 21 | .required({ email: true }); 22 | 23 | export const POST = async ( 24 | req: AuthenticatedMedusaRequest, 25 | res: MedusaResponse 26 | ) => { 27 | const authIdentityId = req.auth_context.auth_identity_id; 28 | const restaurantId = req.params.id; 29 | 30 | const validatedBody = schema.parse(req.body) as { 31 | email: string; 32 | first_name: string; 33 | last_name: string; 34 | }; 35 | 36 | const { result, errors } = await createUserWorkflow(req.scope).run({ 37 | input: { 38 | user: { 39 | ...validatedBody, 40 | actor_type: "restaurant", 41 | restaurant_id: restaurantId, 42 | }, 43 | auth_identity_id: authIdentityId, 44 | }, 45 | throwOnError: false, 46 | }); 47 | 48 | if (Array.isArray(errors) && errors[0]) { 49 | throw errors[0].error; 50 | } 51 | 52 | res.status(201).json({ user: result }); 53 | }; 54 | 55 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 56 | const restaurantId = req.params.id; 57 | 58 | if (!restaurantId) { 59 | return MedusaError.Types.NOT_FOUND, "Restaurant not found"; 60 | } 61 | 62 | const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY); 63 | 64 | const restaurantAdminsQuery = remoteQueryObjectFromString({ 65 | entryPoint: "restaurant_admin", 66 | fields: ["id", "email", "first_name", "last_name"], 67 | filters: { 68 | restaurant_id: restaurantId, 69 | }, 70 | }); 71 | 72 | const restaurantAdmins = await remoteQuery(restaurantAdminsQuery); 73 | 74 | return res.status(200).json({ restaurant_admins: restaurantAdmins }); 75 | } 76 | 77 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) { 78 | const restaurantId = req.params.id; 79 | const adminId = req.params.adminId; 80 | 81 | if (!restaurantId || !adminId) { 82 | return MedusaError.Types.INVALID_DATA; 83 | } 84 | 85 | const restaurantModuleService = req.scope.resolve(RESTAURANT_MODULE); 86 | 87 | await restaurantModuleService.deleteRestaurantAdmins(adminId); 88 | 89 | return res.status(200).json({ message: "Admin deleted" }); 90 | } 91 | -------------------------------------------------------------------------------- /backend/src/api/store/restaurants/[id]/products/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"; 3 | import { deleteProductsWorkflow } from "@medusajs/core-flows"; 4 | import { createRestaurantProductsWorkflow } from "../../../../../workflows/restaurant/workflows"; 5 | import { AdminCreateProduct } from "@medusajs/types"; 6 | import { z } from "zod"; 7 | 8 | const createSchema = z.object({ 9 | products: z.array(z.custom()), 10 | }); 11 | 12 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 13 | const validatedBody = createSchema.parse(req.body); 14 | 15 | const { result: restaurantProducts } = await createRestaurantProductsWorkflow( 16 | req.scope 17 | ).run({ 18 | input: { 19 | products: validatedBody.products as any[], 20 | restaurant_id: req.params.id, 21 | }, 22 | }); 23 | 24 | // Return the product 25 | return res.status(200).json({ restaurant_products: restaurantProducts }); 26 | } 27 | 28 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 29 | const restaurantId = req.params.id; 30 | 31 | if (!restaurantId) { 32 | return MedusaError.Types.NOT_FOUND; 33 | } 34 | 35 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 36 | 37 | const restaurantProductsQuery = { 38 | entity: "product", 39 | filters: { 40 | restaurant: restaurantId, 41 | }, 42 | fields: [ 43 | "id", 44 | "title", 45 | "description", 46 | "thumbnail", 47 | "categories.*", 48 | "categories.id", 49 | "categories.name", 50 | "variants.*", 51 | "variants.id", 52 | "variants.price_set.*", 53 | "variants.price_set.id", 54 | "restaurant.*", 55 | ], 56 | }; 57 | 58 | const { data: restaurantProducts } = await query.graph( 59 | restaurantProductsQuery 60 | ); 61 | 62 | return res.status(200).json({ restaurant_products: restaurantProducts }); 63 | } 64 | 65 | const deleteSchema = z.object({ 66 | product_ids: z.string().array(), 67 | }); 68 | 69 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) { 70 | const validatedBody = deleteSchema.parse(req.body); 71 | 72 | await deleteProductsWorkflow(req.scope).run({ 73 | input: { 74 | ids: validatedBody.product_ids, 75 | }, 76 | }); 77 | 78 | return res.status(200).json({ success: true }); 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/api/store/restaurants/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { QueryContext } from "@medusajs/framework/utils"; 3 | import { ContainerRegistrationKeys } from "@medusajs/utils"; 4 | import { query } from "express"; 5 | 6 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 7 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 8 | 9 | const { currency_code = "eur", ...reqQuery } = req.query; 10 | 11 | const restaurantId = req.params.id; 12 | 13 | const restaurantQuery = { 14 | entity: "restaurant", 15 | fields: [ 16 | "*", 17 | "products.*", 18 | "products.categories.*", 19 | "products.categories.*", 20 | "products.variants.*", 21 | "products.variants.calculated_price.*", 22 | "deliveries.*", 23 | "deliveries.cart.*", 24 | "deliveries.cart.items.*", 25 | "deliveries.order.*", 26 | "deliveries.order.items.*", 27 | ], 28 | filters: { 29 | id: restaurantId, 30 | }, 31 | context: { 32 | products: { 33 | variants: { 34 | calculated_price: QueryContext({ 35 | currency_code, 36 | }), 37 | }, 38 | }, 39 | }, 40 | }; 41 | 42 | const { 43 | data: [restaurant], 44 | } = await query.graph(restaurantQuery); 45 | 46 | return res.status(200).json({ restaurant }); 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/api/store/restaurants/[id]/status/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import zod from "zod"; 3 | import { RESTAURANT_MODULE } from "../../../../../modules/restaurant"; 4 | 5 | const schema = zod.object({ 6 | is_open: zod.boolean(), 7 | }); 8 | 9 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 10 | const { id } = req.params; 11 | const { is_open } = schema.parse(req.body); 12 | 13 | const restaurantService = req.scope.resolve(RESTAURANT_MODULE); 14 | 15 | try { 16 | const restaurant = await restaurantService.updateRestaurants({ 17 | id, 18 | is_open, 19 | }); 20 | res.status(200).json({ restaurant }); 21 | } catch (error) { 22 | res.status(400).json({ message: error.message }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/api/store/restaurants/route.ts: -------------------------------------------------------------------------------- 1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; 2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"; 3 | import zod from "zod"; 4 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations"; 5 | import { createRestaurantWorkflow } from "../../../workflows/restaurant/workflows"; 6 | import { QueryContext } from "@medusajs/framework/utils"; 7 | 8 | const schema = zod.object({ 9 | name: zod.string(), 10 | handle: zod.string(), 11 | address: zod.string(), 12 | phone: zod.string(), 13 | email: zod.string(), 14 | image_url: zod.string().optional(), 15 | }); 16 | 17 | export async function POST(req: MedusaRequest, res: MedusaResponse) { 18 | const validatedBody = schema.parse(req.body) as CreateRestaurantDTO; 19 | 20 | if (!validatedBody) { 21 | return MedusaError.Types.INVALID_DATA; 22 | } 23 | 24 | const { result: restaurant } = await createRestaurantWorkflow(req.scope).run({ 25 | input: { 26 | restaurant: validatedBody, 27 | }, 28 | }); 29 | 30 | return res.status(200).json({ message: "Restaurant created", restaurant }); 31 | } 32 | 33 | export async function GET(req: MedusaRequest, res: MedusaResponse) { 34 | const { currency_code = "eur", ...queryFilters } = req.query; 35 | 36 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); 37 | 38 | const restaurantsQuery = { 39 | entity: "restaurant", 40 | fields: [ 41 | "id", 42 | "handle", 43 | "name", 44 | "address", 45 | "phone", 46 | "email", 47 | "image_url", 48 | "is_open", 49 | "products.*", 50 | "products.categories.*", 51 | "products.variants.*", 52 | "products.variants.calculated_price.*", 53 | ], 54 | filters: queryFilters, 55 | context: { 56 | products: { 57 | variants: { 58 | calculated_price: QueryContext({ 59 | currency_code, 60 | }), 61 | }, 62 | }, 63 | }, 64 | }; 65 | 66 | const { data: restaurants } = await query.graph(restaurantsQuery); 67 | 68 | return res.status(200).json({ restaurants }); 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/api/store/users/me/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticatedMedusaRequest, 3 | MedusaResponse, 4 | } from "@medusajs/framework"; 5 | import { RESTAURANT_MODULE } from "../../../../modules/restaurant"; 6 | import { DELIVERY_MODULE } from "../../../../modules/delivery"; 7 | 8 | export const GET = async ( 9 | req: AuthenticatedMedusaRequest, 10 | res: MedusaResponse 11 | ) => { 12 | const { user_id, actor_type } = req.user as { 13 | user_id: string; 14 | actor_type: "restaurant" | "driver"; 15 | }; 16 | 17 | if (actor_type === "restaurant") { 18 | const service = req.scope.resolve(RESTAURANT_MODULE); 19 | const user = await service.retrieveRestaurantAdmin(user_id); 20 | return res.json({ user }); 21 | } 22 | 23 | if (actor_type === "driver") { 24 | const service = req.scope.resolve(DELIVERY_MODULE); 25 | const user = await service.retrieveDriver(user_id); 26 | return res.json({ user }); 27 | } 28 | 29 | return res.status(404).json({ message: "User not found" }); 30 | }; 31 | -------------------------------------------------------------------------------- /backend/src/api/store/users/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticatedMedusaRequest, 3 | MedusaResponse, 4 | } from "@medusajs/framework"; 5 | import zod from "zod"; 6 | 7 | import { 8 | CreateDriverInput, 9 | createUserWorkflow, 10 | } from "../../../workflows/user/workflows/create-user"; 11 | 12 | const schema = zod 13 | .object({ 14 | email: zod.string().email(), 15 | first_name: zod.string(), 16 | last_name: zod.string(), 17 | phone: zod.string(), 18 | avatar_url: zod.string().optional(), 19 | restaurant_id: zod.string().optional(), 20 | actor_type: zod.ZodEnum.create(["restaurant", "driver"]), 21 | }) 22 | .required({ 23 | email: true, 24 | first_name: true, 25 | last_name: true, 26 | phone: true, 27 | actor_type: true, 28 | }); 29 | 30 | export const POST = async ( 31 | req: AuthenticatedMedusaRequest, 32 | res: MedusaResponse 33 | ) => { 34 | const { auth_identity_id } = req.auth_context; 35 | 36 | const validatedBody = schema.parse(req.body) as CreateDriverInput & { 37 | actor_type: "restaurant" | "driver"; 38 | }; 39 | 40 | const { result, errors } = await createUserWorkflow(req.scope).run({ 41 | input: { 42 | user: validatedBody, 43 | auth_identity_id, 44 | }, 45 | throwOnError: false, 46 | }); 47 | 48 | if (Array.isArray(errors) && errors[0]) { 49 | throw errors[0].error; 50 | } 51 | 52 | res.status(201).json({ user: result }); 53 | }; 54 | -------------------------------------------------------------------------------- /backend/src/api/types.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedMedusaRequest } from "@medusajs/framework"; 2 | 3 | export interface AuthUserScopedMedusaRequest 4 | extends AuthenticatedMedusaRequest { 5 | auth_user_id: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/jobs/README.md: -------------------------------------------------------------------------------- 1 | # Custom scheduled jobs 2 | 3 | You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory. 4 | 5 | ```ts 6 | import { 7 | ProductService, 8 | ScheduledJobArgs, 9 | ScheduledJobConfig, 10 | } from "@medusajs/medusa" 11 | 12 | export default async function myCustomJob({ container }: ScheduledJobArgs) { 13 | const productService: ProductService = container.resolve("productService") 14 | 15 | const products = await productService.listAndCount() 16 | 17 | // Do something with the products 18 | } 19 | 20 | export const config: ScheduledJobConfig = { 21 | name: "daily-product-report", 22 | schedule: "0 0 * * *", // Every day at midnight 23 | } 24 | ``` 25 | 26 | A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object. 27 | 28 | The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties: 29 | 30 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 31 | - `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object. 32 | - `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin. 33 | -------------------------------------------------------------------------------- /backend/src/links/delivery-cart.ts: -------------------------------------------------------------------------------- 1 | import DeliveryModule from "../modules/delivery"; 2 | import CartModule from "@medusajs/cart"; 3 | import { defineLink } from "@medusajs/utils"; 4 | 5 | export default defineLink( 6 | DeliveryModule.linkable.delivery, 7 | CartModule.linkable.cart 8 | ); 9 | -------------------------------------------------------------------------------- /backend/src/links/delivery-order.ts: -------------------------------------------------------------------------------- 1 | import DeliveryModule from "../modules/delivery"; 2 | import OrderModule from "@medusajs/order"; 3 | import { defineLink } from "@medusajs/utils"; 4 | 5 | export default defineLink( 6 | DeliveryModule.linkable.delivery, 7 | OrderModule.linkable.order 8 | ); 9 | -------------------------------------------------------------------------------- /backend/src/links/restaurant-deliveries.ts: -------------------------------------------------------------------------------- 1 | import RestaurantModule from "../modules/restaurant"; 2 | import DeliveryModule from "../modules/delivery"; 3 | import { defineLink } from "@medusajs/utils"; 4 | 5 | export default defineLink( 6 | RestaurantModule.linkable.restaurant, 7 | { 8 | linkable: DeliveryModule.linkable.delivery, 9 | isList: true, 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /backend/src/links/restaurant-products.ts: -------------------------------------------------------------------------------- 1 | import RestaurantModule from "../modules/restaurant"; 2 | import ProductModule from "@medusajs/product"; 3 | import { defineLink } from "@medusajs/utils"; 4 | 5 | export default defineLink( 6 | RestaurantModule.linkable.restaurant, 7 | { 8 | linkable: ProductModule.linkable.product, 9 | isList: true, 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /backend/src/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Custom loader 2 | 3 | A loader is a function executed when the Medusa application starts. 4 | 5 | To create a loader in your Medusa application, create a TypeScript or JavaScript file under the `src/loaders` directory that default exports a function. 6 | 7 | ```ts 8 | export default function () { 9 | console.log( 10 | "[HELLO LOADER] Just started the Medusa application!" 11 | ) 12 | } 13 | ``` 14 | 15 | ## Loader Parameters 16 | 17 | A loader receives the Medusa container as a first parameter, and the Medusa configuration as a second parameter. 18 | 19 | ```ts 20 | import { MedusaContainer } from "@medusajs/medusa" 21 | import { ConfigModule } from "@medusajs/types" 22 | 23 | export default async function ( 24 | container: MedusaContainer, 25 | config: ConfigModule 26 | ) { 27 | console.log(`You have ${ 28 | Object.values(config.modules || {}).length 29 | } modules!`) 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/index.ts: -------------------------------------------------------------------------------- 1 | import Service from "./service"; 2 | import { Module } from "@medusajs/utils"; 3 | 4 | export const DELIVERY_MODULE = "deliveryModuleService"; 5 | 6 | export default Module(DELIVERY_MODULE, { 7 | service: Service, 8 | }); 9 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/migrations/Migration20240822141125.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20240822141125 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('create table if not exists "delivery" ("id" text not null, "transaction_id" text null, "driver_id" text null, "delivery_status" text check ("delivery_status" in (\'pending\', \'restaurant_declined\', \'restaurant_accepted\', \'pickup_claimed\', \'restaurant_preparing\', \'ready_for_pickup\', \'in_transit\', \'delivered\')) not null default \'pending\', "eta" timestamptz null, "delivered_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "delivery_pkey" primary key ("id"));'); 7 | 8 | this.addSql('create table if not exists "driver" ("id" text not null, "first_name" text not null, "last_name" text not null, "email" text not null, "phone" text not null, "avatar_url" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "driver_pkey" primary key ("id"));'); 9 | 10 | this.addSql('create table if not exists "delivery_driver" ("id" text not null, "delivery_id" text not null, "driver_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "delivery_driver_pkey" primary key ("id"));'); 11 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_delivery_driver_delivery_id" ON "delivery_driver" (delivery_id) WHERE deleted_at IS NULL;'); 12 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_delivery_driver_driver_id" ON "delivery_driver" (driver_id) WHERE deleted_at IS NULL;'); 13 | 14 | this.addSql('alter table if exists "delivery_driver" add constraint "delivery_driver_delivery_id_foreign" foreign key ("delivery_id") references "delivery" ("id") on update cascade;'); 15 | this.addSql('alter table if exists "delivery_driver" add constraint "delivery_driver_driver_id_foreign" foreign key ("driver_id") references "driver" ("id") on update cascade;'); 16 | } 17 | 18 | async down(): Promise { 19 | this.addSql('alter table if exists "delivery_driver" drop constraint if exists "delivery_driver_delivery_id_foreign";'); 20 | 21 | this.addSql('alter table if exists "delivery_driver" drop constraint if exists "delivery_driver_driver_id_foreign";'); 22 | 23 | this.addSql('drop table if exists "delivery" cascade;'); 24 | 25 | this.addSql('drop table if exists "driver" cascade;'); 26 | 27 | this.addSql('drop table if exists "delivery_driver" cascade;'); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/models/delivery-driver.ts: -------------------------------------------------------------------------------- 1 | import { model } from "@medusajs/utils"; 2 | import { Delivery } from "./delivery"; 3 | import { Driver } from "./driver"; 4 | 5 | export const DeliveryDriver = model.define("delivery_driver", { 6 | id: model 7 | .id({ 8 | prefix: "deldrv", 9 | }) 10 | .primaryKey(), 11 | delivery: model.belongsTo(() => Delivery, { 12 | mappedBy: "deliveryDriver" 13 | }), 14 | driver: model.belongsTo(() => Driver, { 15 | mappedBy: "deliveryDriver" 16 | }) 17 | }); 18 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/models/delivery.ts: -------------------------------------------------------------------------------- 1 | import { model } from "@medusajs/utils"; 2 | import { DeliveryStatus } from "../types/common"; 3 | import { DeliveryDriver } from "./delivery-driver"; 4 | 5 | export const Delivery = model.define("delivery", { 6 | id: model 7 | .id({ 8 | prefix: "del", 9 | }) 10 | .primaryKey(), 11 | transaction_id: model.text().nullable(), 12 | driver_id: model.text().nullable(), 13 | delivery_status: model.enum(DeliveryStatus).default(DeliveryStatus.PENDING), 14 | eta: model.dateTime().nullable(), 15 | delivered_at: model.dateTime().nullable(), 16 | deliveryDriver: model.hasMany(() => DeliveryDriver, { 17 | mappedBy: "delivery" 18 | }) 19 | }); 20 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/models/driver.ts: -------------------------------------------------------------------------------- 1 | import { model } from "@medusajs/utils"; 2 | import { DeliveryDriver } from "./delivery-driver"; 3 | 4 | export const Driver = model.define("driver", { 5 | id: model 6 | .id({ 7 | prefix: "drv", 8 | }) 9 | .primaryKey(), 10 | first_name: model.text(), 11 | last_name: model.text(), 12 | email: model.text(), 13 | phone: model.text(), 14 | avatar_url: model.text().nullable(), 15 | deliveryDriver: model.hasMany(() => DeliveryDriver, { 16 | mappedBy: "driver" 17 | }) 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/models/index.ts: -------------------------------------------------------------------------------- 1 | export { Delivery } from "./delivery"; 2 | export { Driver } from "./driver"; 3 | export { DeliveryDriver } from "./delivery-driver"; 4 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/service.ts: -------------------------------------------------------------------------------- 1 | import { MedusaService } from "@medusajs/utils"; 2 | import { Delivery, DeliveryDriver, Driver } from "./models"; 3 | 4 | class DeliveryModuleService extends MedusaService({ 5 | Delivery, 6 | Driver, 7 | DeliveryDriver, 8 | }) {} 9 | 10 | export default DeliveryModuleService; 11 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/types/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CartLineItemDTO, 3 | OrderLineItemDTO, 4 | CartDTO, 5 | OrderDTO, 6 | } from "@medusajs/types"; 7 | import { RestaurantDTO } from "../../restaurant/types/common"; 8 | import DeliveryModuleService from "../service"; 9 | 10 | export enum DeliveryStatus { 11 | PENDING = "pending", 12 | RESTAURANT_DECLINED = "restaurant_declined", 13 | RESTAURANT_ACCEPTED = "restaurant_accepted", 14 | PICKUP_CLAIMED = "pickup_claimed", 15 | RESTAURANT_PREPARING = "restaurant_preparing", 16 | READY_FOR_PICKUP = "ready_for_pickup", 17 | IN_TRANSIT = "in_transit", 18 | DELIVERED = "delivered", 19 | } 20 | 21 | export interface DeliveryDTO { 22 | id: string; 23 | transaction_id: string; 24 | driver_id?: string; 25 | delivered_at?: Date; 26 | delivery_status: DeliveryStatus; 27 | created_at: Date; 28 | updated_at: Date; 29 | eta?: Date; 30 | items: DeliveryItemDTO[]; 31 | cart?: CartDTO; 32 | order?: OrderDTO; 33 | restaurant: RestaurantDTO; 34 | } 35 | 36 | export type DeliveryItemDTO = (CartLineItemDTO | OrderLineItemDTO) & { 37 | quantity: number; 38 | }; 39 | 40 | export interface DriverDTO { 41 | id: string; 42 | first_name: string; 43 | last_name: string; 44 | email: string; 45 | phone: string; 46 | avatar_url?: string; 47 | created_at: Date; 48 | updated_at: Date; 49 | } 50 | 51 | export interface DeliveryDriverDTO { 52 | id: string; 53 | delivery_id: string; 54 | driver_id: string; 55 | } 56 | 57 | declare module "@medusajs/types" { 58 | export interface ModuleImplementations { 59 | deliveryModuleService: DeliveryModuleService; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/modules/delivery/types/mutations.ts: -------------------------------------------------------------------------------- 1 | import { DeliveryDTO, DriverDTO } from "./common"; 2 | 3 | export interface CreateDeliveryDTO { 4 | cart_id: string; 5 | } 6 | 7 | export interface UpdateDeliveryDTO extends Partial { 8 | id: string; 9 | } 10 | 11 | export interface CreateDriverDTO { 12 | first_name: string; 13 | last_name: string; 14 | email: string; 15 | phone: string; 16 | avatar_url?: string; 17 | } 18 | 19 | export interface UpdateDriverDTO extends Partial { 20 | id: string; 21 | } 22 | 23 | export interface CreateDeliveryDriverDTO { 24 | delivery_id: string; 25 | driver_id: string; 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/index.ts: -------------------------------------------------------------------------------- 1 | import Service from "./service"; 2 | import { Module } from "@medusajs/utils"; 3 | 4 | export const RESTAURANT_MODULE = "restaurantModuleService"; 5 | 6 | export default Module(RESTAURANT_MODULE, { 7 | service: Service, 8 | }); 9 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/migrations/Migration20240925143313.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20240925143313 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('create table if not exists "restaurant" ("id" text not null, "handle" text not null, "is_open" boolean not null default false, "name" text not null, "description" text null, "phone" text not null, "email" text not null, "address" text not null, "image_url" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "restaurant_pkey" primary key ("id"));'); 7 | 8 | this.addSql('create table if not exists "restaurant_admin" ("id" text not null, "first_name" text not null, "last_name" text not null, "email" text not null, "avatar_url" text null, "restaurant_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "restaurant_admin_pkey" primary key ("id"));'); 9 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_restaurant_admin_restaurant_id" ON "restaurant_admin" (restaurant_id) WHERE deleted_at IS NULL;'); 10 | 11 | this.addSql('alter table if exists "restaurant_admin" add constraint "restaurant_admin_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurant" ("id") on update cascade;'); 12 | } 13 | 14 | async down(): Promise { 15 | this.addSql('alter table if exists "restaurant_admin" drop constraint if exists "restaurant_admin_restaurant_id_foreign";'); 16 | 17 | this.addSql('drop table if exists "restaurant" cascade;'); 18 | 19 | this.addSql('drop table if exists "restaurant_admin" cascade;'); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/models/index.ts: -------------------------------------------------------------------------------- 1 | export { Restaurant } from "./restaurant"; 2 | export { RestaurantAdmin } from "./restaurant-admin"; 3 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/models/restaurant-admin.ts: -------------------------------------------------------------------------------- 1 | import { model } from "@medusajs/utils"; 2 | import { Restaurant } from "./restaurant"; 3 | 4 | export const RestaurantAdmin = model.define("restaurant_admin", { 5 | id: model 6 | .id({ 7 | prefix: "resadm", 8 | }) 9 | .primaryKey(), 10 | first_name: model.text(), 11 | last_name: model.text(), 12 | email: model.text(), 13 | avatar_url: model.text().nullable(), 14 | restaurant: model.belongsTo(() => Restaurant, { 15 | mappedBy: "admins", 16 | }), 17 | }); 18 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/models/restaurant.ts: -------------------------------------------------------------------------------- 1 | import { model } from "@medusajs/utils"; 2 | import { RestaurantAdmin } from "./restaurant-admin"; 3 | 4 | export const Restaurant = model.define("restaurant", { 5 | id: model 6 | .id({ 7 | prefix: "res", 8 | }) 9 | .primaryKey(), 10 | handle: model.text(), 11 | is_open: model.boolean().default(false), 12 | name: model.text(), 13 | description: model.text().nullable(), 14 | phone: model.text(), 15 | email: model.text(), 16 | address: model.text(), 17 | image_url: model.text().nullable(), 18 | admins: model.hasMany(() => RestaurantAdmin), 19 | }); 20 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/service.ts: -------------------------------------------------------------------------------- 1 | import { MedusaService } from "@medusajs/utils"; 2 | import { Restaurant, RestaurantAdmin } from "./models"; 3 | 4 | class RestaurantModuleService extends MedusaService({ 5 | Restaurant, 6 | RestaurantAdmin, 7 | }) {} 8 | 9 | export default RestaurantModuleService; 10 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/types/common.ts: -------------------------------------------------------------------------------- 1 | import { ProductDTO } from "@medusajs/types"; 2 | import RestaurantModuleService from "../service"; 3 | 4 | export interface RestaurantDTO { 5 | id: string; 6 | handle: string; 7 | is_open: boolean; 8 | name: string; 9 | description?: string; 10 | address: string; 11 | phone: string; 12 | email: string; 13 | image_url?: string; 14 | created_at: Date; 15 | updated_at: Date; 16 | products?: ProductDTO[]; 17 | } 18 | 19 | export interface RestaurantAdminDTO { 20 | id: string; 21 | restaurant: RestaurantDTO; 22 | first_name: string; 23 | last_name: string; 24 | email: string; 25 | created_at: Date; 26 | updated_at: Date; 27 | } 28 | 29 | export interface RestaurantProductDTO { 30 | restaurant_id: string; 31 | product_id: string; 32 | } 33 | 34 | declare module "@medusajs/types" { 35 | export interface ModuleImplementations { 36 | restaurantModuleService: RestaurantModuleService; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/modules/restaurant/types/mutations.ts: -------------------------------------------------------------------------------- 1 | export interface CreateRestaurantDTO { 2 | name: string; 3 | handle: string; 4 | address: string; 5 | phone: string; 6 | email: string; 7 | image_url?: string; 8 | is_open?: boolean; 9 | } 10 | 11 | export interface UpdateRestaurantsDTO extends Partial { 12 | id: string; 13 | } 14 | 15 | export interface CreateRestaurantAdminDTO { 16 | email: string; 17 | first_name: string; 18 | last_name: string; 19 | restaurant_id: string; 20 | } 21 | 22 | export interface UpdateRestaurantAdminsDTO 23 | extends Partial { 24 | id: string; 25 | } 26 | 27 | export interface CreateAdminInviteDTO { 28 | resadm_id: string; 29 | role?: string | null; 30 | email?: string; 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. 4 | 5 | ```ts 6 | import MyCustomService from "../services/my-custom" 7 | import { 8 | OrderService, 9 | SubscriberArgs, 10 | SubscriberConfig, 11 | } from "@medusajs/medusa" 12 | 13 | type OrderPlacedEvent = { 14 | id: string 15 | no_notification: boolean 16 | } 17 | 18 | export default async function orderPlacedHandler({ 19 | data, 20 | eventName, 21 | container, 22 | }: SubscriberArgs) { 23 | const orderService: OrderService = container.resolve(OrderService) 24 | 25 | const order = await orderService.retrieve(data.id, { 26 | relations: ["items", "items.variant", "items.variant.product"], 27 | }) 28 | 29 | // Do something with the order 30 | } 31 | 32 | export const config: SubscriberConfig = { 33 | event: OrderService.Events.PLACED, 34 | } 35 | ``` 36 | 37 | A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to. 38 | 39 | The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs` with the following properties: 40 | 41 | - `data` - an `object` of type `T` containing information about the event. 42 | - `eventName` - a `string` containing the name of the event. 43 | - `container` - a `MedusaContainer` instance which can be used to resolve services. 44 | - `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin. 45 | -------------------------------------------------------------------------------- /backend/src/utils/create-variant-price-set.ts: -------------------------------------------------------------------------------- 1 | import { MedusaContainer } from "@medusajs/framework"; 2 | import { 3 | CreatePriceSetDTO, 4 | IPricingModuleService, 5 | PriceSetDTO, 6 | } from "@medusajs/types"; 7 | import { Modules, remoteQueryObjectFromString } from "@medusajs/utils"; 8 | 9 | export const createVariantPriceSet = async ({ 10 | container, 11 | variantId, 12 | prices, 13 | }: { 14 | container: MedusaContainer; 15 | variantId: string; 16 | prices: CreatePriceSetDTO["prices"]; 17 | }): Promise => { 18 | const remoteLink = container.resolve("remoteLink"); 19 | const remoteQuery = container.resolve("remoteQuery"); 20 | const pricingModuleService: IPricingModuleService = container.resolve( 21 | "pricingModuleService" 22 | ); 23 | 24 | const priceSet = await pricingModuleService.createPriceSets({ 25 | prices, 26 | }); 27 | 28 | await remoteLink.create({ 29 | [Modules.PRODUCT]: { variant_id: variantId }, 30 | [Modules.PRICING]: { price_set_id: priceSet.id }, 31 | }); 32 | 33 | const pricingQuery = remoteQueryObjectFromString({ 34 | entryPoint: "price", 35 | fields: ["*"], 36 | filters: { 37 | id: priceSet.id, 38 | }, 39 | }); 40 | 41 | return await remoteQuery(pricingQuery); 42 | }; 43 | -------------------------------------------------------------------------------- /backend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-variant-price-set" -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/await-delivery.ts: -------------------------------------------------------------------------------- 1 | import { createStep } from "@medusajs/workflows-sdk"; 2 | 3 | export const awaitDeliveryStepId = "await-delivery-step"; 4 | export const awaitDeliveryStep = createStep( 5 | { name: awaitDeliveryStepId, async: true, timeout: 60 * 15 }, 6 | async function (_, { container }) { 7 | const logger = container.resolve("logger"); 8 | logger.info("Awaiting delivery by driver..."); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/await-pick-up.ts: -------------------------------------------------------------------------------- 1 | import { createStep } from "@medusajs/workflows-sdk"; 2 | 3 | export const awaitPickUpStepId = "await-pick-up-step"; 4 | export const awaitPickUpStep = createStep( 5 | { name: awaitPickUpStepId, async: true, timeout: 60 * 15 }, 6 | async function (_, { container }) { 7 | const logger = container.resolve("logger"); 8 | logger.info("Awaiting pick up by driver..."); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/await-preparation.ts: -------------------------------------------------------------------------------- 1 | import { createStep } from "@medusajs/workflows-sdk"; 2 | 3 | export const awaitPreparationStepId = "await-preparation-step"; 4 | export const awaitPreparationStep = createStep( 5 | { name: awaitPreparationStepId, async: true, timeout: 60 * 15 }, 6 | async function (_, { container }) { 7 | const logger = container.resolve("logger"); 8 | logger.info("Awaiting preparation..."); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/await-start-preparation.ts: -------------------------------------------------------------------------------- 1 | import { createStep } from "@medusajs/workflows-sdk"; 2 | 3 | export const awaitStartPreparationStepId = "await-start-preparation-step"; 4 | export const awaitStartPreparationStep = createStep( 5 | { name: awaitStartPreparationStepId, async: true, timeout: 60 * 15 }, 6 | async function (_, { container }) { 7 | const logger = container.resolve("logger"); 8 | logger.info("Awaiting start of preparation..."); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/create-delivery.ts: -------------------------------------------------------------------------------- 1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 2 | import { DeliveryDTO } from "../../../modules/delivery/types/common"; 3 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 4 | 5 | export const createDeliveryStepId = "create-delivery-step"; 6 | export const createDeliveryStep = createStep( 7 | createDeliveryStepId, 8 | async function ({}, { container }) { 9 | const service = container.resolve(DELIVERY_MODULE); 10 | 11 | const delivery = await service.createDeliveries({}) as DeliveryDTO 12 | 13 | return new StepResponse(delivery, { 14 | delivery_id: delivery.id, 15 | }); 16 | }, 17 | async function ( 18 | { 19 | delivery_id, 20 | }, 21 | { container } 22 | ) { 23 | const service = container.resolve(DELIVERY_MODULE); 24 | 25 | service.softDeleteDeliveries(delivery_id); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/create-fulfillment.ts: -------------------------------------------------------------------------------- 1 | import { OrderDTO } from "@medusajs/types"; 2 | import { ModuleRegistrationName } from "@medusajs/utils"; 3 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 4 | 5 | export const createFulfillmentStepId = "create-fulfillment-step"; 6 | export const createFulfillmentStep = createStep( 7 | createFulfillmentStepId, 8 | async function (order: OrderDTO, { container }) { 9 | const fulfillmentModuleService = container.resolve( 10 | ModuleRegistrationName.FULFILLMENT 11 | ); 12 | 13 | const items = order.items?.map((lineItem) => { 14 | return { 15 | title: lineItem.title, 16 | sku: lineItem.variant_sku || "", 17 | quantity: lineItem.quantity, 18 | barcode: lineItem.variant_barcode || "", 19 | line_item_id: lineItem.id, 20 | }; 21 | }); 22 | 23 | const fulfillment = await fulfillmentModuleService.createFulfillment({ 24 | provider_id: "manual_manual", 25 | location_id: "1", 26 | delivery_address: order.shipping_address!, 27 | items: items || [], 28 | labels: [], 29 | order, 30 | }); 31 | 32 | return new StepResponse(fulfillment, fulfillment.id); 33 | }, 34 | function (input: string, { container }) { 35 | const fulfillmentModuleService = container.resolve( 36 | ModuleRegistrationName.FULFILLMENT 37 | ); 38 | 39 | return fulfillmentModuleService.softDeleteFulfillmentSets([input]); 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/create-order.ts: -------------------------------------------------------------------------------- 1 | import { CreateOrderShippingMethodDTO } from "@medusajs/types"; 2 | import { 3 | ModuleRegistrationName, 4 | Modules, 5 | ContainerRegistrationKeys, 6 | } from "@medusajs/utils"; 7 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 8 | 9 | export const createOrderStepId = "create-order-step"; 10 | export const createOrderStep = createStep( 11 | createOrderStepId, 12 | async function (deliveryId: string, { container }) { 13 | const query = container.resolve(ContainerRegistrationKeys.QUERY); 14 | 15 | const deliveryQuery = { 16 | entity: "delivery", 17 | filters: { 18 | id: deliveryId, 19 | }, 20 | fields: ["id", "cart.id", "delivery_status", "driver_id"], 21 | }; 22 | 23 | const { 24 | data: [delivery], 25 | } = await query.graph(deliveryQuery); 26 | 27 | const cartQuery = { 28 | entity: "cart", 29 | fields: ["*", "items.*"], 30 | filters: { 31 | id: delivery.cart.id, 32 | }, 33 | }; 34 | 35 | const { 36 | data: [cart], 37 | } = await query.graph(cartQuery); 38 | 39 | const orderModuleService = container.resolve(ModuleRegistrationName.ORDER); 40 | 41 | const order = await orderModuleService.createOrders({ 42 | currency_code: cart.currency_code, 43 | email: cart.email, 44 | shipping_address: cart.shipping_address, 45 | billing_address: cart.billing_address, 46 | items: cart.items, 47 | region_id: cart.region_id, 48 | customer_id: cart.customer_id, 49 | sales_channel_id: cart.sales_channel_id, 50 | shipping_methods: 51 | cart.shipping_methods as unknown as CreateOrderShippingMethodDTO[], 52 | }); 53 | 54 | const remoteLink = container.resolve("remoteLink"); 55 | 56 | await remoteLink.create({ 57 | deliveryModuleService: { 58 | delivery_id: delivery.id, 59 | }, 60 | [Modules.ORDER]: { 61 | order_id: order.id, 62 | }, 63 | }); 64 | 65 | return new StepResponse(order, { 66 | orderId: order.id, 67 | deliveryId, 68 | }); 69 | }, 70 | async ( 71 | { 72 | orderId, 73 | deliveryId, 74 | }: { 75 | orderId: string; 76 | deliveryId: string; 77 | }, 78 | { container } 79 | ) => { 80 | const remoteLink = container.resolve("remoteLink"); 81 | 82 | await remoteLink.dismiss({ 83 | deliveryModuleService: { 84 | delivery_id: deliveryId, 85 | }, 86 | [Modules.ORDER]: { 87 | order_id: orderId, 88 | }, 89 | }); 90 | 91 | const orderService = container.resolve(ModuleRegistrationName.ORDER); 92 | 93 | await orderService.softDeleteOrders([orderId]); 94 | } 95 | ); 96 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/delete-delivery-drivers.ts: -------------------------------------------------------------------------------- 1 | import { remoteQueryObjectFromString } from "@medusajs/utils"; 2 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 3 | import { DriverDTO } from "../../../modules/delivery/types/common"; 4 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 5 | 6 | export type DeleteDeliveryDriversStepInput = { 7 | delivery_id: string; 8 | driver_id?: string; 9 | }; 10 | 11 | export const deleteDeliveryDriversStepId = "delete-delivery-drivers-step"; 12 | export const deleteDeliveryDriversStep = createStep( 13 | deleteDeliveryDriversStepId, 14 | async function (input: DeleteDeliveryDriversStepInput, { container }) { 15 | const remoteQuery = container.resolve("remoteQuery"); 16 | 17 | const driverQuery = remoteQueryObjectFromString({ 18 | entryPoint: "delivery_driver", 19 | filters: { 20 | id: input.driver_id, 21 | }, 22 | fields: ["id"], 23 | }); 24 | 25 | const drivers = await remoteQuery(driverQuery) 26 | .then((res) => res.map((d: DriverDTO) => d.id)) 27 | .catch(() => []); 28 | 29 | const deliveryModuleService = container.resolve(DELIVERY_MODULE); 30 | 31 | await deliveryModuleService.softDeleteDeliveryDrivers(drivers); 32 | 33 | return new StepResponse(drivers, drivers); 34 | }, 35 | (deletedDrivers: string[], { container }) => {} 36 | ); 37 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/find-driver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleRegistrationName, 3 | remoteQueryObjectFromString, 4 | } from "@medusajs/utils"; 5 | import { createStep } from "@medusajs/workflows-sdk"; 6 | import { DriverDTO } from "../../../modules/delivery/types/common"; 7 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 8 | 9 | export const findDriverStepId = "await-driver-response-step"; 10 | export const findDriverStep = createStep( 11 | { name: findDriverStepId, async: true, timeout: 60 * 15, maxRetries: 2 }, 12 | async function (deliveryId: string, { container }) { 13 | const remoteQuery = container.resolve("remoteQuery"); 14 | 15 | const driversQuery = remoteQueryObjectFromString({ 16 | entryPoint: "drivers", 17 | fields: ["id"], 18 | pagination: { 19 | skip: 0, 20 | take: null, 21 | }, 22 | }); 23 | 24 | const { rows: drivers } = await remoteQuery(driversQuery).catch((e) => []); 25 | 26 | const idsToNotify = drivers.map((d: DriverDTO) => d.id); 27 | 28 | const createData = idsToNotify.map((driverId: string) => ({ 29 | delivery_id: deliveryId, 30 | driver_id: driverId, 31 | })); 32 | 33 | const deliveryService = container.resolve(DELIVERY_MODULE); 34 | 35 | await deliveryService.createDeliveryDrivers(createData); 36 | 37 | const eventBus = container.resolve(ModuleRegistrationName.EVENT_BUS); 38 | 39 | await eventBus.emit({ 40 | name: "notify.drivers", 41 | data: { 42 | drivers: idsToNotify, 43 | delivery_id: deliveryId, 44 | }, 45 | }); 46 | }, 47 | (input, { container }) => { 48 | const deliveryService = container.resolve(DELIVERY_MODULE); 49 | 50 | return deliveryService.softDeleteDeliveryDrivers(input); 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./await-delivery"; 2 | export * from "./await-pick-up"; 3 | export * from "./await-preparation"; 4 | export * from "./await-start-preparation"; 5 | export * from "./create-delivery"; 6 | export * from "./create-fulfillment"; 7 | export * from "./create-order"; 8 | export { 9 | DeleteDeliveryDriversStepInput as CreateDeliveryStepInput, 10 | deleteDeliveryDriversStep, 11 | deleteDeliveryDriversStepId, 12 | } from "./delete-delivery-drivers"; 13 | export * from "./find-driver"; 14 | export * from "./notify-restaurant"; 15 | export * from "./set-transaction-id"; 16 | export * from "./update-delivery"; 17 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/notify-restaurant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleRegistrationName, 3 | remoteQueryObjectFromString, 4 | } from "@medusajs/utils"; 5 | import { createStep } from "@medusajs/workflows-sdk"; 6 | 7 | export const notifyRestaurantStepId = "notify-restaurant-step"; 8 | export const notifyRestaurantStep = createStep( 9 | { 10 | name: notifyRestaurantStepId, 11 | async: true, 12 | timeout: 60 * 15, 13 | maxRetries: 2, 14 | }, 15 | async function (deliveryId: string, { container }) { 16 | const remoteQuery = container.resolve("remoteQuery"); 17 | 18 | const deliveryQuery = remoteQueryObjectFromString({ 19 | entryPoint: "deliveries", 20 | filters: { 21 | id: deliveryId, 22 | }, 23 | fields: ["id", "restaurant.id"], 24 | }); 25 | 26 | const delivery = await remoteQuery(deliveryQuery).then((res) => res[0]); 27 | 28 | const eventBus = container.resolve(ModuleRegistrationName.EVENT_BUS); 29 | 30 | await eventBus.emit({ 31 | name: "notify.restaurant", 32 | data: { 33 | restaurant_id: delivery.restaurant.id, 34 | delivery_id: delivery.id, 35 | }, 36 | }); 37 | }, 38 | function (input: string, { container }) { 39 | const logger = container.resolve("logger"); 40 | 41 | logger.error("Failed to notify restaurant", { input }); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/set-transaction-id.ts: -------------------------------------------------------------------------------- 1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 2 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 3 | 4 | export type SetTransactionIdStepInput = { 5 | delivery_id: string; 6 | }; 7 | 8 | export const setTransactionIdStepId = "create-delivery-step"; 9 | export const setTransactionIdStep = createStep( 10 | setTransactionIdStepId, 11 | async function (deliveryId: string, { container, context }) { 12 | const service = container.resolve(DELIVERY_MODULE); 13 | 14 | const delivery = await service.updateDeliveries({ 15 | id: deliveryId, 16 | transaction_id: context.transactionId, 17 | }); 18 | 19 | return new StepResponse(delivery, delivery.id); 20 | }, 21 | async function (delivery_id: string, { container }) { 22 | const service = container.resolve(DELIVERY_MODULE); 23 | 24 | const delivery = await service.updateDeliveries({ 25 | id: delivery_id, 26 | transaction_id: null, 27 | }); 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/steps/update-delivery.ts: -------------------------------------------------------------------------------- 1 | import { createStep, StepResponse } from "@medusajs/workflows-sdk"; 2 | import { UpdateDeliveryDTO } from "../../../modules/delivery/types/mutations"; 3 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 4 | 5 | type UpdateDeliveryStepInput = { 6 | data: UpdateDeliveryDTO; 7 | }; 8 | 9 | export const updateDeliveryStepId = "update-delivery-step"; 10 | export const updateDeliveryStep = createStep( 11 | updateDeliveryStepId, 12 | async function (input: UpdateDeliveryStepInput, { container }) { 13 | const deliveryService = container.resolve(DELIVERY_MODULE); 14 | 15 | const delivery = await deliveryService 16 | .updateDeliveries([input.data]) 17 | .then((res) => res[0]); 18 | 19 | return new StepResponse(delivery, delivery.id); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/claim-delivery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | WorkflowData, 4 | WorkflowResponse, 5 | } from "@medusajs/workflows-sdk"; 6 | import { DeliveryStatus } from "../../../modules/delivery/types/common"; 7 | import { setStepSuccessStep } from "../../util/steps"; 8 | import { deleteDeliveryDriversStep, updateDeliveryStep } from "../steps"; 9 | import { findDriverStepId } from "../steps/find-driver"; 10 | 11 | export type ClaimWorkflowInput = { 12 | driver_id: string; 13 | delivery_id: string; 14 | }; 15 | 16 | export const claimDeliveryWorkflow = createWorkflow( 17 | "claim-delivery-workflow", 18 | function (input: WorkflowData) { 19 | // Update the delivery with the provided data 20 | const claimedDelivery = updateDeliveryStep({ 21 | data: { 22 | id: input.delivery_id, 23 | driver_id: input.driver_id, 24 | delivery_status: DeliveryStatus.PICKUP_CLAIMED, 25 | }, 26 | }); 27 | 28 | // Delete the delivery drivers as they are no longer needed 29 | deleteDeliveryDriversStep({ delivery_id: claimedDelivery.id }); 30 | 31 | // Set the step success for the find driver step 32 | setStepSuccessStep({ 33 | stepId: findDriverStepId, 34 | updatedDelivery: claimedDelivery, 35 | }); 36 | 37 | // Return the updated delivery 38 | return new WorkflowResponse(claimedDelivery); 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/create-delivery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkflowData, 3 | WorkflowResponse, 4 | createWorkflow, 5 | transform, 6 | } from "@medusajs/workflows-sdk"; 7 | import { Modules } from "@medusajs/utils" 8 | import { createRemoteLinkStep } from "@medusajs/core-flows" 9 | import { DeliveryDTO } from "../../../modules/delivery/types/common"; 10 | import { createDeliveryStep } from "../../delivery/steps"; 11 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 12 | import { RESTAURANT_MODULE } from "../../../modules/restaurant"; 13 | 14 | type WorkflowInput = { 15 | cart_id: string; 16 | restaurant_id: string; 17 | }; 18 | 19 | export const createDeliveryWorkflowId = "create-delivery-workflow"; 20 | export const createDeliveryWorkflow = createWorkflow( 21 | createDeliveryWorkflowId, 22 | function (input: WorkflowData): WorkflowResponse { 23 | const delivery = createDeliveryStep(); 24 | 25 | const links = transform({ 26 | input, 27 | delivery 28 | }, (data) => ([ 29 | { 30 | [DELIVERY_MODULE]: { 31 | delivery_id: data.delivery.id 32 | }, 33 | [Modules.CART]: { 34 | cart_id: data.input.cart_id 35 | } 36 | }, 37 | { 38 | [RESTAURANT_MODULE]: { 39 | restaurant_id: data.input.restaurant_id 40 | }, 41 | [DELIVERY_MODULE]: { 42 | delivery_id: data.delivery.id 43 | } 44 | } 45 | ])) 46 | 47 | createRemoteLinkStep(links) 48 | 49 | return new WorkflowResponse(delivery); 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/handle-delivery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkflowData, 3 | WorkflowResponse, 4 | createWorkflow, 5 | } from "@medusajs/workflows-sdk"; 6 | import { 7 | awaitDeliveryStep, 8 | awaitPickUpStep, 9 | awaitPreparationStep, 10 | awaitStartPreparationStep, 11 | createFulfillmentStep, 12 | createOrderStep, 13 | findDriverStep, 14 | notifyRestaurantStep, 15 | setTransactionIdStep, 16 | } from "../../delivery/steps"; 17 | 18 | type WorkflowInput = { 19 | delivery_id: string; 20 | }; 21 | 22 | const TWO_HOURS = 60 * 60 * 2; 23 | export const handleDeliveryWorkflowId = "handle-delivery-workflow"; 24 | export const handleDeliveryWorkflow = createWorkflow( 25 | { 26 | name: handleDeliveryWorkflowId, 27 | store: true, 28 | retentionTime: TWO_HOURS, 29 | }, 30 | function (input: WorkflowData): WorkflowResponse { 31 | setTransactionIdStep(input.delivery_id); 32 | 33 | notifyRestaurantStep(input.delivery_id); 34 | 35 | findDriverStep(input.delivery_id); 36 | 37 | const order = createOrderStep(input.delivery_id); 38 | 39 | awaitStartPreparationStep(); 40 | 41 | awaitPreparationStep(); 42 | 43 | createFulfillmentStep(order); 44 | 45 | awaitPickUpStep(); 46 | 47 | awaitDeliveryStep(); 48 | 49 | return new WorkflowResponse("Delivery completed"); 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./claim-delivery"; 2 | export * from "./create-delivery"; 3 | export * from "./handle-delivery"; 4 | export * from "./pass-delivery"; 5 | export { WorkflowInput, updateDeliveryWorkflow } from "./update-delivery"; 6 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/pass-delivery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | WorkflowData, 4 | WorkflowResponse, 5 | } from "@medusajs/workflows-sdk"; 6 | import { DeliveryDTO } from "../../../modules/delivery/types/common"; 7 | import { deleteDeliveryDriversStep } from "../../delivery/steps"; 8 | 9 | export type WorkflowInput = { 10 | driver_id: string; 11 | delivery_id: string; 12 | }; 13 | 14 | export const passDeliveryWorkflow = createWorkflow( 15 | "pass-delivery-workflow", 16 | function (input: WorkflowData) { 17 | // Delete the delivery drivers as they are no longer needed 18 | deleteDeliveryDriversStep({ 19 | delivery_id: input.delivery_id, 20 | driver_id: input.driver_id, 21 | }); 22 | 23 | return new WorkflowResponse({} as DeliveryDTO); 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /backend/src/workflows/delivery/workflows/update-delivery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | WorkflowData, 4 | WorkflowResponse, 5 | } from "@medusajs/workflows-sdk"; 6 | import { UpdateDeliveryDTO } from "../../../modules/delivery/types/mutations"; 7 | import { updateDeliveryStep } from "../../delivery/steps"; 8 | import { setStepFailedStep, setStepSuccessStep } from "../../util/steps"; 9 | 10 | export type WorkflowInput = { 11 | data: UpdateDeliveryDTO; 12 | stepIdToSucceed?: string; 13 | stepIdToFail?: string; 14 | }; 15 | 16 | export const updateDeliveryWorkflow = createWorkflow( 17 | "update-delivery-workflow", 18 | function (input: WorkflowData) { 19 | // Update the delivery with the provided data 20 | const updatedDelivery = updateDeliveryStep({ 21 | data: input.data, 22 | }); 23 | 24 | // If a stepIdToSucceed is provided, we will set that step as successful 25 | setStepSuccessStep({ 26 | stepId: input.stepIdToSucceed, 27 | updatedDelivery, 28 | }); 29 | 30 | // If a stepIdToFail is provided, we will set that step as failed 31 | setStepFailedStep({ 32 | stepId: input.stepIdToFail, 33 | updatedDelivery, 34 | }); 35 | 36 | // Return the updated delivery 37 | return new WorkflowResponse(updatedDelivery); 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /backend/src/workflows/restaurant/steps/create-restaurant.ts: -------------------------------------------------------------------------------- 1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 2 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations"; 3 | import { RESTAURANT_MODULE } from "../../../modules/restaurant"; 4 | 5 | export const createRestaurantStepId = "create-restaurant-step"; 6 | export const createRestaurantStep = createStep( 7 | createRestaurantStepId, 8 | async function (data: CreateRestaurantDTO, { container }) { 9 | const restaurantModuleService = container.resolve( 10 | RESTAURANT_MODULE 11 | ); 12 | 13 | const restaurant = await restaurantModuleService.createRestaurants(data); 14 | 15 | return new StepResponse(restaurant, restaurant.id); 16 | }, 17 | function (input: string, { container }) { 18 | const restaurantModuleService = container.resolve( 19 | RESTAURANT_MODULE 20 | ); 21 | 22 | return restaurantModuleService.deleteRestaurants([input]); 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /backend/src/workflows/restaurant/steps/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-restaurant"; -------------------------------------------------------------------------------- /backend/src/workflows/restaurant/workflows/create-restaurant-products.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createProductsWorkflow, 3 | createRemoteLinkStep 4 | } from "@medusajs/core-flows"; 5 | import { CreateProductDTO } from "@medusajs/types"; 6 | import { Modules } from "@medusajs/utils" 7 | import { 8 | WorkflowData, 9 | WorkflowResponse, 10 | createWorkflow, 11 | transform, 12 | } from "@medusajs/workflows-sdk"; 13 | import { RESTAURANT_MODULE } from "../../../modules/restaurant"; 14 | 15 | type WorkflowInput = { 16 | products: CreateProductDTO[]; 17 | restaurant_id: string; 18 | }; 19 | 20 | export const createRestaurantProductsWorkflow = createWorkflow( 21 | "create-restaurant-products-workflow", 22 | function (input: WorkflowData) { 23 | const products = createProductsWorkflow.runAsStep({ 24 | input: { 25 | products: input.products, 26 | }, 27 | }); 28 | 29 | const links = transform({ 30 | products, 31 | input 32 | }, (data) => data.products.map((product) => ({ 33 | [RESTAURANT_MODULE]: { 34 | restaurant_id: data.input.restaurant_id 35 | }, 36 | [Modules.PRODUCT]: { 37 | product_id: product.id 38 | } 39 | }))) 40 | 41 | createRemoteLinkStep(links) 42 | 43 | return new WorkflowResponse(links); 44 | } 45 | ); 46 | -------------------------------------------------------------------------------- /backend/src/workflows/restaurant/workflows/create-restaurant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | WorkflowData, 4 | WorkflowResponse, 5 | } from "@medusajs/workflows-sdk"; 6 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations"; 7 | import { createRestaurantStep } from "../steps"; 8 | 9 | type WorkflowInput = { 10 | restaurant: CreateRestaurantDTO; 11 | }; 12 | 13 | export const createRestaurantWorkflow = createWorkflow( 14 | "create-restaurant-workflow", 15 | function (input: WorkflowData) { 16 | const restaurant = createRestaurantStep(input.restaurant); 17 | 18 | return new WorkflowResponse(restaurant); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /backend/src/workflows/restaurant/workflows/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-restaurant"; 2 | export * from "./create-restaurant-products"; -------------------------------------------------------------------------------- /backend/src/workflows/user/steps/create-user.ts: -------------------------------------------------------------------------------- 1 | import { MedusaError } from "@medusajs/utils"; 2 | import { createStep, StepResponse } from "@medusajs/workflows-sdk"; 3 | import { DriverDTO } from "../../../modules/delivery/types/common"; 4 | import { RestaurantAdminDTO } from "../../../modules/restaurant/types/common"; 5 | import { 6 | CreateDriverInput, 7 | CreateRestaurantAdminInput, 8 | } from "../workflows/create-user"; 9 | import { RESTAURANT_MODULE } from "../../../modules/restaurant"; 10 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 11 | 12 | type CreateUserStepInput = (CreateRestaurantAdminInput | CreateDriverInput) & { 13 | actor_type: "restaurant" | "driver"; 14 | }; 15 | 16 | type CompensationStepInput = { 17 | id: string; 18 | actor_type: string; 19 | }; 20 | 21 | export const createUserStepId = "create-user-step"; 22 | export const createUserStep = createStep( 23 | createUserStepId, 24 | async ( 25 | input: CreateUserStepInput, 26 | { container } 27 | ): Promise< 28 | StepResponse 29 | > => { 30 | if (input.actor_type === "restaurant") { 31 | const service = container.resolve(RESTAURANT_MODULE); 32 | 33 | const restaurantAdmin = await service.createRestaurantAdmins( 34 | input as CreateRestaurantAdminInput 35 | ); 36 | 37 | const compensationData = { 38 | id: restaurantAdmin.id, 39 | actor_type: "restaurant", 40 | }; 41 | 42 | return new StepResponse(restaurantAdmin, compensationData); 43 | } 44 | 45 | if (input.actor_type === "driver") { 46 | const service = container.resolve(DELIVERY_MODULE); 47 | 48 | const driver = await service.createDrivers(input as CreateDriverInput); 49 | 50 | const driverWithAvatar = await service.updateDrivers({ 51 | id: driver.id, 52 | avatar_url: `https://robohash.org/${driver.id}?size=40x40&set=set1&bgset=bg1`, 53 | }); 54 | 55 | const compensationData = { 56 | id: driverWithAvatar.id, 57 | actor_type: "driver", 58 | }; 59 | 60 | return new StepResponse(driverWithAvatar, compensationData); 61 | } 62 | 63 | throw MedusaError.Types.INVALID_DATA; 64 | }, 65 | function ({ id, actor_type }: CompensationStepInput, { container }) { 66 | if (actor_type === "restaurant") { 67 | const service = container.resolve(RESTAURANT_MODULE); 68 | 69 | return service.deleteRestaurantAdmins(id); 70 | } 71 | 72 | if (actor_type === "driver") { 73 | const service = container.resolve(DELIVERY_MODULE); 74 | 75 | return service.deleteDrivers(id); 76 | } 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /backend/src/workflows/user/steps/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-user"; 2 | -------------------------------------------------------------------------------- /backend/src/workflows/user/steps/update-user.ts: -------------------------------------------------------------------------------- 1 | import { MedusaError } from "@medusajs/utils"; 2 | import { createStep, StepResponse } from "@medusajs/workflows-sdk"; 3 | import { DriverDTO } from "../../../modules/delivery/types/common"; 4 | import { RestaurantAdminDTO } from "../../../modules/restaurant/types/common"; 5 | import { 6 | UpdateRestaurantsDTO, 7 | UpdateRestaurantAdminsDTO, 8 | } from "../../../modules/restaurant/types/mutations"; 9 | import { RESTAURANT_MODULE } from "../../../modules/restaurant"; 10 | import { DELIVERY_MODULE } from "../../../modules/delivery"; 11 | 12 | type UpdateUserStepInput = ( 13 | | UpdateRestaurantsDTO 14 | | UpdateRestaurantAdminsDTO 15 | ) & { 16 | actor_type: "restaurant" | "driver"; 17 | }; 18 | 19 | export const updateUserStepId = "update-user-step"; 20 | export const updateUserStep = createStep( 21 | updateUserStepId, 22 | async ( 23 | input: UpdateUserStepInput, 24 | { container } 25 | ): Promise< 26 | StepResponse 27 | > => { 28 | const { actor_type, ...data } = input; 29 | 30 | if (actor_type === "restaurant") { 31 | const service = container.resolve(RESTAURANT_MODULE); 32 | 33 | const compensationData = { 34 | ...(await service.retrieveRestaurantAdmin(data.id)), 35 | actor_type: "restaurant" as "restaurant", 36 | }; 37 | 38 | const restaurantAdmin = await service.updateRestaurantAdmins(data); 39 | 40 | return new StepResponse(restaurantAdmin, compensationData); 41 | } 42 | 43 | if (actor_type === "driver") { 44 | const service = container.resolve(DELIVERY_MODULE); 45 | 46 | const compensationData = { 47 | ...(await service.retrieveDriver(data.id)), 48 | actor_type: "driver" as "driver", 49 | }; 50 | 51 | const driver = await service.updateDrivers(data); 52 | 53 | return new StepResponse(driver, compensationData); 54 | } 55 | 56 | throw MedusaError.Types.INVALID_DATA; 57 | }, 58 | function ({ actor_type, ...data }: UpdateUserStepInput, { container }) { 59 | if (actor_type === "restaurant") { 60 | const service = container.resolve(RESTAURANT_MODULE); 61 | 62 | return service.updateRestaurantAdmins(data); 63 | } 64 | 65 | if (actor_type === "driver") { 66 | const service = container.resolve(DELIVERY_MODULE); 67 | 68 | return service.updateDrivers(data); 69 | } 70 | } 71 | ); 72 | -------------------------------------------------------------------------------- /backend/src/workflows/user/workflows/create-user.ts: -------------------------------------------------------------------------------- 1 | import { setAuthAppMetadataStep } from "@medusajs/core-flows"; 2 | import { 3 | createWorkflow, 4 | transform, 5 | WorkflowData, 6 | WorkflowResponse, 7 | } from "@medusajs/workflows-sdk"; 8 | import { createUserStep } from "../../user/steps"; 9 | 10 | export type CreateRestaurantAdminInput = { 11 | restaurant_id: string; 12 | email: string; 13 | first_name: string; 14 | last_name: string; 15 | }; 16 | 17 | export type CreateDriverInput = { 18 | email: string; 19 | first_name: string; 20 | last_name: string; 21 | phone: string; 22 | avatar_url?: string; 23 | }; 24 | 25 | type WorkflowInput = { 26 | user: (CreateRestaurantAdminInput | CreateDriverInput) & { 27 | actor_type: "restaurant" | "driver"; 28 | }; 29 | auth_identity_id: string; 30 | }; 31 | 32 | export const createUserWorkflow = createWorkflow( 33 | "create-user-workflow", 34 | function (input: WorkflowData) { 35 | let user = createUserStep(input.user); 36 | 37 | const authUserInput = transform({ input, user }, ({ input, user }) => { 38 | const data = { 39 | authIdentityId: input.auth_identity_id, 40 | actorType: input.user.actor_type, 41 | key: 42 | input.user.actor_type === "restaurant" 43 | ? "restaurant_id" 44 | : "driver_id", 45 | value: user.id, 46 | }; 47 | 48 | return data; 49 | }); 50 | 51 | setAuthAppMetadataStep(authUserInput); 52 | 53 | return new WorkflowResponse(user); 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /backend/src/workflows/user/workflows/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-user"; 2 | -------------------------------------------------------------------------------- /backend/src/workflows/util/steps/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./set-step-failed"; 2 | export * from "./set-step-success"; 3 | -------------------------------------------------------------------------------- /backend/src/workflows/util/steps/set-step-failed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleRegistrationName, 3 | TransactionHandlerType, 4 | } from "@medusajs/utils"; 5 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 6 | import { DeliveryDTO } from "../../../modules/delivery/types/common"; 7 | import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery"; 8 | 9 | type SetStepFailedtepInput = { 10 | stepId?: string; 11 | updatedDelivery: DeliveryDTO; 12 | }; 13 | 14 | export const setStepFailedStepId = "set-step-failed-step"; 15 | export const setStepFailedStep = createStep( 16 | setStepFailedStepId, 17 | async function ( 18 | { stepId, updatedDelivery }: SetStepFailedtepInput, 19 | { container } 20 | ) { 21 | if (!stepId) { 22 | return; 23 | } 24 | 25 | const engineService = container.resolve( 26 | ModuleRegistrationName.WORKFLOW_ENGINE 27 | ); 28 | 29 | await engineService.setStepFailure({ 30 | idempotencyKey: { 31 | action: TransactionHandlerType.INVOKE, 32 | transactionId: updatedDelivery.transaction_id, 33 | stepId, 34 | workflowId: handleDeliveryWorkflowId, 35 | }, 36 | stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id), 37 | options: { 38 | container, 39 | }, 40 | }); 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /backend/src/workflows/util/steps/set-step-success.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleRegistrationName, 3 | TransactionHandlerType, 4 | } from "@medusajs/utils"; 5 | import { StepResponse, createStep } from "@medusajs/workflows-sdk"; 6 | import { DeliveryDTO } from "../../../modules/delivery/types/common"; 7 | import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery"; 8 | 9 | type SetStepSuccessStepInput = { 10 | stepId?: string; 11 | updatedDelivery: DeliveryDTO; 12 | }; 13 | 14 | export const setStepSuccessStepId = "set-step-success-step"; 15 | export const setStepSuccessStep = createStep( 16 | setStepSuccessStepId, 17 | async function ( 18 | { stepId, updatedDelivery }: SetStepSuccessStepInput, 19 | { container } 20 | ) { 21 | if (!stepId) { 22 | return; 23 | } 24 | 25 | const engineService = container.resolve( 26 | ModuleRegistrationName.WORKFLOW_ENGINE 27 | ); 28 | 29 | await engineService.setStepSuccess({ 30 | idempotencyKey: { 31 | action: TransactionHandlerType.INVOKE, 32 | transactionId: updatedDelivery.transaction_id, 33 | stepId, 34 | workflowId: handleDeliveryWorkflowId, 35 | }, 36 | stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id), 37 | options: { 38 | container, 39 | }, 40 | }); 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "esModuleInterop": true, 5 | "module": "Node16", 6 | "moduleResolution": "Node16", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "skipDefaultLibCheck": true, 11 | "declaration": false, 12 | "sourceMap": false, 13 | "inlineSourceMap": true, 14 | "outDir": "./.medusa/server", 15 | "rootDir": "./", 16 | "baseUrl": ".", 17 | "jsx": "react-jsx", 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "checkJs": false 21 | }, 22 | "ts-node": { 23 | "swc": true 24 | }, 25 | "include": ["**/*", ".medusa/types/*"], 26 | "exclude": ["node_modules", ".medusa/server", ".medusa/admin", ".cache"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/.env.template: -------------------------------------------------------------------------------- 1 | JWT_SECRET=supersecret 2 | BACKEND_URL=http://localhost:9000 -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 |

Frontend Overview

2 | 3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 4 | 5 | ## Getting Started 6 | 7 | First, run the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | # or 14 | pnpm dev 15 | # or 16 | bun dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 20 | 21 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "http", 7 | hostname: "localhost", 8 | port: "3000", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "robohash.org", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "medusa-eats.vercel.app", 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | export default nextConfig; 23 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-eats", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@dotlottie/react-player": "^1.6.19", 13 | "@medusajs/icons": "^1.2.2-preview-20240925090259", 14 | "@medusajs/js-sdk": "^0.0.2-preview-20241008090600", 15 | "@medusajs/ui": "preview", 16 | "@next/third-parties": "^14.2.5", 17 | "eventsource": "^2.0.2", 18 | "jose": "^5.2.4", 19 | "lucide-react": "^0.427.0", 20 | "next": "14.1.4", 21 | "next-view-transitions": "^0.1.1", 22 | "prop-types": "^15.8.1", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-lottie": "^1.2.4", 26 | "sharp": "^0.33.4", 27 | "tailwind-scrollbar-hide": "^1.1.7" 28 | }, 29 | "devDependencies": { 30 | "@medusajs/types": "preview", 31 | "@medusajs/ui-preset": "preview", 32 | "@types/eventsource": "^1.1.15", 33 | "@types/jsonwebtoken": "^9.0.6", 34 | "@types/node": "^20", 35 | "@types/react": "^18", 36 | "@types/react-dom": "^18", 37 | "@types/tailwindcss": "^3.1.0", 38 | "autoprefixer": "^10.0.1", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.1.4", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.3.0", 43 | "typescript": "^5.5.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/_1ed747de-53f1-4915-aaba-92c046039beb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_1ed747de-53f1-4915-aaba-92c046039beb.jpeg -------------------------------------------------------------------------------- /frontend/public/_35841975-1e89-4c07-adb8-208cc8c59e17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_35841975-1e89-4c07-adb8-208cc8c59e17.jpeg -------------------------------------------------------------------------------- /frontend/public/_411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg -------------------------------------------------------------------------------- /frontend/public/_4476ce30-b852-402d-b65f-e3db54165e4c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_4476ce30-b852-402d-b65f-e3db54165e4c.jpeg -------------------------------------------------------------------------------- /frontend/public/_7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg -------------------------------------------------------------------------------- /frontend/public/_ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg -------------------------------------------------------------------------------- /frontend/public/_de4759b3-096f-47f6-936a-ea291df53f9f.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_de4759b3-096f-47f6-936a-ea291df53f9f.jpeg -------------------------------------------------------------------------------- /frontend/public/medusa-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/notification.mp3 -------------------------------------------------------------------------------- /frontend/public/pizza-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/pizza-loading.gif -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/(checkout)/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | import CheckoutForm from "@frontend/components/store/checkout/checkout-form"; 2 | import { OrderSummary } from "@frontend/components/store/checkout/order-summary"; 3 | import { retrieveCart } from "@frontend/lib/data"; 4 | import { HttpTypes } from "@medusajs/types"; 5 | import { cookies } from "next/headers"; 6 | 7 | export default async function CheckoutPage() { 8 | const cartId = cookies().get("_medusa_cart_id")?.value; 9 | 10 | if (!cartId) { 11 | return null; 12 | } 13 | 14 | const cart = (await retrieveCart(cartId)) as HttpTypes.StoreCart; 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/(checkout)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@frontend/components/common/footer"; 2 | import { ProfileBadge } from "@frontend/components/common/profile-badge"; 3 | import { retrieveUser } from "@frontend/lib/data"; 4 | import { FlyingBox } from "@medusajs/icons"; 5 | import { Text } from "@medusajs/ui"; 6 | import type { Metadata } from "next"; 7 | import { Link } from "next-view-transitions"; 8 | import Image from "next/image"; 9 | 10 | export const metadata: Metadata = { 11 | title: "Medusa Eats", 12 | description: "Order food from your favorite restaurants", 13 | }; 14 | 15 | export default async function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | const user = await retrieveUser(); 21 | 22 | return ( 23 | <> 24 | 47 |
48 | {children} 49 |
50 |