├── .env.example ├── dump.rdb ├── .gitignore ├── develop.sh ├── src ├── modules │ ├── role │ │ ├── role.repository.ts │ │ ├── role.module.ts │ │ ├── 1655131148363-role.migration.ts │ │ └── role.entity.ts │ ├── permission │ │ ├── permission.repository.ts │ │ ├── permission.module.ts │ │ ├── permission.guard.ts │ │ ├── permission.entity.ts │ │ └── 1655131601491-permission.migration.ts │ ├── store │ │ ├── store.module.ts │ │ ├── repositories │ │ │ └── store.repository.ts │ │ ├── entities │ │ │ └── store.entity.ts │ │ └── services │ │ │ └── store.service.ts │ ├── invite │ │ ├── invite.router.ts │ │ ├── invite.repository.ts │ │ ├── invite.entity.ts │ │ ├── invite.module.ts │ │ ├── 1655123458263-invite.migration.ts │ │ ├── inviteSubscriber.middleware.ts │ │ ├── invite.subscriber.ts │ │ ├── acceptInvite.controller.ts │ │ └── invite.service.ts │ ├── order │ │ ├── order.repository.ts │ │ ├── order.module.ts │ │ ├── order.entity.ts │ │ ├── 1652101349791-order.migration.ts │ │ ├── order.service.ts │ │ └── order.subscriber.ts │ ├── user │ │ ├── repositories │ │ │ └── user.repository.ts │ │ ├── routers │ │ │ └── user.router.ts │ │ ├── user.migration.ts │ │ ├── entities │ │ │ └── user.entity.ts │ │ ├── middlewares │ │ │ ├── loggedInUser.middleware.ts │ │ │ └── userSubscriber.middleware.ts │ │ ├── user.module.ts │ │ ├── subscribers │ │ │ └── user.subscriber.ts │ │ ├── 1655132360987-user.migration.ts │ │ └── services │ │ │ └── user.service.ts │ └── product │ │ ├── repositories │ │ └── product.repository.ts │ │ ├── entities │ │ └── product.entity.ts │ │ ├── product.migration.ts │ │ ├── product.router.ts │ │ ├── product.module.ts │ │ ├── middlewares │ │ └── product.middleware.ts │ │ ├── subscribers │ │ └── product.subscriber.ts │ │ └── services │ │ └── product.service.ts ├── main.ts ├── api │ └── README.md ├── subscribers │ └── README.md └── services │ └── README.md ├── Dockerfile ├── .babelrc.js ├── tsconfig.json ├── docker-compose.yml ├── README.md ├── medusa-config.js ├── package.json └── data └── seed.json /.env.example: -------------------------------------------------------------------------------- 1 | DB_USERNAME= 2 | DB_PASSWORD= 3 | DB_HOST= 4 | DB_PORT= 5 | DB_SCHEME= -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahednasser/medusa-marketplace-tutorial/HEAD/dump.rdb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | .env 3 | .DS_Store 4 | .idea 5 | /uploads 6 | /node_modules 7 | yarn-error.log 8 | 9 | -------------------------------------------------------------------------------- /develop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Run migrations to ensure the database is updated 4 | medusa migrations run 5 | 6 | #Start development environment 7 | npm run start -------------------------------------------------------------------------------- /src/modules/role/role.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | 3 | import { Repository as MedusaRepository } from "medusa-extender"; 4 | import { Role } from './role.entity'; 5 | 6 | @MedusaRepository() 7 | @EntityRepository(Role) 8 | export class RoleRepository extends Repository {} -------------------------------------------------------------------------------- /src/modules/permission/permission.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | 3 | import { Repository as MedusaRepository } from "medusa-extender"; 4 | import { Permission } from './permission.entity'; 5 | 6 | @MedusaRepository() 7 | @EntityRepository(Permission) 8 | export class PermissionRepository extends Repository {} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17.1.0 2 | 3 | WORKDIR /app/medusa 4 | 5 | COPY package.json . 6 | COPY develop.sh . 7 | COPY yarn.lock . 8 | 9 | RUN apt-get update 10 | 11 | RUN apt-get install -y python 12 | 13 | RUN npm install -g npm@latest 14 | 15 | RUN npm install -g @medusajs/medusa-cli@latest 16 | 17 | RUN npm install 18 | 19 | ENTRYPOINT ["./develop.sh"] -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | let ignore = [`**/dist`]; 2 | 3 | // Jest needs to compile this code, but generally we don't want this copied 4 | // to output folders 5 | if (process.env.NODE_ENV !== `test`) { 6 | ignore.push(`**/__tests__`); 7 | } 8 | 9 | module.exports = { 10 | presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]], 11 | ignore, 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'medusa-extender'; 2 | import { Store } from './entities/store.entity'; 3 | import StoreRepository from './repositories/store.repository'; 4 | import StoreService from './services/store.service'; 5 | 6 | @Module({ 7 | imports: [Store, StoreRepository, StoreService], 8 | }) 9 | export class StoreModule {} 10 | -------------------------------------------------------------------------------- /src/modules/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "medusa-extender"; 2 | import { Role } from "./role.entity"; 3 | import { RoleMigration1655131148363 } from './1655131148363-role.migration'; 4 | import { RoleRepository } from "./role.repository"; 5 | 6 | @Module({ 7 | imports: [ 8 | Role, 9 | RoleRepository, 10 | RoleMigration1655131148363 11 | ] 12 | }) 13 | export class RoleModule {} -------------------------------------------------------------------------------- /src/modules/invite/invite.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'medusa-extender'; 2 | import acceptInvite from './acceptInvite.controller'; 3 | 4 | @Router({ 5 | routes: [ 6 | { 7 | requiredAuth: false, 8 | path: '/admin/invites/accept', 9 | method: 'post', 10 | handlers: [acceptInvite], 11 | }, 12 | ], 13 | }) 14 | export class AcceptInviteRouter {} -------------------------------------------------------------------------------- /src/modules/permission/permission.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "medusa-extender"; 2 | import { Permission } from "./permission.entity"; 3 | import { PermissionMigration1655131601491 } from "./1655131601491-permission.migration"; 4 | import { PermissionRepository } from "./permission.repository"; 5 | 6 | @Module({ 7 | imports: [ 8 | Permission, 9 | PermissionRepository, 10 | PermissionMigration1655131601491 11 | ] 12 | }) 13 | export class PermissionModule {} -------------------------------------------------------------------------------- /src/modules/order/order.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository as MedusaRepository, Utils } from "medusa-extender"; 2 | 3 | import { EntityRepository } from "typeorm"; 4 | import { OrderRepository as MedusaOrderRepository } from "@medusajs/medusa/dist/repositories/order"; 5 | import { Order } from "./order.entity"; 6 | 7 | @MedusaRepository({override: MedusaOrderRepository}) 8 | @EntityRepository(Order) 9 | export class OrderRepository extends Utils.repositoryMixin(MedusaOrderRepository) {} -------------------------------------------------------------------------------- /src/modules/invite/invite.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository as MedusaRepository, Utils } from "medusa-extender"; 2 | 3 | import { EntityRepository } from "typeorm"; 4 | import { Invite } from "./invite.entity"; 5 | import { InviteRepository as MedusaInviteRepository } from "@medusajs/medusa/dist/repositories/invite"; 6 | 7 | @MedusaRepository({override: MedusaInviteRepository}) 8 | @EntityRepository(Invite) 9 | export class InviteRepository extends Utils.repositoryMixin(MedusaInviteRepository) {} -------------------------------------------------------------------------------- /src/modules/order/order.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'medusa-extender'; 2 | import { Order } from './order.entity'; 3 | import { OrderMigration1652101349791 } from './1652101349791-order.migration'; 4 | import { OrderRepository } from './order.repository'; 5 | import { OrderService } from './order.service'; 6 | import { OrderSubscriber } from './order.subscriber'; 7 | 8 | @Module({ 9 | imports: [Order, OrderRepository, OrderService, OrderSubscriber, OrderMigration1652101349791] 10 | }) 11 | export class OrderModule {} -------------------------------------------------------------------------------- /src/modules/store/repositories/store.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository } from 'typeorm'; 2 | import { StoreRepository as MedusaStoreRepository } from '@medusajs/medusa/dist/repositories/store'; 3 | import { Repository as MedusaRepository, Utils } from 'medusa-extender'; 4 | import { Store } from '../entities/store.entity'; 5 | 6 | @MedusaRepository({ override: MedusaStoreRepository }) 7 | @EntityRepository(Store) 8 | export default class StoreRepository extends Utils.repositoryMixin(MedusaStoreRepository) { 9 | } -------------------------------------------------------------------------------- /src/modules/user/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository as MedusaRepository, Utils } from "medusa-extender"; 2 | 3 | import { EntityRepository } from "typeorm"; 4 | import { UserRepository as MedusaUserRepository } from "@medusajs/medusa/dist/repositories/user"; 5 | import { User } from "../entities/user.entity"; 6 | 7 | @MedusaRepository({ override: MedusaUserRepository }) 8 | @EntityRepository(User) 9 | export default class UserRepository extends Utils.repositoryMixin(MedusaUserRepository) { 10 | } -------------------------------------------------------------------------------- /src/modules/user/routers/user.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'medusa-extender'; 2 | import createUserHandler from '@medusajs/medusa/dist/api/routes/admin/users/create-user'; 3 | import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware'; 4 | 5 | @Router({ 6 | routes: [ 7 | { 8 | requiredAuth: false, 9 | path: '/admin/create-user', 10 | method: 'post', 11 | handlers: [wrapHandler(createUserHandler)], 12 | }, 13 | ], 14 | }) 15 | export class UserRouter { 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/product/repositories/product.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository as MedusaRepository, Utils } from "medusa-extender"; 2 | 3 | import { EntityRepository } from "typeorm"; 4 | import { ProductRepository as MedusaProductRepository } from "@medusajs/medusa/dist/repositories/product"; 5 | import { Product } from '../entities/product.entity'; 6 | 7 | @MedusaRepository({ override: MedusaProductRepository }) 8 | @EntityRepository(Product) 9 | export default class ProductRepository extends Utils.repositoryMixin(MedusaProductRepository) { 10 | } -------------------------------------------------------------------------------- /src/modules/invite/invite.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; 2 | 3 | import { Entity as MedusaEntity } from "medusa-extender"; 4 | import { Invite as MedusaInvite } from "@medusajs/medusa"; 5 | import { Store } from "../store/entities/store.entity"; 6 | 7 | @MedusaEntity({override: MedusaInvite}) 8 | @Entity() 9 | export class Invite extends MedusaInvite { 10 | @Index() 11 | @Column({ nullable: true }) 12 | store_id: string; 13 | 14 | @ManyToOne(() => Store, (store) => store.invites) 15 | @JoinColumn({ name: 'store_id' }) 16 | store: Store; 17 | } -------------------------------------------------------------------------------- /src/modules/product/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Product as MedusaProduct } from '@medusajs/medusa/dist'; 2 | import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; 3 | import { Entity as MedusaEntity } from 'medusa-extender'; 4 | import { Store } from '../../store/entities/store.entity'; 5 | 6 | @MedusaEntity({ override: MedusaProduct }) 7 | @Entity() 8 | export class Product extends MedusaProduct { 9 | @Index() 10 | @Column({ nullable: false }) 11 | store_id: string; 12 | 13 | @ManyToOne(() => Store, (store) => store.members) 14 | @JoinColumn({ name: 'store_id', referencedColumnName: 'id' }) 15 | store: Store; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/user/user.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export default class addStoreIdToUser1611063162649 implements MigrationInterface { 7 | name = 'addStoreIdToUser1611063162649'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "store_id" text;`; 11 | await queryRunner.query(query); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | const query = `ALTER TABLE public."user" DROP COLUMN "store_id";`; 16 | await queryRunner.query(query); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/invite/invite.module.ts: -------------------------------------------------------------------------------- 1 | import { AcceptInviteRouter } from "./invite.router"; 2 | import { AttachInviteSubscriberMiddleware } from "./inviteSubscriber.middleware"; 3 | import { Invite } from "./invite.entity"; 4 | import { InviteMigration1655123458263 } from './1655123458263-invite.migration'; 5 | import { InviteRepository } from './invite.repository'; 6 | import { InviteService } from './invite.service'; 7 | import { Module } from "medusa-extender"; 8 | 9 | @Module({ 10 | imports: [ 11 | Invite, 12 | InviteRepository, 13 | InviteService, 14 | InviteMigration1655123458263, 15 | AttachInviteSubscriberMiddleware, 16 | AcceptInviteRouter, 17 | ] 18 | }) 19 | export class InviteModule {} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "allowSyntheticDefaultImports": true, 8 | "moduleResolution": "node", 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "allowJs": true, 13 | "outDir": "dist", 14 | "rootDir": "src", 15 | "esModuleInterop": true 16 | }, 17 | "include": [ 18 | "src", 19 | ], 20 | "exclude": [ 21 | "dist", 22 | "node_modules", 23 | "**/*.spec.ts", 24 | "medusa-config.js", 25 | ] 26 | } -------------------------------------------------------------------------------- /src/modules/product/product.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export default class addStoreIdToProduct1645034402086 implements MigrationInterface { 7 | name = 'addStoreIdToProduct1645034402086'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = `ALTER TABLE public."product" ADD COLUMN IF NOT EXISTS "store_id" text;`; 11 | await queryRunner.query(query); 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | const query = `ALTER TABLE public."product" DROP COLUMN "store_id";`; 16 | await queryRunner.query(query); 17 | } 18 | } -------------------------------------------------------------------------------- /src/modules/product/product.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'medusa-extender'; 2 | import listProductsHandler from '@medusajs/medusa/dist/api/routes/admin/products/list-products'; 3 | import permissionGuard from '../permission/permission.guard'; 4 | import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware'; 5 | 6 | @Router({ 7 | routes: [ 8 | { 9 | requiredAuth: true, 10 | path: '/admin/products', 11 | method: 'get', 12 | handlers: [ 13 | permissionGuard([ 14 | {path: "/admin/products"} 15 | ]), 16 | wrapHandler(listProductsHandler) 17 | ], 18 | }, 19 | ], 20 | }) 21 | export class ProductsRouter {} -------------------------------------------------------------------------------- /src/modules/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import AttachProductSubscribersMiddleware from './middlewares/product.middleware'; 2 | import { Module } from 'medusa-extender'; 3 | import { Product } from './entities/product.entity'; 4 | import ProductRepository from './repositories/product.repository'; 5 | import { ProductService } from './services/product.service'; 6 | import { ProductsRouter } from './product.router'; 7 | import addStoreIdToProduct1645034402086 from './product.migration'; 8 | 9 | @Module({ 10 | imports: [ 11 | Product, 12 | ProductRepository, 13 | ProductService, 14 | addStoreIdToProduct1645034402086, 15 | AttachProductSubscribersMiddleware, 16 | // ProductsRouter 17 | ] 18 | }) 19 | export class ProductModule {} -------------------------------------------------------------------------------- /src/modules/invite/1655123458263-invite.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export class InviteMigration1655123458263 implements MigrationInterface { 7 | name = 'InviteMigration1655123458263'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = ` 11 | ALTER TABLE public."invite" ADD COLUMN IF NOT EXISTS "store_id" text; 12 | `; 13 | await queryRunner.query(query); 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | const query = ` 18 | ALTER TABLE public."invite" DROP COLUMN "store_id"; 19 | `; 20 | await queryRunner.query(query); 21 | } 22 | } -------------------------------------------------------------------------------- /src/modules/permission/permission.guard.ts: -------------------------------------------------------------------------------- 1 | import UserService from "../user/services/user.service"; 2 | import _ from "lodash"; 3 | 4 | export default (permissions: Record[]) => { 5 | return async (req, res, next) => { 6 | const userService = req.scope.resolve('userService') as UserService; 7 | const loggedInUser = await userService.retrieve(req.user.userId, { 8 | select: ['id', 'store_id'], 9 | relations: ['teamRole', 'teamRole.permissions'] 10 | }); 11 | 12 | const isAllowed = permissions.every(permission => 13 | loggedInUser.teamRole?.permissions.some((userPermission) => _.isEqual(userPermission.metadata, permission)) 14 | ) 15 | 16 | if (isAllowed) { 17 | return next() 18 | } 19 | 20 | //permission denied 21 | res.sendStatus(401) 22 | } 23 | } -------------------------------------------------------------------------------- /src/modules/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; 2 | 3 | import { Entity as MedusaEntity } from 'medusa-extender'; 4 | import { User as MedusaUser } from '@medusajs/medusa/dist'; 5 | import { Role } from '../../role/role.entity'; 6 | import { Store } from '../../store/entities/store.entity'; 7 | 8 | @MedusaEntity({ override: MedusaUser }) 9 | @Entity() 10 | export class User extends MedusaUser { 11 | @Index() 12 | @Column({ nullable: false }) 13 | store_id: string; 14 | 15 | @ManyToOne(() => Store, (store) => store.members) 16 | @JoinColumn({ name: 'store_id' }) 17 | store: Store; 18 | 19 | @Index() 20 | @Column({ nullable: true }) 21 | role_id: string; 22 | 23 | @ManyToOne(() => Role, (role) => role.users) 24 | @JoinColumn({ name: 'role_id' }) 25 | teamRole: Role; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/invite/inviteSubscriber.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MEDUSA_RESOLVER_KEYS, 3 | MedusaAuthenticatedRequest, 4 | MedusaMiddleware, 5 | Utils as MedusaUtils, 6 | Middleware 7 | } from 'medusa-extender'; 8 | import { NextFunction, Response } from 'express'; 9 | 10 | import { Connection } from 'typeorm'; 11 | import InviteSubscriber from './invite.subscriber'; 12 | 13 | @Middleware({ requireAuth: true, routes: [{ method: "post", path: '/admin/invites*' }] }) 14 | export class AttachInviteSubscriberMiddleware implements MedusaMiddleware { 15 | public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise { 16 | const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection }; 17 | InviteSubscriber.attachTo(connection) 18 | return next(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/modules/user/middlewares/loggedInUser.middleware.ts: -------------------------------------------------------------------------------- 1 | import { MedusaAuthenticatedRequest, MedusaMiddleware, Middleware } from 'medusa-extender'; 2 | import { NextFunction, Response } from 'express'; 3 | 4 | import UserService from '../services/user.service'; 5 | 6 | @Middleware({ requireAuth: true, routes: [{ method: "all", path: '/admin/*' }] }) 7 | export class LoggedInUserMiddleware implements MedusaMiddleware { 8 | public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise { 9 | const userService = req.scope.resolve('userService') as UserService; 10 | const loggedInUser = await userService.retrieve(req.user.userId, { 11 | select: ['id', 'store_id'] 12 | }); 13 | req.scope.register({ 14 | loggedInUser: { 15 | resolve: () => loggedInUser, 16 | }, 17 | }); 18 | next(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/modules/product/middlewares/product.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MEDUSA_RESOLVER_KEYS, 3 | MedusaAuthenticatedRequest, 4 | MedusaMiddleware, 5 | Utils as MedusaUtils, 6 | Middleware 7 | } from 'medusa-extender'; 8 | import { NextFunction, Request, Response } from 'express'; 9 | 10 | import { Connection } from 'typeorm'; 11 | import ProductSubscriber from '../subscribers/product.subscriber'; 12 | 13 | @Middleware({ requireAuth: true, routes: [{ method: 'post', path: '/admin/products' }] }) 14 | export default class AttachProductSubscribersMiddleware implements MedusaMiddleware { 15 | public consume(req: MedusaAuthenticatedRequest | Request, res: Response, next: NextFunction): void | Promise { 16 | const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection }; 17 | MedusaUtils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber); 18 | return next(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { AttachUserSubscriberMiddleware } from './middlewares/userSubscriber.middleware'; 2 | import { LoggedInUserMiddleware } from "./middlewares/loggedInUser.middleware"; 3 | import { Module } from 'medusa-extender'; 4 | import { User } from './entities/user.entity'; 5 | import { UserMigration1655132360987 } from './1655132360987-user.migration'; 6 | import UserRepository from './repositories/user.repository'; 7 | import { UserRouter } from "./routers/user.router"; 8 | import UserService from './services/user.service'; 9 | import addStoreIdToUser1644946220401 from './user.migration'; 10 | 11 | @Module({ 12 | imports: [ 13 | User, 14 | UserService, 15 | UserRepository, 16 | addStoreIdToUser1644946220401, 17 | UserRouter, 18 | LoggedInUserMiddleware, 19 | AttachUserSubscriberMiddleware, 20 | UserMigration1655132360987] 21 | }) 22 | export class UserModule {} -------------------------------------------------------------------------------- /src/modules/user/middlewares/userSubscriber.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MEDUSA_RESOLVER_KEYS, 3 | MedusaAuthenticatedRequest, 4 | MedusaMiddleware, 5 | Utils as MedusaUtils, 6 | Middleware 7 | } from 'medusa-extender'; 8 | import { NextFunction, Response } from 'express'; 9 | 10 | import { Connection } from 'typeorm'; 11 | import UserSubscriber from '../subscribers/user.subscriber'; 12 | 13 | @Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }, { method: "post", path: '/admin/create-user' }] }) 14 | export class AttachUserSubscriberMiddleware implements MedusaMiddleware { 15 | public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise { 16 | const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection }; 17 | MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber); 18 | return next(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/modules/order/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from "typeorm"; 2 | 3 | import { Entity as MedusaEntity } from "medusa-extender"; 4 | import { Order as MedusaOrder } from "@medusajs/medusa"; 5 | import { Store } from "../store/entities/store.entity"; 6 | 7 | @MedusaEntity({override: MedusaOrder}) 8 | @Entity() 9 | export class Order extends MedusaOrder { 10 | @Index() 11 | @Column({ nullable: true }) 12 | store_id: string; 13 | 14 | @Index() 15 | @Column({ nullable: false }) 16 | order_parent_id: string; 17 | 18 | @ManyToOne(() => Store, (store) => store.orders) 19 | @JoinColumn({ name: 'store_id' }) 20 | store: Store; 21 | 22 | @ManyToOne(() => Order, (order) => order.children) 23 | @JoinColumn({ name: 'order_parent_id' }) 24 | parent: Order; 25 | 26 | @OneToMany(() => Order, (order) => order.parent) 27 | @JoinColumn({ name: 'id', referencedColumnName: 'order_parent_id' }) 28 | children: Order[]; 29 | } -------------------------------------------------------------------------------- /src/modules/user/subscribers/user.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; 2 | import { eventEmitter, Utils as MedusaUtils, OnMedusaEntityEvent } from 'medusa-extender'; 3 | import { User } from '../entities/user.entity'; 4 | 5 | @EventSubscriber() 6 | export default class UserSubscriber implements EntitySubscriberInterface { 7 | static attachTo(connection: Connection): void { 8 | MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber); 9 | } 10 | 11 | public listenTo(): typeof User { 12 | return User; 13 | } 14 | 15 | /** 16 | * Relay the event to the handlers. 17 | * @param event Event to pass to the event handler 18 | */ 19 | public async beforeInsert(event: InsertEvent): Promise { 20 | return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(User), { 21 | event, 22 | transactionalEntityManager: event.manager, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/invite/invite.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; 2 | import { Utils as MedusaUtils, OnMedusaEntityEvent, eventEmitter } from 'medusa-extender'; 3 | 4 | import { Invite } from './invite.entity'; 5 | 6 | @EventSubscriber() 7 | export default class InviteSubscriber implements EntitySubscriberInterface { 8 | static attachTo(connection: Connection): void { 9 | MedusaUtils.attachOrReplaceEntitySubscriber(connection, InviteSubscriber); 10 | } 11 | 12 | public listenTo(): typeof Invite { 13 | return Invite; 14 | } 15 | 16 | /** 17 | * Relay the event to the handlers. 18 | * @param event Event to pass to the event handler 19 | */ 20 | public async beforeInsert(event: InsertEvent): Promise { 21 | return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Invite), { 22 | event, 23 | transactionalEntityManager: event.manager, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | backend: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | image: backend:starter 8 | container_name: medusa-server-default 9 | depends_on: 10 | - postgres 11 | - redis 12 | environment: 13 | DATABASE_URL: postgres://postgres:postgres@postgres:5432/medusa-docker 14 | REDIS_URL: redis://redis 15 | NODE_ENV: development 16 | JWT_SECRET: something 17 | COOKIE_SECRET: something 18 | PORT: 9000 19 | ports: 20 | - "9000:9000" 21 | volumes: 22 | - .:/app/medusa 23 | - node_modules:/app/medusa/node_modules 24 | 25 | postgres: 26 | image: postgres:10.4 27 | ports: 28 | - "5432:5432" 29 | environment: 30 | POSTGRES_USER: postgres 31 | POSTGRES_PASSWORD: postgres 32 | POSTGRES_DB: medusa-docker 33 | 34 | redis: 35 | image: redis 36 | expose: 37 | - 6379 38 | 39 | volumes: 40 | node_modules: 41 | -------------------------------------------------------------------------------- /src/modules/user/1655132360987-user.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, TableForeignKey } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export class UserMigration1655132360987 implements MigrationInterface { 7 | name = 'UserMigration1655132360987'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "role_id" text;`; 11 | await queryRunner.query(query); 12 | 13 | await queryRunner.createForeignKey("user", new TableForeignKey({ 14 | columnNames: ["role_id"], 15 | referencedColumnNames: ["id"], 16 | referencedTableName: "role", 17 | onDelete: "CASCADE", 18 | onUpdate: "CASCADE" 19 | })) 20 | } 21 | 22 | public async down(queryRunner: QueryRunner): Promise { 23 | const query = `ALTER TABLE public."user" DROP COLUMN "role_id";`; 24 | await queryRunner.query(query); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { InviteModule } from './modules/invite/invite.module'; 2 | import { Medusa } from 'medusa-extender'; 3 | import { OrderModule } from './modules/order/order.module'; 4 | import { PermissionModule } from './modules/permission/permission.module'; 5 | import { ProductModule } from './modules/product/product.module'; 6 | import { RoleModule } from './modules/role/role.module'; 7 | import { StoreModule } from "./modules/store/store.module"; 8 | import { UserModule } from './modules/user/user.module'; 9 | import express = require('express'); 10 | 11 | async function bootstrap() { 12 | const expressInstance = express(); 13 | 14 | await new Medusa(__dirname + '/../', expressInstance).load([ 15 | UserModule, 16 | ProductModule, 17 | OrderModule, 18 | StoreModule, 19 | InviteModule, 20 | RoleModule, 21 | PermissionModule, 22 | ]); 23 | 24 | expressInstance.listen(9000, () => { 25 | console.info('Server successfully started on port 9000'); 26 | }); 27 | } 28 | 29 | bootstrap(); -------------------------------------------------------------------------------- /src/modules/product/subscribers/product.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm'; 2 | import { OnMedusaEntityEvent, Utils, eventEmitter } from 'medusa-extender'; 3 | 4 | import { Product } from '../entities/product.entity'; 5 | 6 | @EventSubscriber() 7 | export default class ProductSubscriber implements EntitySubscriberInterface { 8 | static attachTo(connection: Connection): void { 9 | Utils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber); 10 | } 11 | 12 | public listenTo(): typeof Product { 13 | return Product; 14 | } 15 | 16 | /** 17 | * Relay the event to the handlers. 18 | * @param event Event to pass to the event handler 19 | */ 20 | public async beforeInsert(event: InsertEvent): Promise { 21 | return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Product), { 22 | event, 23 | transactionalEntityManager: event.manager, 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | # Custom endpoints 2 | 3 | You may define custom endpoints by putting files in the `/api` directory that export functions returning an express router. 4 | ```js 5 | import { Router } from "express" 6 | 7 | export default () => { 8 | const router = Router() 9 | 10 | router.get("/hello-world", (req, res) => { 11 | res.json({ 12 | message: "Welcome to Medusa!" 13 | }) 14 | }) 15 | 16 | return router; 17 | } 18 | ``` 19 | 20 | A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project: 21 | ```js 22 | import { Router } from "express" 23 | 24 | export default () => { 25 | const router = Router() 26 | 27 | router.get("/hello-product", async (req, res) => { 28 | const productService = req.scope.resolve("productService") 29 | 30 | const [product] = await productService.list({}, { take: 1 }) 31 | 32 | res.json({ 33 | message: `Welcome to ${product.title}!` 34 | }) 35 | }) 36 | 37 | return router; 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /src/modules/permission/permission.entity.ts: -------------------------------------------------------------------------------- 1 | import { BeforeInsert, Column, Entity, JoinTable, ManyToMany } from "typeorm"; 2 | 3 | import { BaseEntity } from "@medusajs/medusa"; 4 | import { DbAwareColumn } from "@medusajs/medusa/dist/utils/db-aware-column"; 5 | import { Entity as MedusaEntity } from "medusa-extender"; 6 | import { Role } from "../role/role.entity"; 7 | import { generateEntityId } from "@medusajs/medusa/dist/utils"; 8 | 9 | @MedusaEntity() 10 | @Entity() 11 | export class Permission extends BaseEntity { 12 | 13 | @Column({type: "varchar"}) 14 | name: string; 15 | 16 | @DbAwareColumn({ type: "jsonb", nullable: true }) 17 | metadata: Record 18 | 19 | @ManyToMany(() => Role) 20 | @JoinTable({ 21 | name: "role_permissions", 22 | joinColumn: { 23 | name: "permission_id", 24 | referencedColumnName: "id", 25 | }, 26 | inverseJoinColumn: { 27 | name: "role_id", 28 | referencedColumnName: "id", 29 | }, 30 | }) 31 | roles: Role[] 32 | 33 | @BeforeInsert() 34 | private beforeInsert(): void { 35 | this.id = generateEntityId(this.id, "perm") 36 | } 37 | } -------------------------------------------------------------------------------- /src/subscribers/README.md: -------------------------------------------------------------------------------- 1 | # Custom subscribers 2 | 3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory. 4 | 5 | ```js 6 | class WelcomeSubscriber { 7 | constructor({ welcomeService, eventBusService }) { 8 | this.welcomeService_ = welcomeService; 9 | 10 | eventBusService.subscribe("order.placed", this.handleWelcome); 11 | } 12 | 13 | handleWelcome = async (data) => { 14 | return await this.welcomeService_.sendWelcome(data.id); 15 | }; 16 | } 17 | 18 | export default WelcomeSubscriber; 19 | ``` 20 | 21 | A subscriber is defined as a `class` which is registered as a subscriber by invoking `eventBusService.subscribe` in the `constructor` of the class. 22 | 23 | The type of event that the subscriber subscribes to is passed as the first parameter to the `eventBusService.subscribe` and the eventhandler is passed as the second parameter. The types of events a service can emmit are described in the individual service. 24 | 25 | An eventhandler has one paramenter; a data `object` which contain information relating to the event, including relevant `id's`. The `id` can be used to fetch the appropriate entity in the eventhandler. 26 | -------------------------------------------------------------------------------- /src/modules/order/1652101349791-order.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export class OrderMigration1652101349791 implements MigrationInterface { 7 | name = 'OrderMigration1652101349791'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = ` 11 | ALTER TABLE public."order" ADD COLUMN IF NOT EXISTS "store_id" text; 12 | ALTER TABLE public."order" ADD COLUMN IF NOT EXISTS "order_parent_id" text; 13 | ALTER TABLE public."order" ADD CONSTRAINT "FK_8a96dde86e3cad9d2fcc6cb171f87" FOREIGN KEY ("order_parent_id") REFERENCES "order"("id") ON DELETE CASCADE ON UPDATE CASCADE; 14 | `; 15 | await queryRunner.query(query); 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | const query = ` 20 | ALTER TABLE public."order" DROP COLUMN "store_id"; 21 | ALTER TABLE public."order" DROP COLUMN "order_parent_id"; 22 | ALTER TABLE public."order" DROP FOREIGN KEY "FK_8a96dde86e3cad9d2fcc6cb171f87cb2"; 23 | `; 24 | await queryRunner.query(query); 25 | } 26 | } -------------------------------------------------------------------------------- /src/modules/role/1655131148363-role.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, TableForeignKey } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export class RoleMigration1655131148363 implements MigrationInterface { 7 | name = 'RoleMigration1655131148363'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | const query = ` 11 | CREATE TABLE "role" ("id" character varying NOT NULL, 12 | "name" character varying NOT NULL, "store_id" character varying NOT NULL, 13 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()) 14 | `; 15 | await queryRunner.query(query); 16 | 17 | await queryRunner.createPrimaryKey("role", ["id"]) 18 | await queryRunner.createForeignKey("role", new TableForeignKey({ 19 | columnNames: ["store_id"], 20 | referencedColumnNames: ["id"], 21 | referencedTableName: "store", 22 | onDelete: "CASCADE", 23 | onUpdate: "CASCADE" 24 | })) 25 | } 26 | 27 | public async down(queryRunner: QueryRunner): Promise { 28 | await queryRunner.dropTable("role", true); 29 | } 30 | } -------------------------------------------------------------------------------- /src/modules/store/entities/store.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, JoinColumn, OneToMany } from 'typeorm'; 2 | 3 | import { Invite } from './../../invite/invite.entity'; 4 | import { Entity as MedusaEntity } from 'medusa-extender'; 5 | import { Store as MedusaStore } from '@medusajs/medusa'; 6 | import { Order } from '../../order/order.entity'; 7 | import { Product } from '../../product/entities/product.entity'; 8 | import { Role } from '../../role/role.entity'; 9 | import { User } from '../../user/entities/user.entity'; 10 | 11 | @MedusaEntity({ override: MedusaStore }) 12 | @Entity() 13 | export class Store extends MedusaStore { 14 | @OneToMany(() => User, (user) => user.store) 15 | @JoinColumn({ name: 'id', referencedColumnName: 'store_id' }) 16 | members: User[]; 17 | 18 | @OneToMany(() => Product, (product) => product.store) 19 | @JoinColumn({ name: 'id', referencedColumnName: 'store_id' }) 20 | products: Product[]; 21 | 22 | @OneToMany(() => Order, (order) => order.store) 23 | @JoinColumn({ name: 'id', referencedColumnName: 'store_id' }) 24 | orders: Order[]; 25 | 26 | @OneToMany(() => Invite, (invite) => invite.store) 27 | @JoinColumn({ name: 'id', referencedColumnName: 'store_id' }) 28 | invites: Invite[]; 29 | 30 | @OneToMany(() => Role, (role) => role.store) 31 | @JoinColumn({ name: 'id', referencedColumnName: 'store_id' }) 32 | roles: Role[]; 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Medusa Marketplace Tutorial 2 | 3 | > :warning: this repository is deprecated and it's recommended to check out [this one](https://github.com/shahednasser/medusa-1.8-marketplace-tutorial) instead. 4 | 5 | Code for Medusa Marketplace Tutorial using [Medusa Extender](https://github.com/adrien2p/medusa-extender). 6 | 7 | This includes the full Medusa server. If you want to install the marketplace into an existing Medusa server, please check out the [Medusa Marketplace plugin](https://github.com/shahednasser/medusa-marketplace) instead. 8 | 9 | ## Prerequisites 10 | 11 | Before you run this code you'll need [PostgreSQL](https://www.postgresql.org/download/) and [Redis](https://redis.io/download) installed. 12 | 13 | ## Installation 14 | 15 | After cloning the repository, install the dependencies: 16 | 17 | ```bash 18 | npm i 19 | ``` 20 | 21 | ## Configuration 22 | 23 | Copy `.env.example` to `.env` and add your database and Redis configurations as necessary. 24 | 25 | ## Seed and Migrate Database 26 | 27 | Run the following command to seed the database: 28 | 29 | ```bash 30 | npm run seed 31 | ``` 32 | 33 | Before running migrations, make sure to run the build command: 34 | 35 | ```bash 36 | npm run build 37 | ``` 38 | 39 | Then run the migrations 40 | 41 | ```bash 42 | ./node_modules/.bin/medex migrate --run 43 | ``` 44 | 45 | ## Running the Server 46 | 47 | To run the server run the following command: 48 | 49 | ```bash 50 | npm start 51 | ``` 52 | -------------------------------------------------------------------------------- /src/modules/role/role.entity.ts: -------------------------------------------------------------------------------- 1 | import { BeforeInsert, Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm"; 2 | 3 | import { BaseEntity } from "@medusajs/medusa"; 4 | import { Entity as MedusaEntity } from "medusa-extender"; 5 | import { Permission } from '../permission/permission.entity'; 6 | import { Store } from "../store/entities/store.entity"; 7 | import { User } from "../user/entities/user.entity"; 8 | import { generateEntityId } from "@medusajs/medusa/dist/utils"; 9 | 10 | @MedusaEntity() 11 | @Entity() 12 | export class Role extends BaseEntity { 13 | 14 | @Column({type: "varchar"}) 15 | name: string; 16 | 17 | @Index() 18 | @Column({ nullable: true }) 19 | store_id: string; 20 | 21 | @ManyToMany(() => Permission) 22 | @JoinTable({ 23 | name: "role_permissions", 24 | joinColumn: { 25 | name: "role_id", 26 | referencedColumnName: "id", 27 | }, 28 | inverseJoinColumn: { 29 | name: "permission_id", 30 | referencedColumnName: "id", 31 | }, 32 | }) 33 | permissions: Permission[] 34 | 35 | @OneToMany(() => User, (user) => user.teamRole) 36 | @JoinColumn({ name: 'id', referencedColumnName: 'role_id' }) 37 | users: User[]; 38 | 39 | @ManyToOne(() => Store, (store) => store.roles) 40 | @JoinColumn({ name: 'store_id' }) 41 | store: Store; 42 | 43 | @BeforeInsert() 44 | private beforeInsert(): void { 45 | this.id = generateEntityId(this.id, "role") 46 | } 47 | } -------------------------------------------------------------------------------- /src/services/README.md: -------------------------------------------------------------------------------- 1 | # Custom services 2 | 3 | You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`. 4 | 5 | ```js 6 | // my.js 7 | 8 | import { BaseService } from "medusa-interfaces"; 9 | 10 | class MyService extends BaseService { 11 | constructor({ productService }) { 12 | super(); 13 | 14 | this.productService_ = productService 15 | } 16 | 17 | async getProductMessage() { 18 | const [product] = await this.productService_.list({}, { take: 1 }) 19 | 20 | return `Welcome to ${product.title}!` 21 | } 22 | } 23 | 24 | export default MyService; 25 | ``` 26 | 27 | The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my.js` is registered as `myService`, `custom-thing.js` is registerd as `customThingService`. 28 | 29 | You may use the services you define here in custom endpoints by resolving the services defined. 30 | 31 | ```js 32 | import { Router } from "express" 33 | 34 | export default () => { 35 | const router = Router() 36 | 37 | router.get("/hello-product", async (req, res) => { 38 | const myService = req.scope.resolve("myService") 39 | 40 | res.json({ 41 | message: await myService.getProductMessage() 42 | }) 43 | }) 44 | 45 | return router; 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /src/modules/invite/acceptInvite.controller.ts: -------------------------------------------------------------------------------- 1 | import { AdminPostInvitesInviteAcceptReq } from "@medusajs/medusa" 2 | import { EntityManager } from "typeorm"; 3 | import { InviteService } from './invite.service'; 4 | import { MedusaError } from 'medusa-core-utils'; 5 | import UserService from '../user/services/user.service'; 6 | import { validator } from "@medusajs/medusa/dist/utils/validator" 7 | 8 | export default async (req, res) => { 9 | const validated = await validator(AdminPostInvitesInviteAcceptReq, req.body) 10 | 11 | const inviteService: InviteService = req.scope.resolve(InviteService.resolutionKey) 12 | 13 | const manager: EntityManager = req.scope.resolve("manager") 14 | 15 | await manager.transaction(async (m) => { 16 | //retrieve invite 17 | let decoded 18 | try { 19 | decoded = inviteService 20 | .withTransaction(m) 21 | .verifyToken(validated.token) 22 | } catch (err) { 23 | throw new MedusaError( 24 | MedusaError.Types.INVALID_DATA, 25 | "Token is not valid" 26 | ) 27 | } 28 | 29 | const invite = await inviteService 30 | .withTransaction(m) 31 | .retrieve(decoded.invite_id); 32 | 33 | let store_id = invite ? invite.store_id : null; 34 | 35 | const user = await inviteService 36 | .withTransaction(m) 37 | .accept(validated.token, validated.user); 38 | 39 | if (store_id) { 40 | const userService: UserService = req.scope.resolve("userService"); 41 | await userService 42 | .withTransaction(m) 43 | .addUserToStore(user.id, store_id); 44 | } 45 | 46 | res.sendStatus(200) 47 | }) 48 | } -------------------------------------------------------------------------------- /medusa-config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | 3 | let ENV_FILE_NAME = ''; 4 | switch (process.env.NODE_ENV) { 5 | case 'prod': 6 | ENV_FILE_NAME = '.env'; 7 | break; 8 | case 'test': 9 | ENV_FILE_NAME = '.env.test'; 10 | break; 11 | default: 12 | ENV_FILE_NAME = '.env'; 13 | break; 14 | } 15 | 16 | dotenv.config({ path: process.cwd() + '/' + ENV_FILE_NAME }); 17 | 18 | const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; 19 | const PORT = process.env.PORT || 3000; 20 | const ADMIN_CORS = process.env.ADMIN_CORS || 'http://localhost:7000,http://localhost:7001'; 21 | const STORE_CORS = process.env.STORE_CORS || 'http://localhost:8000'; 22 | 23 | const plugins = [ 24 | `medusa-fulfillment-manual`, 25 | `medusa-payment-manual`, 26 | { 27 | resolve: `medusa-file-minio`, 28 | options: { 29 | endpoint: process.env.MINIO_SERVER, 30 | bucket: process.env.MINIO_BUCKET, 31 | access_key_id: process.env.MINIO_ACCESS_KEY, 32 | secret_access_key: process.env.MINIO_SECRET_KEY, 33 | }, 34 | }, 35 | ]; 36 | 37 | module.exports = { 38 | serverConfig: { 39 | port: PORT, 40 | }, 41 | projectConfig: { 42 | // For more production-like environment install PostgresQL 43 | jwtSecret: process.env.JWT_SECRET, 44 | cookieSecret: process.env.COOKIE_SECRET, 45 | 46 | database_url: `postgres://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_SCHEME}`, 47 | database_type: 'postgres', 48 | store_cors: STORE_CORS, 49 | admin_cors: ADMIN_CORS, 50 | redis_url: REDIS_URL, 51 | cli_migration_dirs: [ 52 | 'dist/**/*.migration.js' 53 | ] 54 | }, 55 | plugins, 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-starter-default", 3 | "version": "0.0.1", 4 | "description": "A starter for Medusa projects.", 5 | "author": "Sebastian Rindom ", 6 | "license": "MIT", 7 | "scripts": { 8 | "seed": "medusa seed -f ./data/seed.json", 9 | "build": "rm -rf dist && ./node_modules/.bin/tsc -p tsconfig.json", 10 | "start": "npm run build && NODE_ENV=development node ./dist/main.js", 11 | "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", 12 | "start:watch": "nodemon --watch './src/**/*.ts' --exec 'ts-node' ./src/main.ts", 13 | "start:prod": "npm run build && NODE_ENV=production node dist/main" 14 | }, 15 | "dependencies": { 16 | "@medusajs/medusa": "^1.3.1", 17 | "@medusajs/medusa-cli": "^1.3.0", 18 | "@types/jsonwebtoken": "^8.5.8", 19 | "awilix": "4.2.3", 20 | "medusa-extender": "^1.7.2", 21 | "medusa-file-minio": "^1.0.4", 22 | "medusa-fulfillment-manual": "^1.1.26", 23 | "medusa-interfaces": "^1.3.0", 24 | "medusa-payment-manual": "^1.0.8", 25 | "medusa-payment-stripe": "^1.1.30", 26 | "mongoose": "^5.13.3", 27 | "typeorm": "^0.2.45" 28 | }, 29 | "repository": "https://github.com/medusajs/medusa-starter-default.git", 30 | "keywords": [ 31 | "sqlite", 32 | "ecommerce", 33 | "headless", 34 | "medusa" 35 | ], 36 | "devDependencies": { 37 | "@babel/cli": "^7.14.3", 38 | "@babel/core": "^7.14.3", 39 | "@babel/preset-typescript": "^7.14.5", 40 | "babel-preset-medusa-package": "^1.1.13", 41 | "nodemon": "^2.0.15", 42 | "ts-node": "^10.7.0", 43 | "typescript": "^4.5.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/order/order.service.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from 'typeorm'; 2 | import { OrderService as MedusaOrderService } from "@medusajs/medusa/dist/services"; 3 | import { OrderRepository } from './order.repository'; 4 | import { Service } from 'medusa-extender'; 5 | import { User } from "../user/entities/user.entity"; 6 | 7 | type InjectedDependencies = { 8 | manager: EntityManager; 9 | orderRepository: typeof OrderRepository; 10 | customerService: any; 11 | paymentProviderService: any; 12 | shippingOptionService: any; 13 | shippingProfileService: any; 14 | discountService: any; 15 | fulfillmentProviderService: any; 16 | fulfillmentService: any; 17 | lineItemService: any; 18 | totalsService: any; 19 | regionService: any; 20 | cartService: any; 21 | addressRepository: any; 22 | giftCardService: any; 23 | draftOrderService: any; 24 | inventoryService: any; 25 | eventBusService: any; 26 | loggedInUser?: User; 27 | orderService: OrderService; 28 | }; 29 | 30 | @Service({ scope: 'SCOPED', override: MedusaOrderService }) 31 | export class OrderService extends MedusaOrderService { 32 | private readonly manager: EntityManager; 33 | private readonly container: InjectedDependencies; 34 | 35 | constructor(container: InjectedDependencies) { 36 | super(container); 37 | 38 | this.manager = container.manager; 39 | this.container = container; 40 | } 41 | 42 | buildQuery_(selector: object, config: {relations: string[], select: string[]}): object { 43 | if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) { 44 | selector['store_id'] = this.container.loggedInUser.store_id; 45 | } 46 | 47 | config.select.push('store_id') 48 | 49 | config.relations = config.relations ?? [] 50 | 51 | config.relations.push("children", "parent", "store") 52 | 53 | return super.buildQuery_(selector, config); 54 | } 55 | } -------------------------------------------------------------------------------- /src/modules/permission/1655131601491-permission.migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, TableForeignKey } from 'typeorm'; 2 | 3 | import { Migration } from 'medusa-extender'; 4 | 5 | @Migration() 6 | export class PermissionMigration1655131601491 implements MigrationInterface { 7 | name = 'PermissionMigration1655131601491'; 8 | 9 | public async up(queryRunner: QueryRunner): Promise { 10 | let query = ` 11 | CREATE TABLE "permission" ("id" character varying NOT NULL, 12 | "name" character varying NOT NULL, "metadata" jsonb, 13 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now())`; 14 | await queryRunner.query(query); 15 | 16 | await queryRunner.createPrimaryKey("permission", ["id"]) 17 | 18 | query = ` 19 | CREATE TABLE "role_permissions" ("role_id" character varying NOT NULL, "permission_id" character varying NOT NULL, 20 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now())`; 21 | 22 | await queryRunner.query(query); 23 | 24 | await queryRunner.createPrimaryKey("role_permissions", ["role_id", "permission_id"]) 25 | 26 | await queryRunner.createForeignKey("role_permissions", new TableForeignKey({ 27 | columnNames: ["role_id"], 28 | referencedColumnNames: ["id"], 29 | referencedTableName: "role", 30 | onDelete: "CASCADE", 31 | onUpdate: "CASCADE" 32 | })) 33 | 34 | await queryRunner.createForeignKey("role_permissions", new TableForeignKey({ 35 | columnNames: ["permission_id"], 36 | referencedColumnNames: ["id"], 37 | referencedTableName: "permission", 38 | onDelete: "CASCADE", 39 | onUpdate: "CASCADE" 40 | })) 41 | } 42 | 43 | public async down(queryRunner: QueryRunner): Promise { 44 | await queryRunner.dropTable("role_permissions", true); 45 | await queryRunner.dropTable("permission", true); 46 | } 47 | } -------------------------------------------------------------------------------- /src/modules/product/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { EntityEventType, MedusaEventHandlerParams, OnMedusaEntityEvent, Service } from 'medusa-extender'; 2 | 3 | import { EntityManager } from "typeorm"; 4 | import { ProductService as MedusaProductService } from '@medusajs/medusa/dist/services'; 5 | import { Product } from '../entities/product.entity'; 6 | import { User } from '../../user/entities/user.entity'; 7 | import UserService from '../../user/services/user.service'; 8 | 9 | type ConstructorParams = { 10 | manager: any; 11 | loggedInUser?: User; 12 | productRepository: any; 13 | productVariantRepository: any; 14 | productOptionRepository: any; 15 | eventBusService: any; 16 | productVariantService: any; 17 | productCollectionService: any; 18 | productTypeRepository: any; 19 | productTagRepository: any; 20 | imageRepository: any; 21 | searchService: any; 22 | userService: UserService; 23 | cartRepository: any; 24 | priceSelectionStrategy: any; 25 | } 26 | 27 | @Service({ scope: 'SCOPED', override: MedusaProductService }) 28 | export class ProductService extends MedusaProductService { 29 | readonly #manager: EntityManager; 30 | 31 | constructor(private readonly container: ConstructorParams) { 32 | super(container); 33 | this.#manager = container.manager; 34 | } 35 | 36 | @OnMedusaEntityEvent.Before.Insert(Product, { async: true }) 37 | public async attachStoreToProduct( 38 | params: MedusaEventHandlerParams 39 | ): Promise> { 40 | const { event } = params; 41 | const loggedInUser = this.container.loggedInUser; 42 | event.entity.store_id = loggedInUser.store_id; 43 | return event; 44 | } 45 | 46 | prepareListQuery_(selector: object, config: object): object { 47 | const loggedInUser = Object.keys(this.container).includes('loggedInUser') ? this.container.loggedInUser : null 48 | if (loggedInUser) { 49 | selector['store_id'] = loggedInUser.store_id 50 | } 51 | 52 | return super.prepareListQuery_(selector, config); 53 | } 54 | } -------------------------------------------------------------------------------- /src/modules/invite/invite.service.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule } from '@medusajs/medusa/dist/types/global'; 2 | import { EntityManager } from 'typeorm'; 3 | import { EventBusService } from '@medusajs/medusa'; 4 | import { Invite } from './invite.entity'; 5 | import { InviteRepository } from './invite.repository'; 6 | import { default as MedusaInviteService } from "@medusajs/medusa/dist/services/invite"; 7 | import { Service } from "medusa-extender"; 8 | import { User } from '../user/entities/user.entity'; 9 | import UserRepository from '../user/repositories/user.repository'; 10 | import UserService from '../user/services/user.service'; 11 | 12 | type InviteServiceProps = { 13 | manager: EntityManager; 14 | userService: UserService; 15 | userRepository: UserRepository; 16 | eventBusService: EventBusService; 17 | loggedInUser?: User; 18 | inviteRepository: InviteRepository; 19 | } 20 | 21 | @Service({ scope: 'SCOPED', override: MedusaInviteService }) 22 | export class InviteService extends MedusaInviteService { 23 | static readonly resolutionKey = "inviteService" 24 | 25 | private readonly manager: EntityManager; 26 | private readonly container: InviteServiceProps; 27 | private readonly inviteRepository: InviteRepository; 28 | 29 | constructor(container: InviteServiceProps, configModule: ConfigModule) { 30 | super(container, configModule); 31 | 32 | this.manager = container.manager; 33 | this.container = container; 34 | this.inviteRepository = container.inviteRepository 35 | } 36 | 37 | withTransaction(transactionManager: EntityManager): InviteService { 38 | if (!transactionManager) { 39 | return this 40 | } 41 | 42 | const cloned = new InviteService({ 43 | ...this.container, 44 | manager: transactionManager 45 | }, 46 | this.configModule_ 47 | ) 48 | 49 | cloned.transactionManager = transactionManager 50 | 51 | return cloned 52 | } 53 | 54 | async retrieve (invite_id: string) : Promise { 55 | return await this.atomicPhase_(async (m) => { 56 | const inviteRepo: InviteRepository = m.getCustomRepository( 57 | this.inviteRepository 58 | ) 59 | 60 | return await inviteRepo.findOne({ where: { id: invite_id } }) 61 | 62 | }) 63 | } 64 | 65 | buildQuery_(selector, config = {}): object { 66 | if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) { 67 | selector['store_id'] = this.container.loggedInUser.store_id; 68 | } 69 | 70 | return super.buildQuery_(selector, config); 71 | } 72 | } -------------------------------------------------------------------------------- /src/modules/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from 'typeorm'; 2 | import EventBusService from '@medusajs/medusa/dist/services/event-bus'; 3 | import { FindConfig } from '@medusajs/medusa/dist/types/common'; 4 | import { MedusaError } from 'medusa-core-utils'; 5 | import { UserService as MedusaUserService } from '@medusajs/medusa/dist/services'; 6 | import { Service } from 'medusa-extender'; 7 | import { User } from '../entities/user.entity'; 8 | import UserRepository from '../repositories/user.repository'; 9 | 10 | type ConstructorParams = { 11 | manager: EntityManager; 12 | userRepository: typeof UserRepository; 13 | eventBusService: EventBusService; 14 | loggedInUser?: User; 15 | }; 16 | 17 | @Service({ scope: 'SCOPED', override: MedusaUserService }) 18 | export default class UserService extends MedusaUserService { 19 | private readonly manager: EntityManager; 20 | private readonly userRepository: typeof UserRepository; 21 | private readonly eventBus: EventBusService; 22 | private readonly container: ConstructorParams; 23 | 24 | constructor(container: ConstructorParams) { 25 | super(container); 26 | this.manager = container.manager; 27 | this.userRepository = container.userRepository; 28 | this.eventBus = container.eventBusService; 29 | this.container = container; 30 | } 31 | 32 | withTransaction(transactionManager: EntityManager): UserService { 33 | if (!transactionManager) { 34 | return this 35 | } 36 | 37 | const cloned = new UserService({ 38 | ...this.container, 39 | manager: transactionManager 40 | }) 41 | 42 | cloned.transactionManager = transactionManager 43 | 44 | return cloned 45 | } 46 | 47 | public async retrieve(userId: string, config?: FindConfig): Promise { 48 | const userRepo = this.manager.getCustomRepository(this.userRepository); 49 | const validatedId = this.validateId_(userId); 50 | const query = this.buildQuery_({ id: validatedId }, config); 51 | 52 | const user = await userRepo.findOne(query); 53 | 54 | if (!user) { 55 | throw new MedusaError(MedusaError.Types.NOT_FOUND, `User with id: ${userId} was not found`); 56 | } 57 | 58 | return user as User; 59 | } 60 | 61 | buildQuery_(selector, config = {}): object { 62 | if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) { 63 | selector['store_id'] = this.container.loggedInUser.store_id; 64 | } 65 | 66 | return super.buildQuery_(selector, config); 67 | } 68 | 69 | public async addUserToStore (user_id, store_id) { 70 | await this.atomicPhase_(async (m) => { 71 | const userRepo = m.getCustomRepository(this.userRepository); 72 | const query = this.buildQuery_({ id: user_id }); 73 | 74 | const user = await userRepo.findOne(query); 75 | if (user) { 76 | user.store_id = store_id; 77 | await userRepo.save(user); 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/store/services/store.service.ts: -------------------------------------------------------------------------------- 1 | import { EntityEventType, MedusaEventHandlerParams, OnMedusaEntityEvent, Service } from 'medusa-extender'; 2 | 3 | import { CurrencyRepository } from '@medusajs/medusa/dist/repositories/currency'; 4 | import { EntityManager } from 'typeorm'; 5 | import EventBusService from '@medusajs/medusa/dist/services/event-bus'; 6 | import { Invite } from '../../invite/invite.entity'; 7 | import { StoreService as MedusaStoreService } from '@medusajs/medusa/dist/services'; 8 | import { Store } from '../entities/store.entity'; 9 | import StoreRepository from '../repositories/store.repository'; 10 | import { User } from '../../user/entities/user.entity'; 11 | 12 | interface ConstructorParams { 13 | loggedInUser?: User; 14 | manager: EntityManager; 15 | storeRepository: typeof StoreRepository; 16 | currencyRepository: typeof CurrencyRepository; 17 | eventBusService: EventBusService; 18 | } 19 | 20 | @Service({ override: MedusaStoreService, scope: 'SCOPED' }) 21 | export default class StoreService extends MedusaStoreService { 22 | private readonly manager: EntityManager; 23 | private readonly storeRepository: typeof StoreRepository; 24 | 25 | constructor(private readonly container: ConstructorParams) { 26 | super(container); 27 | this.manager = container.manager; 28 | this.storeRepository = container.storeRepository; 29 | } 30 | 31 | withTransaction(transactionManager: EntityManager): StoreService { 32 | if (!transactionManager) { 33 | return this; 34 | } 35 | 36 | const cloned = new StoreService({ 37 | ...this.container, 38 | manager: transactionManager, 39 | }); 40 | 41 | cloned.transactionManager_ = transactionManager; 42 | 43 | return cloned; 44 | } 45 | 46 | @OnMedusaEntityEvent.Before.Insert(User, { async: true }) 47 | public async createStoreForNewUser( 48 | params: MedusaEventHandlerParams 49 | ): Promise> { 50 | const { event } = params; 51 | let store_id = Object.keys(this.container).includes("loggedInUser") 52 | ? this.container.loggedInUser.store_id 53 | : null; 54 | if (!store_id) { 55 | const createdStore = await this.withTransaction(event.manager).createForUser(event.entity); 56 | if (!!createdStore) { 57 | store_id = createdStore.id; 58 | } 59 | } 60 | 61 | event.entity.store_id = store_id; 62 | 63 | return event; 64 | } 65 | 66 | @OnMedusaEntityEvent.Before.Insert(Invite, { async: true }) 67 | public async addStoreToInvite( 68 | params: MedusaEventHandlerParams 69 | ): Promise> { 70 | const { event } = params; 71 | let store_id = this.container.loggedInUser.store_id 72 | 73 | if (!event.entity.store_id && store_id) { 74 | event.entity.store_id = store_id; 75 | } 76 | 77 | return event; 78 | } 79 | 80 | /** 81 | * Create a store for a particular user. It mainly used from the event BeforeInsert to create a store 82 | * for the user that is being inserting. 83 | * @param user 84 | */ 85 | public async createForUser(user: User): Promise { 86 | if (user.store_id) { 87 | return; 88 | } 89 | const storeRepo = this.manager.getCustomRepository(this.storeRepository); 90 | const store = storeRepo.create() as Store; 91 | return storeRepo.save(store); 92 | } 93 | 94 | /** 95 | * Return the store that belongs to the authenticated user. 96 | * @param relations 97 | */ 98 | public async retrieve(relations: string[] = []) { 99 | if (!Object.keys(this.container).includes('loggedInUser')) { 100 | return super.retrieve(relations); 101 | } 102 | 103 | const storeRepo = this.manager.getCustomRepository(this.storeRepository); 104 | const store = await storeRepo.findOne({ 105 | relations, 106 | join: { alias: 'store', innerJoin: { members: 'store.members' } }, 107 | where: (qb) => { 108 | qb.where('members.id = :memberId', { memberId: this.container.loggedInUser.id }); 109 | }, 110 | }); 111 | 112 | if (!store) { 113 | throw new Error('Unable to find the user store'); 114 | } 115 | 116 | return store; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/modules/order/order.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { EventBusService, OrderService } from "@medusajs/medusa/dist/services"; 2 | import { LineItem, OrderStatus } from '@medusajs/medusa'; 3 | 4 | import { EntityManager } from "typeorm"; 5 | import { LineItemRepository } from '@medusajs/medusa/dist/repositories/line-item'; 6 | import { Order } from './order.entity'; 7 | import { OrderRepository } from "./order.repository"; 8 | import { PaymentRepository } from "@medusajs/medusa/dist/repositories/payment"; 9 | import { Product } from "../product/entities/product.entity"; 10 | import { ProductService } from './../product/services/product.service'; 11 | import { ShippingMethodRepository } from "@medusajs/medusa/dist/repositories/shipping-method"; 12 | import { Subscriber } from 'medusa-extender'; 13 | 14 | type InjectedDependencies = { 15 | eventBusService: EventBusService; 16 | orderService: OrderService; 17 | orderRepository: typeof OrderRepository; 18 | productService: ProductService; 19 | manager: EntityManager; 20 | lineItemRepository: typeof LineItemRepository; 21 | shippingMethodRepository: typeof ShippingMethodRepository; 22 | paymentRepository: typeof PaymentRepository; 23 | }; 24 | 25 | @Subscriber() 26 | export class OrderSubscriber { 27 | private readonly manager: EntityManager; 28 | private readonly eventBusService: EventBusService; 29 | private readonly orderService: OrderService; 30 | private readonly orderRepository: typeof OrderRepository; 31 | private readonly productService: ProductService; 32 | private readonly lineItemRepository: typeof LineItemRepository; 33 | private readonly shippingMethodRepository: typeof ShippingMethodRepository; 34 | 35 | constructor({ eventBusService, orderService, orderRepository, productService, manager, lineItemRepository, shippingMethodRepository, paymentRepository}: InjectedDependencies) { 36 | this.eventBusService = eventBusService; 37 | this.orderService = orderService; 38 | this.orderRepository = orderRepository; 39 | this.productService = productService; 40 | this.manager = manager; 41 | this.lineItemRepository = lineItemRepository; 42 | this.shippingMethodRepository = shippingMethodRepository; 43 | this.eventBusService.subscribe( 44 | OrderService.Events.PLACED, 45 | this.handleOrderPlaced.bind(this) 46 | ); 47 | 48 | //add handler for different status changes 49 | this.eventBusService.subscribe( 50 | OrderService.Events.CANCELED, 51 | this.checkStatus.bind(this) 52 | ); 53 | this.eventBusService.subscribe( 54 | OrderService.Events.UPDATED, 55 | this.checkStatus.bind(this) 56 | ); 57 | this.eventBusService.subscribe( 58 | OrderService.Events.COMPLETED, 59 | this.checkStatus.bind(this) 60 | ); 61 | } 62 | 63 | private async handleOrderPlaced({ id }: {id: string}): Promise { 64 | //create child orders 65 | //retrieve order 66 | const order: Order = await this.orderService.retrieve(id, { 67 | relations: ['items', 'items.variant', 'cart', 'shipping_methods', 'payments'] 68 | }); 69 | //group items by store id 70 | const groupedItems = {}; 71 | 72 | for (const item of order.items) { 73 | const product: Product = await this.productService.retrieve(item.variant.product_id, { select: ['store_id']}); 74 | const store_id = product.store_id; 75 | if (!store_id) { 76 | continue; 77 | } 78 | if (!groupedItems.hasOwnProperty(store_id)) { 79 | groupedItems[store_id] = []; 80 | } 81 | 82 | groupedItems[store_id].push(item); 83 | } 84 | 85 | const orderRepo = this.manager.getCustomRepository(this.orderRepository); 86 | const lineItemRepo = this.manager.getCustomRepository(this.lineItemRepository); 87 | const shippingMethodRepo = this.manager.getCustomRepository(this.shippingMethodRepository); 88 | 89 | for (const store_id in groupedItems) { 90 | //create order 91 | const childOrder = orderRepo.create({ 92 | ...order, 93 | order_parent_id: id, 94 | store_id: store_id, 95 | cart_id: null, 96 | cart: null, 97 | id: null, 98 | shipping_methods: [] 99 | }) as Order; 100 | const orderResult = await orderRepo.save(childOrder); 101 | 102 | //create shipping methods 103 | for (const shippingMethod of order.shipping_methods) { 104 | const newShippingMethod = shippingMethodRepo.create({ 105 | ...shippingMethod, 106 | id: null, 107 | cart_id: null, 108 | cart: null, 109 | order_id: orderResult.id 110 | }); 111 | 112 | await shippingMethodRepo.save(newShippingMethod); 113 | } 114 | 115 | //create line items 116 | const items: LineItem[] = groupedItems[store_id]; 117 | for (const item of items) { 118 | const newItem = lineItemRepo.create({ 119 | ...item, 120 | id: null, 121 | order_id: orderResult.id, 122 | cart_id: null 123 | }) 124 | await lineItemRepo.save(newItem); 125 | } 126 | } 127 | } 128 | 129 | public async checkStatus({ id }: {id: string}): Promise { 130 | //retrieve order 131 | const order: Order = await this.orderService.retrieve(id); 132 | 133 | if (order.order_parent_id) { 134 | //retrieve parent 135 | const orderRepo = this.manager.getCustomRepository(this.orderRepository); 136 | const parentOrder = await this.orderService.retrieve(order.order_parent_id, { 137 | relations: ['children'] 138 | }); 139 | 140 | const newStatus = this.getStatusFromChildren(parentOrder); 141 | if (newStatus !== parentOrder.status) { 142 | switch (newStatus) { 143 | case OrderStatus.CANCELED: 144 | this.orderService.cancel(parentOrder.id); 145 | break; 146 | case OrderStatus.ARCHIVED: 147 | this.orderService.archive(parentOrder.id); 148 | break; 149 | case OrderStatus.COMPLETED: 150 | this.orderService.completeOrder(parentOrder.id); 151 | break; 152 | default: 153 | parentOrder.status = newStatus; 154 | parentOrder.fulfillment_status = newStatus; 155 | parentOrder.payment_status = newStatus; 156 | await orderRepo.save(parentOrder); 157 | } 158 | } 159 | } 160 | } 161 | 162 | public getStatusFromChildren (order: Order): string { 163 | if (!order.children) { 164 | return order.status; 165 | } 166 | 167 | //collect all statuses 168 | let statuses = order.children.map((child) => child.status); 169 | 170 | //remove duplicate statuses 171 | statuses = [...new Set(statuses)]; 172 | 173 | if (statuses.length === 1) { 174 | return statuses[0]; 175 | } 176 | 177 | //remove archived and canceled orders 178 | statuses = statuses.filter((status) => status !== OrderStatus.CANCELED && status !== OrderStatus.ARCHIVED); 179 | 180 | if (!statuses.length) { 181 | //all child orders are archived or canceled 182 | return OrderStatus.CANCELED; 183 | } 184 | 185 | if (statuses.length === 1) { 186 | return statuses[0]; 187 | } 188 | 189 | //check if any order requires action 190 | const hasRequiresAction = statuses.some((status) => status === OrderStatus.REQUIRES_ACTION); 191 | if (hasRequiresAction) { 192 | return OrderStatus.REQUIRES_ACTION; 193 | } 194 | 195 | //since more than one status is left and we filtered out canceled, archived, 196 | //and requires action statuses, only pending and complete left. So, return pending 197 | return OrderStatus.PENDING; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /data/seed.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "currencies": ["eur", "usd"] 4 | }, 5 | "users": [ 6 | { 7 | "email": "admin@medusa-test.com", 8 | "password": "supersecret" 9 | } 10 | ], 11 | "regions": [ 12 | { 13 | "id": "test-region-eu", 14 | "name": "EU", 15 | "currency_code": "eur", 16 | "tax_rate": 0, 17 | "payment_providers": ["manual"], 18 | "fulfillment_providers": ["manual"], 19 | "countries": ["gb", "de", "dk", "se", "fr", "es", "it"] 20 | }, 21 | { 22 | "id": "test-region-na", 23 | "name": "NA", 24 | "currency_code": "usd", 25 | "tax_rate": 0, 26 | "payment_providers": ["manual"], 27 | "fulfillment_providers": ["manual"], 28 | "countries": ["us", "ca"] 29 | } 30 | ], 31 | "shipping_options": [ 32 | { 33 | "name": "PostFake Standard", 34 | "region_id": "test-region-eu", 35 | "provider_id": "manual", 36 | "data": { 37 | "id": "manual-fulfillment" 38 | }, 39 | "price_type": "flat_rate", 40 | "amount": 1000 41 | }, 42 | { 43 | "name": "PostFake Express", 44 | "region_id": "test-region-eu", 45 | "provider_id": "manual", 46 | "data": { 47 | "id": "manual-fulfillment" 48 | }, 49 | "price_type": "flat_rate", 50 | "amount": 1500 51 | }, 52 | { 53 | "name": "PostFake Return", 54 | "region_id": "test-region-eu", 55 | "provider_id": "manual", 56 | "data": { 57 | "id": "manual-fulfillment" 58 | }, 59 | "price_type": "flat_rate", 60 | "is_return": true, 61 | "amount": 1000 62 | }, 63 | { 64 | "name": "I want to return it myself", 65 | "region_id": "test-region-eu", 66 | "provider_id": "manual", 67 | "data": { 68 | "id": "manual-fulfillment" 69 | }, 70 | "price_type": "flat_rate", 71 | "is_return": true, 72 | "amount": 0 73 | }, 74 | { 75 | "name": "FakeEx Standard", 76 | "region_id": "test-region-na", 77 | "provider_id": "manual", 78 | "data": { 79 | "id": "manual-fulfillment" 80 | }, 81 | "price_type": "flat_rate", 82 | "amount": 800 83 | }, 84 | { 85 | "name": "FakeEx Express", 86 | "region_id": "test-region-na", 87 | "provider_id": "manual", 88 | "data": { 89 | "id": "manual-fulfillment" 90 | }, 91 | "price_type": "flat_rate", 92 | "amount": 1200 93 | }, 94 | { 95 | "name": "FakeEx Return", 96 | "region_id": "test-region-na", 97 | "provider_id": "manual", 98 | "data": { 99 | "id": "manual-fulfillment" 100 | }, 101 | "price_type": "flat_rate", 102 | "is_return": true, 103 | "amount": 800 104 | }, 105 | { 106 | "name": "I want to return it myself", 107 | "region_id": "test-region-na", 108 | "provider_id": "manual", 109 | "data": { 110 | "id": "manual-fulfillment" 111 | }, 112 | "price_type": "flat_rate", 113 | "is_return": true, 114 | "amount": 0 115 | } 116 | ], 117 | "products": [ 118 | { 119 | "title": "Medusa T-Shirt", 120 | "subtitle": null, 121 | "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", 122 | "handle": "t-shirt", 123 | "is_giftcard": false, 124 | "weight": 400, 125 | "images": [ 126 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", 127 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", 128 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", 129 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png" 130 | ], 131 | "options": [ 132 | { 133 | "title": "Size", 134 | "values": ["S", "M", "L", "XL"] 135 | }, 136 | { 137 | "title": "Color", 138 | "values": ["Black", "White"] 139 | } 140 | ], 141 | "variants": [ 142 | { 143 | "title": "S / Black", 144 | "prices": [ 145 | { 146 | "currency_code": "eur", 147 | "amount": 1950 148 | }, 149 | { 150 | "currency_code": "usd", 151 | "amount": 2200 152 | } 153 | ], 154 | "options": [ 155 | { 156 | "value": "S" 157 | }, 158 | { 159 | "value": "Black" 160 | } 161 | ], 162 | "inventory_quantity": 100, 163 | "manage_inventory": true 164 | }, 165 | { 166 | "title": "S / White", 167 | "prices": [ 168 | { 169 | "currency_code": "eur", 170 | "amount": 1950 171 | }, 172 | { 173 | "currency_code": "usd", 174 | "amount": 2200 175 | } 176 | ], 177 | "options": [ 178 | { 179 | "value": "S" 180 | }, 181 | { 182 | "value": "White" 183 | } 184 | ], 185 | "inventory_quantity": 100, 186 | "manage_inventory": true 187 | }, 188 | { 189 | "title": "M / Black", 190 | "prices": [ 191 | { 192 | "currency_code": "eur", 193 | "amount": 1950 194 | }, 195 | { 196 | "currency_code": "usd", 197 | "amount": 2200 198 | } 199 | ], 200 | "options": [ 201 | { 202 | "value": "M" 203 | }, 204 | { 205 | "value": "Black" 206 | } 207 | ], 208 | "inventory_quantity": 100, 209 | "manage_inventory": true 210 | }, 211 | { 212 | "title": "M / White", 213 | "prices": [ 214 | { 215 | "currency_code": "eur", 216 | "amount": 1950 217 | }, 218 | { 219 | "currency_code": "usd", 220 | "amount": 2200 221 | } 222 | ], 223 | "options": [ 224 | { 225 | "value": "M" 226 | }, 227 | { 228 | "value": "White" 229 | } 230 | ], 231 | "inventory_quantity": 100, 232 | "manage_inventory": true 233 | }, 234 | { 235 | "title": "L / Black", 236 | "prices": [ 237 | { 238 | "currency_code": "eur", 239 | "amount": 1950 240 | }, 241 | { 242 | "currency_code": "usd", 243 | "amount": 2200 244 | } 245 | ], 246 | "options": [ 247 | { 248 | "value": "L" 249 | }, 250 | { 251 | "value": "Black" 252 | } 253 | ], 254 | "inventory_quantity": 100, 255 | "manage_inventory": true 256 | }, 257 | { 258 | "title": "L / White", 259 | "prices": [ 260 | { 261 | "currency_code": "eur", 262 | "amount": 1950 263 | }, 264 | { 265 | "currency_code": "usd", 266 | "amount": 2200 267 | } 268 | ], 269 | "options": [ 270 | { 271 | "value": "L" 272 | }, 273 | { 274 | "value": "White" 275 | } 276 | ], 277 | "inventory_quantity": 100, 278 | "manage_inventory": true 279 | }, 280 | { 281 | "title": "XL / Black", 282 | "prices": [ 283 | { 284 | "currency_code": "eur", 285 | "amount": 1950 286 | }, 287 | { 288 | "currency_code": "usd", 289 | "amount": 2200 290 | } 291 | ], 292 | "options": [ 293 | { 294 | "value": "XL" 295 | }, 296 | { 297 | "value": "Black" 298 | } 299 | ], 300 | "inventory_quantity": 100, 301 | "manage_inventory": true 302 | }, 303 | { 304 | "title": "XL / White", 305 | "prices": [ 306 | { 307 | "currency_code": "eur", 308 | "amount": 1950 309 | }, 310 | { 311 | "currency_code": "usd", 312 | "amount": 2200 313 | } 314 | ], 315 | "options": [ 316 | { 317 | "value": "XL" 318 | }, 319 | { 320 | "value": "White" 321 | } 322 | ], 323 | "inventory_quantity": 100, 324 | "manage_inventory": true 325 | } 326 | ] 327 | }, 328 | { 329 | "title": "Medusa Sweatshirt", 330 | "subtitle": null, 331 | "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", 332 | "handle": "sweatshirt", 333 | "is_giftcard": false, 334 | "weight": 400, 335 | "images": [ 336 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", 337 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png" 338 | ], 339 | "options": [ 340 | { 341 | "title": "Size", 342 | "values": ["S", "M", "L", "XL"] 343 | } 344 | ], 345 | "variants": [ 346 | { 347 | "title": "S", 348 | "prices": [ 349 | { 350 | "currency_code": "eur", 351 | "amount": 2950 352 | }, 353 | { 354 | "currency_code": "usd", 355 | "amount": 3350 356 | } 357 | ], 358 | "options": [ 359 | { 360 | "value": "S" 361 | } 362 | ], 363 | "inventory_quantity": 100, 364 | "manage_inventory": true 365 | }, 366 | { 367 | "title": "M", 368 | "prices": [ 369 | { 370 | "currency_code": "eur", 371 | "amount": 2950 372 | }, 373 | { 374 | "currency_code": "usd", 375 | "amount": 3350 376 | } 377 | ], 378 | "options": [ 379 | { 380 | "value": "M" 381 | } 382 | ], 383 | "inventory_quantity": 100, 384 | "manage_inventory": true 385 | }, 386 | { 387 | "title": "L", 388 | "prices": [ 389 | { 390 | "currency_code": "eur", 391 | "amount": 2950 392 | }, 393 | { 394 | "currency_code": "usd", 395 | "amount": 3350 396 | } 397 | ], 398 | "options": [ 399 | { 400 | "value": "L" 401 | } 402 | ], 403 | "inventory_quantity": 100, 404 | "manage_inventory": true 405 | }, 406 | { 407 | "title": "XL", 408 | "prices": [ 409 | { 410 | "currency_code": "eur", 411 | "amount": 2950 412 | }, 413 | { 414 | "currency_code": "usd", 415 | "amount": 3350 416 | } 417 | ], 418 | "options": [ 419 | { 420 | "value": "XL" 421 | } 422 | ], 423 | "inventory_quantity": 100, 424 | "manage_inventory": true 425 | } 426 | ] 427 | }, 428 | { 429 | "title": "Medusa Sweatpants", 430 | "subtitle": null, 431 | "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", 432 | "handle": "sweatpants", 433 | "is_giftcard": false, 434 | "weight": 400, 435 | "images": [ 436 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", 437 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png" 438 | ], 439 | "options": [ 440 | { 441 | "title": "Size", 442 | "values": ["S", "M", "L", "XL"] 443 | } 444 | ], 445 | "variants": [ 446 | { 447 | "title": "S", 448 | "prices": [ 449 | { 450 | "currency_code": "eur", 451 | "amount": 2950 452 | }, 453 | { 454 | "currency_code": "usd", 455 | "amount": 3350 456 | } 457 | ], 458 | "options": [ 459 | { 460 | "value": "S" 461 | } 462 | ], 463 | "inventory_quantity": 100, 464 | "manage_inventory": true 465 | }, 466 | { 467 | "title": "M", 468 | "prices": [ 469 | { 470 | "currency_code": "eur", 471 | "amount": 2950 472 | }, 473 | { 474 | "currency_code": "usd", 475 | "amount": 3350 476 | } 477 | ], 478 | "options": [ 479 | { 480 | "value": "M" 481 | } 482 | ], 483 | "inventory_quantity": 100, 484 | "manage_inventory": true 485 | }, 486 | { 487 | "title": "L", 488 | "prices": [ 489 | { 490 | "currency_code": "eur", 491 | "amount": 2950 492 | }, 493 | { 494 | "currency_code": "usd", 495 | "amount": 3350 496 | } 497 | ], 498 | "options": [ 499 | { 500 | "value": "L" 501 | } 502 | ], 503 | "inventory_quantity": 100, 504 | "manage_inventory": true 505 | }, 506 | { 507 | "title": "XL", 508 | "prices": [ 509 | { 510 | "currency_code": "eur", 511 | "amount": 2950 512 | }, 513 | { 514 | "currency_code": "usd", 515 | "amount": 3350 516 | } 517 | ], 518 | "options": [ 519 | { 520 | "value": "XL" 521 | } 522 | ], 523 | "inventory_quantity": 100, 524 | "manage_inventory": true 525 | } 526 | ] 527 | }, 528 | { 529 | "title": "Medusa Shorts", 530 | "subtitle": null, 531 | "description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", 532 | "handle": "shorts", 533 | "is_giftcard": false, 534 | "weight": 400, 535 | "images": [ 536 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", 537 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png" 538 | ], 539 | "options": [ 540 | { 541 | "title": "Size", 542 | "values": ["S", "M", "L", "XL"] 543 | } 544 | ], 545 | "variants": [ 546 | { 547 | "title": "S", 548 | "prices": [ 549 | { 550 | "currency_code": "eur", 551 | "amount": 2500 552 | }, 553 | { 554 | "currency_code": "usd", 555 | "amount": 2850 556 | } 557 | ], 558 | "options": [ 559 | { 560 | "value": "S" 561 | } 562 | ], 563 | "inventory_quantity": 100, 564 | "manage_inventory": true 565 | }, 566 | { 567 | "title": "M", 568 | "prices": [ 569 | { 570 | "currency_code": "eur", 571 | "amount": 2500 572 | }, 573 | { 574 | "currency_code": "usd", 575 | "amount": 2850 576 | } 577 | ], 578 | "options": [ 579 | { 580 | "value": "M" 581 | } 582 | ], 583 | "inventory_quantity": 100, 584 | "manage_inventory": true 585 | }, 586 | { 587 | "title": "L", 588 | "prices": [ 589 | { 590 | "currency_code": "eur", 591 | "amount": 2500 592 | }, 593 | { 594 | "currency_code": "usd", 595 | "amount": 2850 596 | } 597 | ], 598 | "options": [ 599 | { 600 | "value": "L" 601 | } 602 | ], 603 | "inventory_quantity": 100, 604 | "manage_inventory": true 605 | }, 606 | { 607 | "title": "XL", 608 | "prices": [ 609 | { 610 | "currency_code": "eur", 611 | "amount": 2500 612 | }, 613 | { 614 | "currency_code": "usd", 615 | "amount": 2850 616 | } 617 | ], 618 | "options": [ 619 | { 620 | "value": "XL" 621 | } 622 | ], 623 | "inventory_quantity": 100, 624 | "manage_inventory": true 625 | } 626 | ] 627 | }, 628 | { 629 | "title": "Medusa Hoodie", 630 | "subtitle": null, 631 | "description": "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.", 632 | "handle": "hoodie", 633 | "is_giftcard": false, 634 | "weight": 400, 635 | "images": [ 636 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png", 637 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png" 638 | ], 639 | "options": [ 640 | { 641 | "title": "Size", 642 | "values": ["S", "M", "L", "XL"] 643 | } 644 | ], 645 | "variants": [ 646 | { 647 | "title": "S", 648 | "prices": [ 649 | { 650 | "currency_code": "eur", 651 | "amount": 3650 652 | }, 653 | { 654 | "currency_code": "usd", 655 | "amount": 4150 656 | } 657 | ], 658 | "options": [ 659 | { 660 | "value": "S" 661 | } 662 | ], 663 | "inventory_quantity": 100, 664 | "manage_inventory": true 665 | }, 666 | { 667 | "title": "M", 668 | "prices": [ 669 | { 670 | "currency_code": "eur", 671 | "amount": 3650 672 | }, 673 | { 674 | "currency_code": "usd", 675 | "amount": 4150 676 | } 677 | ], 678 | "options": [ 679 | { 680 | "value": "M" 681 | } 682 | ], 683 | "inventory_quantity": 100, 684 | "manage_inventory": true 685 | }, 686 | { 687 | "title": "L", 688 | "prices": [ 689 | { 690 | "currency_code": "eur", 691 | "amount": 3650 692 | }, 693 | { 694 | "currency_code": "usd", 695 | "amount": 4150 696 | } 697 | ], 698 | "options": [ 699 | { 700 | "value": "L" 701 | } 702 | ], 703 | "inventory_quantity": 100, 704 | "manage_inventory": true 705 | }, 706 | { 707 | "title": "XL", 708 | "prices": [ 709 | { 710 | "currency_code": "eur", 711 | "amount": 3650 712 | }, 713 | { 714 | "currency_code": "usd", 715 | "amount": 4150 716 | } 717 | ], 718 | "options": [ 719 | { 720 | "value": "XL" 721 | } 722 | ], 723 | "inventory_quantity": 100, 724 | "manage_inventory": true 725 | } 726 | ] 727 | }, 728 | { 729 | "title": "Medusa Longsleeve", 730 | "subtitle": null, 731 | "description": "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.", 732 | "handle": "longsleeve", 733 | "is_giftcard": false, 734 | "weight": 400, 735 | "images": [ 736 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png", 737 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png" 738 | ], 739 | "options": [ 740 | { 741 | "title": "Size", 742 | "values": ["S", "M", "L", "XL"] 743 | } 744 | ], 745 | "variants": [ 746 | { 747 | "title": "S", 748 | "prices": [ 749 | { 750 | "currency_code": "eur", 751 | "amount": 3650 752 | }, 753 | { 754 | "currency_code": "usd", 755 | "amount": 4150 756 | } 757 | ], 758 | "options": [ 759 | { 760 | "value": "S" 761 | } 762 | ], 763 | "inventory_quantity": 100, 764 | "manage_inventory": true 765 | }, 766 | { 767 | "title": "M", 768 | "prices": [ 769 | { 770 | "currency_code": "eur", 771 | "amount": 3650 772 | }, 773 | { 774 | "currency_code": "usd", 775 | "amount": 4150 776 | } 777 | ], 778 | "options": [ 779 | { 780 | "value": "M" 781 | } 782 | ], 783 | "inventory_quantity": 100, 784 | "manage_inventory": true 785 | }, 786 | { 787 | "title": "L", 788 | "prices": [ 789 | { 790 | "currency_code": "eur", 791 | "amount": 3650 792 | }, 793 | { 794 | "currency_code": "usd", 795 | "amount": 4150 796 | } 797 | ], 798 | "options": [ 799 | { 800 | "value": "L" 801 | } 802 | ], 803 | "inventory_quantity": 100, 804 | "manage_inventory": true 805 | }, 806 | { 807 | "title": "XL", 808 | "prices": [ 809 | { 810 | "currency_code": "eur", 811 | "amount": 3650 812 | }, 813 | { 814 | "currency_code": "usd", 815 | "amount": 4150 816 | } 817 | ], 818 | "options": [ 819 | { 820 | "value": "XL" 821 | } 822 | ], 823 | "inventory_quantity": 100, 824 | "manage_inventory": true 825 | } 826 | ] 827 | }, 828 | { 829 | "title": "Medusa Coffee Mug", 830 | "subtitle": null, 831 | "description": "Every programmer's best friend.", 832 | "handle": "coffee-mug", 833 | "is_giftcard": false, 834 | "weight": 400, 835 | "images": [ 836 | "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png" 837 | ], 838 | "options": [ 839 | { 840 | "title": "Size", 841 | "values": ["One Size"] 842 | } 843 | ], 844 | "variants": [ 845 | { 846 | "title": "One Size", 847 | "prices": [ 848 | { 849 | "currency_code": "eur", 850 | "amount": 1000 851 | }, 852 | { 853 | "currency_code": "usd", 854 | "amount": 1200 855 | } 856 | ], 857 | "options": [ 858 | { 859 | "value": "One Size" 860 | } 861 | ], 862 | "inventory_quantity": 100, 863 | "manage_inventory": true 864 | } 865 | ] 866 | } 867 | ] 868 | } 869 | --------------------------------------------------------------------------------