Within the first part of this collection, you discovered about Medusa Extender and tips on how to use it to create a market ecommerce platform. The primary half demonstrated tips on how to hyperlink shops to customers so that every new person has their very own retailer, then tips on how to hyperlink merchandise to a retailer in order that the person can see and handle solely their merchandise.
On this half, you’ll learn to hyperlink orders to their respective shops. This can cowl use circumstances the place a buyer purchases merchandise from a number of shops, and tips on how to handle the general standing of that order.
Yow will discover the complete code for this tutorial in this GitHub repository.
You may alternatively use the Medusa Market plugin as indicated within the README of the GitHub repository. If you happen to’re already utilizing it be certain to replace to the newest model:
npm set up medusa-marketplace@newest
Stipulations
It’s assumed that you simply’ve adopted together with the primary a part of the collection earlier than persevering with this half. If you happen to haven’t, please begin from there.
If you happen to don’t have the Medusa Admin put in, it’s endorsed that you simply set up it so that you could simply view merchandise and orders, amongst different functionalities.
Alternatively, you should use Medusa’s Admin APIs to entry the info in your server. Nonetheless, the remainder of the tutorial will principally showcase options by way of the Medusa Admin.
So as to add merchandise with pictures you additionally want a file service plugin like MinIO put in in your Medusa server. You too can verify the documentation for extra choices for a file service.
This tutorial moreover makes use of the Next.js starter storefront to showcase inserting orders. That is additionally non-obligatory and you’re free to make use of different storefronts or Medusa’s Storefront APIs as a substitute.
Lastly, this half makes use of model 1.6.5
of the Medusa Extender which launched new options and a greater developer expertise.
You probably have an outdated model of the Medusa Extender put in, replace the extender in your Medusa server:
npm set up medusa-extender@1.6.5
Change the content material of tsconfig.json
to the next:
{
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"goal": "es2017",
"sourceMap": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true
},
"embrace": [
"src",
],
"exclude": [
"dist",
"node_modules",
"**/*.spec.ts",
"medusa-config.js",
]
}
And alter scripts
in bundle.json
to the next:
"scripts": {
"seed": "medusa seed -f ./information/seed.json",
"construct": "rm -rf dist && ./node_modules/.bin/tsc -p tsconfig.json",
"begin": "npm run construct && NODE_ENV=growth node ./dist/major.js",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
"begin:watch": "nodemon --watch './src/**/*.ts' --exec 'ts-node' ./src/major.ts",
"begin:prod": "npm run construct && NODE_ENV=manufacturing node dist/major"
},
What You Will Be Constructing
As talked about within the introduction, this a part of the collection will information you thru linking orders to shops. To do this, you’ll override the Order
mannequin so as to add the relation between it and the Retailer
mannequin.
In a market, prospects ought to have the ability to buy merchandise from a number of distributors on the similar time. So, you’ll additionally add a subscriber that, when an order is positioned, will create “baby” orders. Youngster orders will likely be linked to a retailer, will solely have merchandise from the unique order that belongs to that retailer, and will likely be linked to the guardian order.
For that purpose, you’ll additionally add a parent-child relation between Order
fashions. This relation will moreover allow you to handle the guardian order’s standing based mostly on the statuses of kid orders.
Furthermore, you’ll add a filter that ensures when a person retrieves the checklist of orders of their retailer, solely orders that belong to their retailer are retrieved. This may also permit a brilliant admin who doesn’t belong to any retailer to trace the guardian orders.
Add Relations to the Order Mannequin
Step one is including the relation between the Order
and Retailer
mannequin, and between Order
fashions. To do this, you have to override the Order
mannequin.
Create a brand new listing src/modules/order
which is able to maintain all order-related courses that you simply create all through this tutorial.
Then, create the file src/modules/order/order.entity.ts
with the next content material:
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from "typeorm";
import { Entity as MedusaEntity } from "medusa-extender";
import { Order as MedusaOrder } from "@medusajs/medusa";
import { Retailer } from "../retailer/entities/retailer.entity";
@MedusaEntity({override: MedusaOrder})
@Entity()
export class Order extends MedusaOrder {
@Index()
@Column({ nullable: true })
store_id: string;
@Index()
@Column({ nullable: false })
order_parent_id: string;
@ManyToOne(() => Retailer, (retailer) => retailer.orders)
@JoinColumn({ title: 'store_id' })
retailer: Retailer;
@ManyToOne(() => Order, (order) => order.youngsters)
@JoinColumn({ title: 'order_parent_id' })
guardian: Order;
@OneToMany(() => Order, (order) => order.guardian)
@JoinColumn({ title: 'id', referencedColumnName: 'order_parent_id' })
youngsters: Order[];
}
You add the mannequin Order
which overrides and extends Medusa’s Order
mannequin. On this mannequin, you add 2 new columns: store_id
and order_parent_id
. The store_id
column will likely be used for the many-to-one relation between the Order
mannequin and Retailer
mannequin, which you display by way of the retailer
property.
The order_parent_id
column will likely be used for the many-to-one and one-to-many relation between Order
fashions. This results in guardian
and youngsters
properties ensuing from these relations.
Subsequent, in src/modules/retailer/entities/retailer.entity.ts
add a brand new import for the Order
mannequin initially of the file:
import { Order } from '../../order/order.entity';
And contained in the Retailer
class add the relation to the Order
mannequin:
@OneToMany(() => Order, (order) => order.retailer)
@JoinColumn({ title: 'id', referencedColumnName: 'store_id' })
orders: Order[];
Add a New Migration
To mirror the brand new columns within the database, you have to create a migration file within the order module.
As migration recordsdata have the format <timestamp>-order.migration.ts
, a migration file is exclusive to you so you have to create it your self.
Fortunately, the brand new replace of Medusa Extender added plenty of useful CLI instructions to make redundant duties simpler for you. You may generate the migration file utilizing the next command:
./node_modules/.bin/medex g -mi order
This can create the file src/modules/order/<timestamp>-order.migration.ts
for you. Open that file and substitute the up
and down
strategies with the next implementation:
public async up(queryRunner: QueryRunner): Promise<void> {
const question = `
ALTER TABLE public."order" ADD COLUMN IF NOT EXISTS "store_id" textual content;
ALTER TABLE public."order" ADD COLUMN IF NOT EXISTS "order_parent_id" textual content;
ALTER TABLE public."order" ADD CONSTRAINT "FK_8a96dde86e3cad9d2fcc6cb171f87" FOREIGN KEY ("order_parent_id") REFERENCES "order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
`;
await queryRunner.question(question);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const question = `
ALTER TABLE public."order" DROP COLUMN "store_id";
ALTER TABLE public."order" DROP COLUMN "order_parent_id";
ALTER TABLE public."order" DROP FOREIGN KEY "FK_8a96dde86e3cad9d2fcc6cb171f87cb2";
`;
await queryRunner.question(question);
}
The up
technique provides the columns store_id
and order_parent_id
to the order
desk with a international key, and the down
technique removes these columns and international key from the order
desk.
Run Migrations
A part of the Medusa Extender new CLI instructions is the [migrate
command](https://adrien2p.github.io/medusa-extender/#/?id=command-migrate-reference) which seems to be contained in the src
and dist
directories for each recordsdata ending with .migration.js
and JavaScript recordsdata inside a migrations
sub-directory of the two directories.
You may check with the Medusa Marketplace plugin to study how one can run migrations from it.
Then, if the migrations inside these recordsdata haven’t been run earlier than it runs or present them based mostly on the choice you cross to the command.
Because the migration file you’ve created is a TypeScript file, you have to transpile it to JavaScript first earlier than migrating the modifications. So, run the next command:
npm run construct
This can transpile all TypeScript recordsdata contained in the src
listing into JavaScript recordsdata contained in the dist
listing.
Lastly, run the migration with the next command:
./node_modules/.bin/medex migrate --run
If you happen to get an error about duplicate migrations due to migrations from the earlier a part of this collection, go forward and take away the outdated ones from the dist
listing and check out working the command once more.
If you happen to verify your database as soon as the migration is run efficiently, you may see that the two new columns have been added to the order
desk.
Override OrderRepository
Because you’ve overridden the Order
mannequin, it is best to override OrderRepository
to make it possible for when an order is retrieved, the overridden mannequin is used.
Create the file src/modules/order/order.repository.ts
with the next content material:
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { OrderRepository as MedusaOrderRepository } from "@medusajs/medusa/dist/repositories/order";
import { Order } from "./order.entity";
@MedusaRepository({override: MedusaOrderRepository})
@EntityRepository(Order)
export class OrderRepository extends Utils.repositoryMixin<Order, MedusaOrderRepository>(MedusaOrderRepository) {}
Record Orders By Retailer
On this part, you’ll retrieve orders based mostly on the shop of the at the moment logged-in person.
Modify LoggedInUserMiddleware
Within the earlier half, you created a middleware LoggedInUserMiddleware
which checks if a person is logged in and registers them within the scope. This lets you entry the logged-in person from providers and subscribers, and this was used to retrieve merchandise based mostly on the logged-in person’s retailer.
Nonetheless, the earlier implementation impacts each storefront and admin routes in Medusa. This will trigger inconsistencies for purchasers accessing the storefront.
To make sure that the logged-in person is simply added to the scope for admin routes, change the code in src/modules/person/middlewares/loggedInUser.middleware.ts
to the next content material:
import { MedusaAuthenticatedRequest, MedusaMiddleware, Middleware } from 'medusa-extender';
import { NextFunction, Response } from 'specific';
import UserService from '../providers/person.service';
@Middleware({ requireAuth: true, routes: [{ method: "all", path: '*' }] })
export class LoggedInUserMiddleware implements MedusaMiddleware {
public async eat(req: MedusaAuthenticatedRequest, res: Response, subsequent: NextFunction): Promise<void> {
let loggedInUser = null;
if (req.person && req.person.userId && **/^/admin/.take a look at(req.originalUrl)**) {
const userService = req.scope.resolve('userService') as UserService;
loggedInUser = await userService.retrieve(req.person.userId, {
choose: ['id', 'store_id'],
});
}
req.scope.register({
loggedInUser: {
resolve: () => loggedInUser,
},
});
subsequent();
}
}
The brand new change provides a brand new situation to verify if the present route begins with /admin
. If it does and if the person is logged in, the logged-in person is added to the scope. In any other case, the worth of loggedInUser
within the scope will likely be null
.
Though you may specify the trail of the middleware to be /admin/*
to register this middleware for admin routes solely, this strategy is critical as a result of if the loggedInUser
just isn’t registered within the scope an error will likely be thrown in any service or subscriber that makes use of it.
Override the OrderService
The Medusa server makes use of the strategy [buildQuery_](https://github.com/adrien2p/medusa-extender/releases/tag/v1.7.0)
in OrderService
to construct the question essential to retrieve the orders from the database. You’ll be overriding the OrderService
, and significantly the buildQuery_
technique so as to add a selector situation for the store_id
if there’s a at the moment logged-in person that has a retailer.
Create the file src/modules/order.service.ts
with the next content material:
import { EntityManager } from 'typeorm';
import { OrderService as MedusaOrderService } from "@medusajs/medusa/dist/providers";
import { OrderRepository } from './order.repository';
import { Service } from 'medusa-extender';
import { Consumer } from "../person/entities/person.entity";
sort InjectedDependencies = {
supervisor: EntityManager;
orderRepository: typeof OrderRepository;
customerService: any;
paymentProviderService: any;
shippingOptionService: any;
shippingProfileService: any;
discountService: any;
fulfillmentProviderService: any;
fulfillmentService: any;
lineItemService: any;
totalsService: any;
regionService: any;
cartService: any;
addressRepository: any;
giftCardService: any;
draftOrderService: any;
inventoryService: any;
eventBusService: any;
loggedInUser: Consumer;
orderService: OrderService;
};
@Service({ scope: 'SCOPED', override: MedusaOrderService })
export class OrderService extends MedusaOrderService {
non-public readonly supervisor: EntityManager;
non-public readonly container: InjectedDependencies;
constructor(container: InjectedDependencies) {
tremendous(container);
this.supervisor = container.supervisor;
this.container = container;
}
buildQuery_(selector: object, config: {relations: string[], choose: string[]}): object {
if (this.container.loggedInUser && this.container.loggedInUser.store_id) {
selector['store_id'] = this.container.loggedInUser.store_id;
}
config.choose.push('store_id')
config.relations = config.relations ?? []
config.relations.push("youngsters", "guardian", "retailer")
return tremendous.buildQuery_(selector, config);
}
}
Inside buildQuery_
you first verify if there’s a logged-in person and if that person has a retailer. If true, you add to the selector
parameter (which is used to filter out information from the database) a brand new property store_id
and set its worth to the shop ID of the logged-in person.
You additionally add to the chosen fields store_id
, and also you add the youngsters
, guardian
, and retailer
relations to be retrieved together with the order.
Create the Order Module
The very last thing earlier than you may take a look at out the modifications you’ve simply made is you have to create an order module that imports these new courses you created into Medusa.
Create the file src/modules/order/order.module.ts
with the next content material:
import { Module } from 'medusa-extender';
import { Order } from './order.entity';
import { OrderMigration1652101349791 } from './1652101349791-order.migration';
import { OrderRepository } from './order.repository';
import { OrderService } from './order.service';
import { OrderSubscriber } from './order.subscriber';
@Module({
imports: [Order, OrderRepository, OrderService, OrderMigration1652101349791]
})
export class OrderModule {}
Please discover that you have to change the import and sophistication title of the migration class based mostly in your migration’s title.
Then, import this new module initially of the file src/major.ts
:
import { OrderModule } from './modules/order/order.module';
And contained in the array handed to the load
technique cross the OrderModule
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
ProductModule,
OrderModule,
StoreModule,
]);
Check it Out
To check it out, begin the server with the next command:
npm begin
This can begin the server on port 9000
Then, begin your Medusa admin and log in with the person you created within the first a part of the collection. It’s best to see on the orders web page that there are not any orders for this person.
If you happen to’re utilizing Medusa’s APIs you may view the orders by sending a
GET
request tolocalhost:9000/admin/orders
.
Deal with Order Place Occasion
On this part, you’ll add a subscriber to deal with the order.positioned
occasion that’s triggered every time a brand new order is positioned by a buyer. As talked about earlier within the tutorial, you’ll use this handler to create baby orders for every retailer that the client bought merchandise from of their order.
Create a brand new file src/modules/order/order.subscriber.ts
with the next content material:
import { EventBusService, OrderService } from "@medusajs/medusa/dist/providers";
import { LineItem, OrderStatus } from '@medusajs/medusa';
import { EntityManager } from "typeorm";
import { LineItemRepository } from '@medusajs/medusa/dist/repositories/line-item';
import { Order } from './order.entity';
import { OrderRepository } from "./order.repository";
import { PaymentRepository } from "@medusajs/medusa/dist/repositories/fee";
import { Product } from "../product/entities/product.entity";
import { ProductService } from './../product/providers/product.service';
import { ShippingMethodRepository } from "@medusajs/medusa/dist/repositories/shipping-method";
import { Subscriber } from 'medusa-extender';
sort InjectedDependencies = {
eventBusService: EventBusService;
orderService: OrderService;
orderRepository: typeof OrderRepository;
productService: ProductService;
supervisor: EntityManager;
lineItemRepository: typeof LineItemRepository;
shippingMethodRepository: typeof ShippingMethodRepository;
paymentRepository: typeof PaymentRepository;
};
@Subscriber()
export class OrderSubscriber {
non-public readonly supervisor: EntityManager;
non-public readonly eventBusService: EventBusService;
non-public readonly orderService: OrderService;
non-public readonly orderRepository: typeof OrderRepository;
non-public readonly productService: ProductService;
non-public readonly lineItemRepository: typeof LineItemRepository;
non-public readonly shippingMethodRepository: typeof ShippingMethodRepository;
constructor({ eventBusService, orderService, orderRepository, productService, supervisor, lineItemRepository, shippingMethodRepository, paymentRepository}: InjectedDependencies) {
this.eventBusService = eventBusService;
this.orderService = orderService;
this.orderRepository = orderRepository;
this.productService = productService;
this.supervisor = supervisor;
this.lineItemRepository = lineItemRepository;
this.shippingMethodRepository = shippingMethodRepository;
this.eventBusService.subscribe(
OrderService.Occasions.PLACED,
this.handleOrderPlaced.bind(this)
);
}
non-public async handleOrderPlaced({ id }: {id: string}): Promise<void> {
//create baby orders
//retrieve order
const order: Order = await this.orderService.retrieve(id, {
relations: ['items', 'items.variant', 'cart', 'shipping_methods', 'payments']
});
//group gadgets by retailer id
const groupedItems = {};
for (const merchandise of order.gadgets) {
const product: Product = await this.productService.retrieve(merchandise.variant.product_id, { choose: ['store_id']});
const store_id = product.store_id;
if (!store_id) {
proceed;
}
if (!groupedItems.hasOwnProperty(store_id)) {
groupedItems[store_id] = [];
}
groupedItems[store_id].push(merchandise);
}
const orderRepo = this.supervisor.getCustomRepository(this.orderRepository);
const lineItemRepo = this.supervisor.getCustomRepository(this.lineItemRepository);
const shippingMethodRepo = this.supervisor.getCustomRepository(this.shippingMethodRepository);
for (const store_id in groupedItems) {
//create order
const childOrder = orderRepo.create({
...order,
order_parent_id: id,
store_id: store_id,
cart_id: null,
cart: null,
id: null,
shipping_methods: []
}) as Order;
const orderResult = await orderRepo.save(childOrder);
//create transport strategies
for (const shippingMethod of order.shipping_methods) {
const newShippingMethod = shippingMethodRepo.create({
...shippingMethod,
id: null,
cart_id: null,
cart: null,
order_id: orderResult.id
});
await shippingMethodRepo.save(newShippingMethod);
}
//create line gadgets
const gadgets: LineItem[] = groupedItems[store_id];
for (const merchandise of gadgets) {
const newItem = lineItemRepo.create({
...merchandise,
id: null,
order_id: orderResult.id,
cart_id: null
})
await lineItemRepo.save(newItem);
}
}
}
}
Right here’s a abstract of this code:
- Within the constructor, you register the strategy
handleOrderPlaced
as a handler for the occasionorder.positioned
. - Inside
handleOrderPlaced
you first retrieve the order utilizing the ID handed to the strategy with the required relations for the creation of kid orders. - You then loop over the gadgets bought within the order and group then inside the article
groupedItems
with the important thing being the distinctive retailer IDs and the worth being an array of things. - You then loop over the keys in
groupedItems
and create achildOrder
for every retailer. The kid orders have the identical information because the guardian order however they’veparent_id
set to the ID of the guardian order andstore_id
set to the ID of the shop it’s related to. - For every baby order, you have to create
shippingMethods
which might be equivalent to the transport strategies of the guardian order however related to the kid order. - For every baby order, you have to add the gadgets that have been within the order for that particular retailer, as every vendor ought to solely see the gadgets ordered from their retailer.
Ensure you have Redis put in and configured with Medusa for this subscriber to work.
Check it Out
To check it out, first, restart your Medusa server, then run the storefront that you simply’re utilizing to your retailer and add one of many merchandise you created for a vendor to the cart then place an order.
If you happen to then open the admin panel once more and verify orders, it is best to see a brand new order on the orders web page of the seller. If you happen to open it you’ll see particulars associated to the order.
Attempt creating extra customers and including merchandise for various customers and shops. You’ll see that every person will see the order with gadgets solely associated to their retailer.
Deal with Order Standing Modified Occasions
To make sure that the standing of the guardian order modifications as needed with the change of standing of the kid orders, it’s essential to hearken to the occasions triggered every time an order’s standing modifications.
Within the constructor
of the OrderSubscriber
class in src/modules/order/order.subscriber.ts
add the next code:
//add handler for various standing modifications
this.eventBusService.subscribe(
OrderService.Occasions.CANCELED,
this.checkStatus.bind(this)
);
this.eventBusService.subscribe(
OrderService.Occasions.UPDATED,
this.checkStatus.bind(this)
);
this.eventBusService.subscribe(
OrderService.Occasions.COMPLETED,
this.checkStatus.bind(this)
);
This provides the identical technique checkStatus
because the order handler of the occasions Canceled, Up to date, and Accomplished of an order.
Subsequent, add inside the category the next strategies:
public async checkStatus({ id }: {id: string}): Promise<void> {
//retrieve order
const order: Order = await this.orderService.retrieve(id);
if (order.order_parent_id) {
//retrieve guardian
const orderRepo = this.supervisor.getCustomRepository(this.orderRepository);
const parentOrder = await this.orderService.retrieve(order.order_parent_id, {
relations: ['children']
});
const newStatus = this.getStatusFromChildren(parentOrder);
if (newStatus !== parentOrder.standing) {
change (newStatus) {
case OrderStatus.CANCELED:
this.orderService.cancel(parentOrder.id);
break;
case OrderStatus.ARCHIVED:
this.orderService.archive(parentOrder.id);
break;
case OrderStatus.COMPLETED:
this.orderService.completeOrder(parentOrder.id);
break;
default:
parentOrder.standing = newStatus;
parentOrder.fulfillment_status = newStatus;
parentOrder.payment_status = newStatus;
await orderRepo.save(parentOrder);
}
}
}
}
public getStatusFromChildren (order: Order): string {
if (!order.youngsters) {
return order.standing;
}
//gather all statuses
let statuses = order.youngsters.map((baby) => baby.standing);
//take away duplicate statuses
statuses = [...new Set(statuses)];
if (statuses.size === 1) {
return statuses[0];
}
//take away archived and canceled orders
statuses = statuses.filter((standing) => standing !== OrderStatus.CANCELED && standing !== OrderStatus.ARCHIVED);
if (!statuses.size) {
//all baby orders are archived or canceled
return OrderStatus.CANCELED;
}
if (statuses.size === 1) {
return statuses[0];
}
//verify if any order requires motion
const hasRequiresAction = statuses.some((standing) => standing === OrderStatus.REQUIRES_ACTION);
if (hasRequiresAction) {
return OrderStatus.REQUIRES_ACTION;
}
//since a couple of standing is left and we filtered out canceled, archived,
//and requires motion statuses, solely pending and full left. So, return pending
return OrderStatus.PENDING;
}
Right here’s a abstract of the code snippet:
- In
checkStatus
you first retrieve the order’s information utilizing its ID. - You verify if the order has a guardian order. That is to keep away from dealing with occasions triggered for the guardian order as it’s not needed.
- You then retrieve the guardian order with its relation to its
youngsters
orders. - You make use of one other technique
getStatusFromChildren
to infer the standing of the guardian order from the youngsters:- You first retrieve all statuses from the kid orders then take away any duplicates.
- If the results of eradicating the duplicates results in just one standing, then it implies that all orders have the identical standing and the guardian can have that very same standing as properly.
- In any other case, if there’s a couple of standing, you take away the archived and canceled orders.
- If this results in no statuses, which means that all youngsters are both canceled or archived and the guardian ought to have the identical standing. The code snippet defaults to the “canceled” standing right here however you may change that.
- In any other case, if there’s solely standing left after eradicating canceled and archived orders you come that standing.
- In any other case, if there’s a couple of standing left, you verify if a type of statuses is
requires_action
and return that because the standing. - If there’s no
requires_action
standing you may infer there are solelypending
andfull
orders left. Because it’s logical to contemplate that if at the very least one order ispending
then you may take into account the guardian orderpending
, you default to that standing.
- After retrieving the deduced standing of the guardian order, if that standing is totally different than the present standing of the guardian order, you replace its standing. Relying on the brand new standing, you both use present strategies within the
OrderService
to replace the standing, or manually set the standing within the order.
Check it Out
Restart your Medusa server. Then, open the Medusa admin to the order you simply created earlier. Attempt canceling the order by clicking on the highest 3 dots then clicking “Cancel Order”.
After canceling the order, sign off and log in with a brilliant admin person. By default, the tremendous admin person is the person created whenever you seed the database initially of your Medusa server arrange. This person has the e-mail “admin@medusa-test.com” and password “supersecret”.
If you happen to open the guardian order you’ll see that it’s now canceled as properly.
Conclusion
By following these 2 components, it is best to now have shops for every person with merchandise and orders linked to them.
Within the subsequent a part of the collection, you’ll study tips on how to add a couple of person to a retailer, tips on how to add tremendous admins, and tips on how to customise different settings.
Ought to you’ve any points or questions associated to Medusa, then be at liberty to succeed in out to the Medusa crew by way of Discord.