docs: update recipes to be based on v2 (#7516)
This commit is contained in:
@@ -1,375 +0,0 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `B2B Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building digital products with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
In a B2B store, it’s essential to differentiate between customer types to provide them with relevant pricing, products, shopping experience, and more.
|
||||
|
||||
Medusa’s commerce modules, including Sales Channel, Customer, and Pricing modules facilitate implementing this setup. Medusa’s architecture and extendible nature allow you to customize your store based on your use case.
|
||||
|
||||
<Note title="Related use-case">
|
||||
|
||||
[Visionary: Frictionless B2B ecommerce with Medusa](https://medusajs.com/blog/visionary/)
|
||||
|
||||
</Note>
|
||||
|
||||
<LearningPath pathName="b2b" />
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Sales Channel
|
||||
|
||||
In Medusa, a sales channel allows you to set product availability per channel. In this case, create a B2B sales channel that includes only B2B products.
|
||||
|
||||
Create a sales channel through the Medusa Admin or Admin REST APIs.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the sales channel using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the sales channel using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create a Publishable API Key
|
||||
|
||||
Publishable API keys are associated with one or more sales channels. Then, in a client such as a storefront, pass the publishable API key in the header of your requests to ensure all products retrieved belong to the associated sales channel(s).
|
||||
|
||||
Create a publishable API key through the Medusa Admin or the Admin REST APIs, then associate it with the B2B sales channel. Later, use this key when developing your B2B storefront.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the publishable API key using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the publishable API key using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Add Products
|
||||
|
||||
To add B2B products, use the Medusa Admin or the Admin REST APIs. You can also import your products from an existing CSV file using the product-import feature.
|
||||
|
||||
After or while you add your products, set the products’ availability to the B2B sales channel you’ve created.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Add Products Using Medusa Admin",
|
||||
text: "Create the product using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Add Products Using REST APIs",
|
||||
text: "Create the product using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Import Products Using Medusa Admin",
|
||||
text: "Import the products using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Import Products Using REST APIs",
|
||||
text: "Import the product using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Customer Groups
|
||||
|
||||
In B2B sales, you often negotiate special prices with each customer or company. So, you organize your B2B customers into groups that allow you to set different prices for each.
|
||||
|
||||
You can create a single B2B customer group that all your B2B customers fall into or create different B2B customer groups for every B2B customer. The second approach is helpful if you apply different prices for each business.
|
||||
|
||||
While creating the customer groups, add to the `metadata` attribute of the customer group a key `is_b2b` that acts as a flag to indicate that this is a B2B customer group. It's useful for later steps.
|
||||
|
||||
For more advanced customer modeling, you may create custom data models. This is covered in the [Create Custom Data Model section](#create-custom-data-model).
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the customer group using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the customer group using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Add B2B Customers
|
||||
|
||||
After adding your B2B customer group, add B2B customers and assign them to the B2B customer group. Alternatively, allowing B2B customers to register themselves is covered in a later step.
|
||||
|
||||
Create a customer through the Medusa Admin or Admin API routes.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the customers using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the customers using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Price List
|
||||
|
||||
A price list allows you to set different prices on a set of products for various conditions. When building a B2B store, use this to assign different prices for B2B customer groups.
|
||||
|
||||
Use price lists to set the same price for all B2B customers or different prices for different B2B customers if you’ve organized them into separate customer groups.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Add Price List Using Medusa Admin",
|
||||
text: "Create the price list using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Add Price List Using REST APIs",
|
||||
text: "Create the price list using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create Custom Data Model
|
||||
|
||||
To implement a more advanced B2B sales flow, use custom data models such as `Company`, `Employee`, `Admin`, and `Buyer`. This provides more granular control of your B2B sales and allows you to build features like privileges, limits, and more.
|
||||
|
||||
Each of the data models you create can be managed using custom logic.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create a Data Model"
|
||||
text="Learn how to create a custom data model."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Create an API Route to Check Customers
|
||||
|
||||
On the frontend clients communicating with your store, such as the storefront, you need to check whether the currently logged-in customer is a B2B customer.
|
||||
|
||||
The API Route's logic differs based on how you’ve implemented the B2B logic. For example, if you’ve used the `is_b2b` flag in the customer group, your API Route checks whether that flag is enabled for the logged-in customer.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create an API Route"
|
||||
text="Learn how to create an API Route in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example Implementation">
|
||||
|
||||
For example, create the following API Route that allows you to check the customer’s group and whether it has the `is_b2b` flag enabled:
|
||||
|
||||
```ts title="src/api/store/customers/is-b2b/route.ts"
|
||||
import type {
|
||||
CustomerService,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerService: CustomerService = req.scope.resolve(
|
||||
"customerService"
|
||||
)
|
||||
|
||||
const customer = await customerService
|
||||
.retrieve(req.user.customer_id, {
|
||||
relations: ["groups"],
|
||||
})
|
||||
|
||||
const is_b2b = customer.groups.some(
|
||||
(group) => group.metadata.is_b2b === "true"
|
||||
)
|
||||
|
||||
return res.json({
|
||||
is_b2b,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Then add the `requireCustomerAuthentication` middleware in `src/api/middlewares.ts` that ensures only authenticated customer can access this API Route:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import {
|
||||
requireCustomerAuthentication,
|
||||
type MiddlewaresConfig,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store/customers/is-b2b",
|
||||
middlewares: [requireCustomerAuthentication()],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Customize Admin
|
||||
|
||||
Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.
|
||||
|
||||
The Medusa Admin plugin can be extended to add widgets, new pages, and setting pages.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Admin Widget",
|
||||
text: "Learn how to add widgets into existing admin pages.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Admin UI Routes",
|
||||
text: "Learn how to add new pages to your Medusa Admin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Admin Setting Page",
|
||||
text: "Learn how to add new page to the Medusa Admin settings.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Customize Storefront
|
||||
|
||||
You may want different interfaces for B2B and regular customers on the storefront, or to display products differently for each type of customer.
|
||||
|
||||
Medusa provides a Next.js Starter storefront that you can use and customize. You can also build your storefront with any frontend technology. Medusa provides you with different client libraries and REST APIs to use.
|
||||
|
||||
Use the publishable API key you associated with your B2B sales channel in the storefront to ensure only B2B products are retrieved.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Next.js Starter",
|
||||
text: "Learn how to install and use the Next.js Starter.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Options 2: Build Custom Storefront",
|
||||
text: "Learn how to build a custom storefront with your preferred technology.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Use Publishable API Keys"
|
||||
text="Learn how to use the publishable API key in client requests."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Deploy B2B Store
|
||||
|
||||
Once you finish your development, deploy your B2B Medusa application and storefront to your preferred hosting providers.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Deploy Medusa Application",
|
||||
text: "Learn how to deploy your Medusa application.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Deploy Storefront",
|
||||
text: "Learn how to deploy your storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -1,585 +0,0 @@
|
||||
import { AcademicCapSolid, BoltSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Commerce Automation Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll learn how to implement different forms of commerce automation with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Commerce automation is essential for businesses to save costs, provide a better user experience, and avoid manual, repetitive tasks that lead to human errors. Businesses utilize automation in different domains, including marketing, customer support, and order management.
|
||||
|
||||
Medusa provides the necessary architecture and tools to implement commerce automation for order management, customer service, and more. You can perform an asynchronous action when an event is triggered, schedule a job that runs at a specified interval, and more.
|
||||
|
||||
---
|
||||
|
||||
## Re-Stock Notifications
|
||||
|
||||
Customers may be interested in a product that is currently out of stock.
|
||||
|
||||
Medusa provides a Re-stock notifications plugin. You can also implement this by creating custom entities, triggering custom events, and handling these events with a subscriber.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Restock Notifications Plugin",
|
||||
text: "Install the Restock Notification plugin.",
|
||||
startIcon: <BoltSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Data Model",
|
||||
text: "Learn how to create a custom data model.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Emit Events",
|
||||
text: "Learn how to emit a custom event.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Automated Customer Support
|
||||
|
||||
Customer support is essential to build a store's brand and customer loyalty. This can include integrating with third-party services or automating notifications sent to customers when changes happen related to their orders, returns, exchanges, and more.
|
||||
|
||||
You can use Medusa's Notification Service to handle notifications triggered by actions on customer orders or profiles.
|
||||
|
||||
For example, when an order's status is updated, the `order.updated` event is triggered. You can create a Notification Service that handles this event by emailing the customer about the status update.
|
||||
|
||||
The Event reference includes an extensive list of events triggered by the Medusa core.
|
||||
|
||||
Medusa also provides official notification plugins that integrate with third-party services, such as SendGrid or TwilioSMS.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Notification Plugins",
|
||||
text: "Check out available notification plugins.",
|
||||
startIcon: <BoltSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Notification Service",
|
||||
text: "Learn how to create a notification service.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Details summaryContent="Example: Sending an email for new notes">
|
||||
|
||||
Here’s an example of a subscriber that uses the SendGrid plugin to send an email to the customer when the order has a new note:
|
||||
|
||||
```ts title="src/subscribers/new-note.ts"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
ProductVariantService,
|
||||
NoteService,
|
||||
OrderService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default async function handleNoteCreated({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const noteService: NoteService = container.resolve(
|
||||
"noteService"
|
||||
)
|
||||
const orderService: OrderService = container.resolve(
|
||||
"orderService"
|
||||
)
|
||||
const sendgridService = container.resolve("sendgridService")
|
||||
|
||||
// retrieve note by id
|
||||
const note = await noteService.retrieve(data.id, {
|
||||
relations: ["author"],
|
||||
})
|
||||
|
||||
if (!note || note.resource_type !== "order") {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve note's order
|
||||
const order = await orderService.retrieve(
|
||||
note.resource_id
|
||||
)
|
||||
|
||||
if (!order) {
|
||||
return
|
||||
}
|
||||
|
||||
sendGridService.sendEmail({
|
||||
templateId: "order-update",
|
||||
from: "hello@medusajs.com",
|
||||
to: order.email,
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
note_text: note.value,
|
||||
note_author: note.author.first_name,
|
||||
note_date: note.created_at,
|
||||
order_id: order.display_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: NoteService.Events.CREATED,
|
||||
context: {
|
||||
subscriberId: "note-created-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Automatic Data Synchronization
|
||||
|
||||
As your commerce store grows, you'll likely need to synchronize data across different systems. For example, you need to synchronize data with an ERP system or a data warehouse.
|
||||
|
||||
Use scheduled jobs to implement automatic synchronization. Within the scheduled job, you can synchronize your internal or external data.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create a Scheduled Job"
|
||||
text="Learn how to create a scheduled job in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Synchronizing products with a third-party service">
|
||||
|
||||
Here’s an example of synchronizing products with a third party service using a loader:
|
||||
|
||||
```ts title="src/loaders/sync-products.ts"
|
||||
import {
|
||||
Logger,
|
||||
ProductService,
|
||||
StoreService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ProductSelector,
|
||||
} from "@medusajs/medusa/dist/types/product"
|
||||
|
||||
export default async (
|
||||
container: MedusaContainer,
|
||||
config: Record<string, unknown>
|
||||
): Promise<void> => {
|
||||
const logger = container.resolve<Logger>("logger")
|
||||
logger.info("Synchronizing products...")
|
||||
const productService = container.resolve<ProductService>(
|
||||
"productService"
|
||||
)
|
||||
const storeService = container.resolve<StoreService>(
|
||||
"storeService"
|
||||
)
|
||||
// retrieve store to get last sync date
|
||||
const store = await storeService.retrieve()
|
||||
|
||||
const productFilters: ProductSelector = {}
|
||||
|
||||
if (store.metadata.last_sync_date) {
|
||||
productFilters.updated_at = {
|
||||
gt: new Date(
|
||||
store.metadata.last_sync_date as string
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const updatedProducts = await productService.list(
|
||||
productFilters
|
||||
)
|
||||
|
||||
updatedProducts.forEach((product) => {
|
||||
// assuming client is an initialized connection
|
||||
// with a third-party service
|
||||
client.sync(product)
|
||||
})
|
||||
|
||||
await storeService.update({
|
||||
metadata: {
|
||||
last_sync_date: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
logger.info("Finish synchronizing products")
|
||||
}
|
||||
```
|
||||
|
||||
Notice that here it’s assumed that:
|
||||
|
||||
1. The last update date is stored in the `Store`'s metadata object. You can instead use a custom data model to handle this.
|
||||
2. The connection to the third-party service is assumed to be available and handled within the `client` variable.
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Order Management Automation
|
||||
|
||||
Using Medusa's architecture and commerce features, you can automate a large amount of order management functionalities.
|
||||
|
||||
To handle events within an order flow and automate actions, create a subscriber. For example, create a subscriber that listens to the `order.placed` event and automatically creates a fulfillment if predefined conditions are met.
|
||||
|
||||
Another example is creating a subscriber that listens to the `order.shipment_created` event and automatically captures payment when an order is fully shipped.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Event Bus",
|
||||
text: "Learn about the event architecture system.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber to listen to events.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Events Reference",
|
||||
text: "Check out triggered events in Medusa and their expected payloads.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Automated RMA Flow
|
||||
|
||||
Businesses must optimize their Return Merchandise Authorization (RMA) flow to ensure a good customer experience and service. By automating the flow, customers request to return their received items, and businesses quickly support them.
|
||||
|
||||
Medusa's commerce features are geared towards automating RMA flows and ensuring a good customer experience.
|
||||
|
||||
For example, customers can create returns for their orders without direct involvement from the store operator. The store operator then receives a notification regarding the return and handles it accordingly. The same applies to order exchanges.
|
||||
|
||||
Medusa also provides features that allow a store operator to edit orders, receive customer approval for the edits, and authorize additional payment if necessary, all within a seamless flow.
|
||||
|
||||
Similar to the examples mentioned earlier, each action triggers events you can listen to and perform additional actions based on your use case.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Event Bus",
|
||||
text: "Learn about the event architecture system.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Events Reference",
|
||||
text: "Check out triggered events in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Returns",
|
||||
text: "Learn about the Order Returns features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Swaps",
|
||||
text: "Learn about the Order Swaps features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Edits",
|
||||
text: "Learn about the Order Edits features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Customer Segmentation
|
||||
|
||||
Businesses use customer segmentation to organize customers into different groups and then apply different price rules to these groups. For example, you may group customers by their product preferences, the number of orders they've placed, or geographical location.
|
||||
|
||||
Based on your use case and the segmentation logic, you can use different Medusa automation development tools to detect a customer's segment. Medusa also provides a customer groups feature that allows you to segment customers, whether you do it manually or automatically.
|
||||
|
||||
For example, to group customers with over twenty orders, create a subscriber that listens to the `order.placed` event and checks the number of orders the customer has so far. Then, add that customer to the VIP customer group. Finally, you can use the price lists feature to provide different prices or discounts for customers in the VIP group.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Customer Groups",
|
||||
text: "Learn about the Customer Groups architecture and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Subscribers and Events",
|
||||
text: "Learn about events in Medusa and how to listen to events.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Price Lists",
|
||||
text: "Learn about the Price Lists architecture and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Details summaryContent="Example: Add customer to VIP group">
|
||||
|
||||
Here’s an example of a subscriber that listens to the `order.placed` event and checks if the customer should be added to the VIP customer group based on their number of orders:
|
||||
|
||||
```ts title="src/subscribers/add-custom-to-vip.ts"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
OrderService,
|
||||
CustomerService,
|
||||
CustomerGroupService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const orderService: OrderService = container.resolve(
|
||||
"orderService"
|
||||
)
|
||||
const customerService: CustomerService = container.resolve(
|
||||
"customerService"
|
||||
)
|
||||
const customerGroupService: CustomerGroupService =
|
||||
container.resolve("customerGroupService")
|
||||
|
||||
// check if VIP group exists
|
||||
const vipGroup = await customerGroupService.list({
|
||||
name: "VIP",
|
||||
}, {
|
||||
relations: ["customers"],
|
||||
})
|
||||
if (!vipGroup.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve order and its customer
|
||||
const order = await orderService.retrieve(data.id)
|
||||
|
||||
if (!order || !order.customer_id ||
|
||||
vipGroup[0].customers.find(
|
||||
(customer) => customer.id === order.customer_id
|
||||
) !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve orders of this customer
|
||||
const [, count] = await orderService.listAndCount({
|
||||
customer_id: order.customer_id,
|
||||
})
|
||||
|
||||
if (count >= 20) {
|
||||
// add customer to VIP group
|
||||
customerGroupService.addCustomers(
|
||||
vipGroup[0].id,
|
||||
order.customer_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: OrderService.Events.PLACED,
|
||||
context: {
|
||||
subscriberId: "order-placed-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Marketing Automation
|
||||
|
||||
In your commerce store, you may utilize marketing strategies that encourage customers to make purchases. For example, you send a newsletter when new products are added to your store.
|
||||
|
||||
To do that, create a subscriber that listens to the `product.created`, and send an email to subscribed customers with tools like SendGrid or Mailchimp.
|
||||
|
||||
Alternatively, create a scheduled job that checks whether the number of new products has exceeded a set threshold, then sends out the newsletter.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Events and Subscribers",
|
||||
text: "Learn about events in Medusa and how to listen to events.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Scheduled Jobs",
|
||||
text: "Learn about scheduled jobs and how you can create one.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Events Reference",
|
||||
text: "Check out triggered events in Medusa and their expected payloads.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Details summaryContent="Example: Sending a newsletter email after adding ten products">
|
||||
|
||||
Here’s an example of listening to the `product.created` event in a subscriber and send a newsletter if the condition is met:
|
||||
|
||||
```ts title="src/subscribers/send-products-newsletter.ts"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
ProductService,
|
||||
StoreService,
|
||||
CustomerService,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ProductSelector,
|
||||
} from "@medusajs/medusa/dist/types/product"
|
||||
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const productService: ProductService = container.resolve(
|
||||
"productService"
|
||||
)
|
||||
const storeService: StoreService = container.resolve(
|
||||
"storeService"
|
||||
)
|
||||
const customerService: CustomerService = container.resolve(
|
||||
"customerService"
|
||||
)
|
||||
const sendgridService = container.resolve("sendgridService")
|
||||
|
||||
// retrieve store to have access to last send date
|
||||
const store = await storeService.retrieve()
|
||||
|
||||
const productFilters: ProductSelector = {}
|
||||
if (store.metadata.last_send_date) {
|
||||
productFilters.created_at = {
|
||||
gt: new Date(store.metadata.last_send_date as string),
|
||||
}
|
||||
}
|
||||
|
||||
const products = await productService.list(
|
||||
productFilters
|
||||
)
|
||||
|
||||
if (products.length > 10) {
|
||||
// get subscribed customers
|
||||
const customers = await customerService.list({
|
||||
metadata: {
|
||||
is_subscribed: true,
|
||||
},
|
||||
})
|
||||
sendgridService.sendEmail({
|
||||
templateId: "product-newsletter",
|
||||
from: "hello@medusajs.com",
|
||||
to: customers.map((customer) => ({
|
||||
name: customer.first_name,
|
||||
email: customer.email,
|
||||
})),
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
products,
|
||||
},
|
||||
})
|
||||
|
||||
await storeService.update({
|
||||
metadata: {
|
||||
last_send_date: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.CREATED,
|
||||
context: {
|
||||
subscriberId: "product-create-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Automation Development Toolkit
|
||||
|
||||
The use cases mentioned in this guide exemplify what commerce automation you can perform or implement with Medusa. Medusa provides the necessary development tools and mechanisms to automate tasks and manual work.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Events and Subscribers",
|
||||
text: "Learn about events in Medusa and how to listen to events.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Notification Service",
|
||||
text: "Learn about notification services and how to create one.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Scheduled Jobs",
|
||||
text: "Learn about scheduled jobs and how you can create one.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Notification Plugins",
|
||||
text: "Check out available notification plugins.",
|
||||
startIcon: <BoltSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
@@ -1,90 +0,0 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Ecommerce Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll learn how to build an ecommerce store and start selling with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Businesses use an ecommerce store to allow customers to browse their products and make purchases. It also includes accepting payments, managing orders, and more.
|
||||
|
||||
Medusa provides all essential commerce features out-of-the-box. Businesses can go live and start selling without making any adjustments. They can also power-up their store with plugins for payment, fulfillment, and more.
|
||||
|
||||
<Note title="Related use-case">
|
||||
|
||||
[How Tekla created an ecommerce store using Medusa](https://medusajs.com/blog/tekla/).
|
||||
|
||||
</Note>
|
||||
|
||||
<LearningPath pathName="simple-quickstart" />
|
||||
|
||||
---
|
||||
|
||||
## Create a Next.js Starter Storefront
|
||||
|
||||
The Medusa application can be used with any storefront. All you have to do is connect to its Store APIs to use the application's commerce features.
|
||||
|
||||
You can install, use, and customize the Next.js Starter storefront to benefit from all the necessary ecommerce features. Alternatively, you can build your own storefront.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Install Next.js Storefront"
|
||||
text="Learn how to install the Next.js Starter storefront."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## (Optional) Install Plugins
|
||||
|
||||
Medusa provides official plugins for payment, fulfillment, search, and more. These plugins provide new functionalities for both your Medusa application and your storefront.
|
||||
|
||||
For example, to accept payment with Stripe, install the Stripe plugin.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Explore Plugins"
|
||||
text="Explore official plugins and learn how to install them."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Deploy the Medusa Application
|
||||
|
||||
The first step to go live is to deploy the Medusa application. The deployment guides available in our Resources documentation provide extensive step-by-step guide for different hosting providers, as well as a general deployment guide that you can follow to deploy your Medusa application to other providers.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Deploy Medusa application"
|
||||
text="Learn how to deploy the Medusa application to your preferred hosting provider."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Deploy the Storefront
|
||||
|
||||
You can now deploy your storefront. The instructions defer whether you're using the Next.js Starter storefront, a custom storefront, or other forms of storefronts such as a mobile app.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Deploy Next.js Storefront"
|
||||
text="Learn how to deploy the Next.js storefront to your preferred hosting provider."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -1,297 +0,0 @@
|
||||
import { AcademicCapSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Integrate Ecommerce Stack Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll learn how to integrate your systems in your ecommerce stack with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Integrating third-party systems, such as ERP or a CMS, into your ecommerce stack can be challenging. It requires:
|
||||
|
||||
- Establishing connections with the different systems based on each of their APIs.
|
||||
- Building flows that span across multiple systems.
|
||||
- Maintaining data consistency and syncing between your systems.
|
||||
|
||||
Medusa’s architecture and functionalities allow you to integrate third-party systems and build flows around them. It also provides error-handling mechanisms and webhook capabilities that prevent data inconsistency between your systems.
|
||||
|
||||
<LearningPath pathName="integrate-ecommerce-stack" />
|
||||
|
||||
---
|
||||
|
||||
## Connect to External Systems with Services
|
||||
|
||||
Medusa’s Services let you implement a client that connects and performs functionalities with your third-party system. You can then use the service to connect to your third-party system in other resources, such as a Workflow or an API Route.
|
||||
|
||||
This increases the maintainability of your integration as it’s all implemented within the service. If the external system makes any changes to its APIs, you only need to make changes within the service.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create a Service"
|
||||
text="Learn how to create a service in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Create a service for an ERP system">
|
||||
|
||||
Here’s an example of a service created for an external ERP system:
|
||||
|
||||
```ts title="src/services/erp.ts"
|
||||
import { ProductService, TransactionBaseService } from "@medusajs/medusa";
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import axios, { AxiosInstance } from "axios"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productService: ProductService
|
||||
}
|
||||
|
||||
class ErpService extends TransactionBaseService {
|
||||
private client_: AxiosInstance
|
||||
private productService_: ProductService
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
|
||||
this.productService_ = container.productService
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://api.erp-example.com`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.ERP_TOKEN}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getProductERPDetails (id: string) {
|
||||
const product = await this.productService_.retrieve(id, {
|
||||
select: ["external_id"]
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, `Product with id ${id} was not found.`)
|
||||
}
|
||||
|
||||
const erpProduct = await this.client_.get(`/product/${product.external_id}`)
|
||||
|
||||
return erpProduct
|
||||
}
|
||||
|
||||
async updateProduct(product: any) {
|
||||
// do stuff
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpService
|
||||
```
|
||||
|
||||
In this example, you create an `ErpService` that is used to interact with the third-party system. In the service, you create a client using [axios](https://axios-http.com/) to send requests to the ERP system. If the system you’re integrating provides a JavaScript SDK, you can use that instead.
|
||||
|
||||
Then, in the service, you create methods for each ERP functionality. For example, the `getProductErpDetails` method retrieves the details of a product in the ERP system.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Products have an `external_id` attribute that can be used to store the product's ID in an external system.
|
||||
|
||||
</Note>
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Build Flows Across Systems
|
||||
|
||||
With Medusa’s workflows, you can build flows with steps that may perform actions on different systems. For example, you can create a workflow that updates the product’s details in integrated systems like ERPs, WMSs, and CMSs.
|
||||
|
||||
Workflows can be executed from anywhere. So, taking the workflow described in the above example, you can listen to the `product.updated` event using a Subscriber and execute the workflow whenever the event is triggered.
|
||||
|
||||

|
||||
|
||||
Workflows guarantee data consistency through their compensation feature. You can provide a compensation function to steps that roll back the actions of that step. Then, if an error occurs in any step, the actions of previous steps are rolled back using their compensation function.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Workflows"
|
||||
text="Learn more about Workflows and how to create them."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Update products across systems with workflows">
|
||||
|
||||
For example, create the following workflow in `src/workflows/update-product.ts`:
|
||||
|
||||
```ts title="src/workflows/update-product.ts"
|
||||
import { Product, ProductService } from "@medusajs/medusa"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import ErpService from "../services/erp"
|
||||
|
||||
type WorkflowInput = {
|
||||
productId: string
|
||||
}
|
||||
|
||||
const updateInErpStep = createStep("update-in-erp", async ({ productId }: WorkflowInput, context) => {
|
||||
const erpService: ErpService = context.container.resolve("erpService")
|
||||
const productService: ProductService = context.container.resolve("productService")
|
||||
|
||||
const updatedProductData = await productService.retrieve(productId)
|
||||
|
||||
const oldProductData = await erpService.getProductERPDetails(productId)
|
||||
|
||||
await erpService.updateProduct(updatedProductData)
|
||||
|
||||
return new StepResponse({}, {
|
||||
// provide the old product data as a parameter
|
||||
// to the compensation function in case an error
|
||||
// occurs.
|
||||
oldProductData
|
||||
})
|
||||
}, async ({
|
||||
oldProductData
|
||||
}, context) => {
|
||||
const erpService: ErpService = context.container.resolve("erpService")
|
||||
|
||||
await erpService.updateProduct(oldProductData)
|
||||
})
|
||||
|
||||
const updateInCmsStep = createStep("update-in-cms", async({ productId }: WorkflowInput, context) => {
|
||||
// update product in CMS...
|
||||
})
|
||||
|
||||
const updateInWmsStep = createStep("update-in-wms", async({ productId }: WorkflowInput, context) => {
|
||||
// update product in WMS...
|
||||
})
|
||||
|
||||
const updateProductWorkflow = createWorkflow<
|
||||
WorkflowInput, void
|
||||
>("update-product-in-systems",
|
||||
function (input) {
|
||||
updateInErpStep(input)
|
||||
|
||||
updateInCmsStep(input)
|
||||
|
||||
updateInWmsStep(input)
|
||||
})
|
||||
|
||||
export default updateProductWorkflow
|
||||
```
|
||||
|
||||
In this workflow, you create three steps: one to update the product in an ERP, another in a CMS, and another in a WMS.
|
||||
|
||||
In the `update-in-erp` step, you resolve the ERP’s service that you created and use it to update the product in the ERP. You also provide a compensation function, which is passed as a third parameter to the `createStep` function. The compensation function reverts the update in the ERP system in case an error occurs.
|
||||
|
||||
You then create the workflow `update-product-in-systems` which uses the three steps in its constructor function. The workflow’s constructor function doesn’t run until it’s executed.
|
||||
|
||||
Then, create the subscriber at `src/subscribers/update-product.ts`:
|
||||
|
||||
```ts title="src/subscribers/update-product.ts"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
ProductService,
|
||||
} from "@medusajs/medusa"
|
||||
import updateProductWorkflow from "../workflows/update-product"
|
||||
|
||||
export default async function handleProductUpdate({
|
||||
data, eventName, container, pluginOptions
|
||||
}: SubscriberArgs<Record<string, any>>) {
|
||||
updateProductWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
productId: data.id
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Updated product across systems.")
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.UPDATED,
|
||||
context: {
|
||||
subscriberId: "product-update"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The subscriber executes the workflow whenever the `product.updated` event is triggered, passing it the ID of the updated product.
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Create Webhook Listeners
|
||||
|
||||
You can provide webhook listeners that your external systems call when their data is updated. This lets you synchronize data between your systems. To create webhook listeners, create an API route.
|
||||
|
||||
For example, suppose an administrator changes the product data in the ERP system. The system then sends a request to the webhook you define in your Medusa application, which updates the product data in the application.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create an API Route"
|
||||
text="Learn how to create an API Route in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Create a webhook listener for ERP changes">
|
||||
|
||||
For example, create the file `src/api/webhooks/erp/update/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/webhooks/erp/update/route.ts"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
ProductService
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { id, updatedData} = req.body
|
||||
|
||||
const productService: ProductService = req.scope.resolve(
|
||||
"productService"
|
||||
)
|
||||
|
||||
await productService.update(id, updatedData)
|
||||
|
||||
res.status(200)
|
||||
}
|
||||
```
|
||||
|
||||
This creates a webhook listener for an ERP system. It receives the ID of a product and its updated data, assuming that’s how your ERP system sends the data.
|
||||
|
||||
Then, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { MiddlewaresConfig } from "@medusajs/medusa"
|
||||
import { raw } from "body-parser"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
method: ["POST", "PUT"],
|
||||
matcher: "/webhooks/*",
|
||||
bodyParser: false,
|
||||
middlewares: [raw({ type: "application/json" })],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This replaces the default JSON middleware with the raw middleware, which is useful for webhook routes.
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -1,430 +0,0 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Marketplace Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building a marketplace with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
A marketplace is an online commerce store that allows different vendors to sell their products within the same commerce system. Customers can purchase products from any of these vendors, and vendors can manage their orders separately.
|
||||
|
||||
<Note title="Related use-case">
|
||||
|
||||
[How Goodchef built subscription-based purchases with Medusa](https://medusajs.com/blog/foraged/).
|
||||
|
||||
</Note>
|
||||
|
||||
<LearningPath pathName="marketplace" />
|
||||
|
||||
---
|
||||
|
||||
## Associate Data Models with Stores
|
||||
|
||||
By default, data models like users, products, or orders aren't associated with a store, as it's assumed there's one store in Medusa. For a marketplace, each of these data models should be associated with their respective stores.
|
||||
|
||||
To associate these data models with the `Store` data model, extend and customize data models created in the Medusa core package `@medusajs/medusa`, such as the `User` data model, to add a relation to the `Store` data model.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Extend a Data Model"
|
||||
text="Learn how to extend a data model in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Associate User with Store">
|
||||
|
||||
For example, to associate the `User` data model with the `Store` data model, create the file `src/models/user.ts` with the following content:
|
||||
|
||||
```ts title="src/models/user.ts"
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm"
|
||||
import {
|
||||
User as MedusaUser,
|
||||
} from "@medusajs/medusa"
|
||||
import { Store } from "./store"
|
||||
|
||||
@Entity()
|
||||
export class User extends MedusaUser {
|
||||
@Index("UserStoreId")
|
||||
@Column({ nullable: true })
|
||||
store_id?: string
|
||||
|
||||
@ManyToOne(() => Store, (store) => store.members)
|
||||
@JoinColumn({ name: "store_id", referencedColumnName: "id" })
|
||||
store?: Store
|
||||
}
|
||||
```
|
||||
|
||||
Then, you need to extend the `UserRepository` to point to your extended data model. To do that, create the file `src/repositories/user.ts` with the following content:
|
||||
|
||||
```ts title="src/repositories/user.ts"
|
||||
import { User } from "../models/user"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
import {
|
||||
UserRepository as MedusaUserRepository,
|
||||
} from "@medusajs/medusa/dist/repositories/user"
|
||||
|
||||
export const UserRepository = dataSource
|
||||
.getRepository(User)
|
||||
.extend({
|
||||
...Object.assign(
|
||||
MedusaUserRepository,
|
||||
{ target: User }
|
||||
),
|
||||
})
|
||||
|
||||
export default UserRepository
|
||||
```
|
||||
|
||||
Next, you need to create a migration that reflects the changes on the `User` data model in your database. To do that, run the following command to create a migration file:
|
||||
|
||||
```bash
|
||||
npx typeorm migration:create src/migrations/add-user-store-id
|
||||
```
|
||||
|
||||
This creates a file in the `src/migrations` directory of the format `<TIMESTAMP>_add-user-store-id.ts`. Replace the `up` and `down` methods in that file with the methods here:
|
||||
|
||||
```ts title="src/migrations/<TIMESTAMP>_add-user-store-id.ts"
|
||||
// ...
|
||||
|
||||
export class AddUserStoreId1681287255173
|
||||
implements MigrationInterface {
|
||||
// ...
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" ADD "store_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "UserStoreId" ON "user" ("store_id")`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."UserStoreId"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" DROP COLUMN "store_id"`
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Finally, to reflect these changes and start using them, `build` your changes and run migrations with the following commands:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
You can extend other data models in a similar manner to associate them with a store.
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Access Logged-in User
|
||||
|
||||
Throughout your development, you'll likely need access to the logged-in user. For example, you add a new product to the logged-in admin user's store.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Access Logged-in User"
|
||||
text="Learn how to access the logged-in user throughout your project using a Middleware."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Customize Data Management Functionalities
|
||||
|
||||
After associating data models with stores, customize how certain data management functionalities are implemented in the Medusa core package.
|
||||
|
||||
For example, when a new user is created, you ensure that it's associated either with a new store or with the store of the logged-in user. Another example is associating a new product with the logged-in user's store.
|
||||
|
||||
To customize these functionalities, extend services.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Extend a Service"
|
||||
text="Learn how to extend a service in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Extend User Service">
|
||||
|
||||
You can extend the user service to change how the `create` method is implemented.
|
||||
|
||||
To extend the user service, create the file `src/services/user.ts` with the following content:
|
||||
|
||||
```ts title="src/services/user.ts"
|
||||
import { Lifetime } from "awilix"
|
||||
import {
|
||||
UserService as MedusaUserService,
|
||||
} from "@medusajs/medusa"
|
||||
import { User } from "../models/user"
|
||||
import {
|
||||
CreateUserInput as MedusaCreateUserInput,
|
||||
} from "@medusajs/medusa/dist/types/user"
|
||||
import StoreRepository from "../repositories/store"
|
||||
|
||||
type CreateUserInput = {
|
||||
store_id?: string
|
||||
} & MedusaCreateUserInput
|
||||
|
||||
class UserService extends MedusaUserService {
|
||||
static LIFE_TIME = Lifetime.SCOPED
|
||||
protected readonly loggedInUser_: User | null
|
||||
protected readonly storeRepository_: typeof StoreRepository
|
||||
|
||||
constructor(container, options) {
|
||||
super(...arguments)
|
||||
this.storeRepository_ = container.storeRepository
|
||||
|
||||
try {
|
||||
this.loggedInUser_ = container.loggedInUser
|
||||
} catch (e) {
|
||||
// avoid errors when backend first runs
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
user: CreateUserInput,
|
||||
password: string
|
||||
): Promise<User> {
|
||||
if (!user.store_id) {
|
||||
const storeRepo = this.manager_.withRepository(
|
||||
this.storeRepository_
|
||||
)
|
||||
let newStore = storeRepo.create()
|
||||
newStore = await storeRepo.save(newStore)
|
||||
user.store_id = newStore.id
|
||||
}
|
||||
|
||||
return await super.create(user, password)
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService
|
||||
```
|
||||
|
||||
In the `create` method of this extended service, you create a new store if the user being created doesn't have a store associated with it.
|
||||
|
||||
You can then test out your customization by running the `build` command and starting the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa develop
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Listening to Events
|
||||
|
||||
While implementing your marketplace, you'll need to listen to certain events then perform actions asynchronously. For example, you listen to the `order.placed` event and, when triggered, create child orders of the order, separating ordered items by their associated store.
|
||||
|
||||
To listen to events, create Subscribers that subscribe a handler function to an event. In that handler function, implement the desired functionality.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Create a Subscriber"
|
||||
text="Learn how to create a subscriber in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Listen to Order Created Event">
|
||||
|
||||
To listen to the `order.placed` event, create the file `src/subscribers/orderNotifier.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/orderNotifier.ts"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
OrderService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
// TODO perform functionality
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: OrderService.Events.PLACED,
|
||||
context: {
|
||||
subscriberId: "order-placed-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This subscribes the `handleOrder` method to be executed whenever the `order.placed` event is emitted.
|
||||
|
||||
You can then test out your subscriber by running the `build` command and starting the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa develop
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Add Payment and Fulfillment Providers
|
||||
|
||||
Payment and fulfillment providers can be added through plugins or directly in your project. You can either create your own provider, use one of Medusa's official plugins, or use community plugins.
|
||||
|
||||
<Note>
|
||||
|
||||
Payment and fulfillment providers are associated with regions, which are not associated with a store, by default. To allow each store to specify its own payment and fulfillment providers, [associate the region with a store](#associate-data-models-with-stores).
|
||||
|
||||
</Note>
|
||||
|
||||
### Option 1: Create your own providers
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Payment Processor",
|
||||
text: "Learn how to create a payment processor.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Fulfillment Provider",
|
||||
text: "Learn how to create a fulfillment provider.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
### Option 2: Install a Plugin
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Install an Official Medusa Plugin",
|
||||
text: "Check out available Medusa plugins to install.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "https://medusajs.com/plugins/",
|
||||
title: "Install a Community Plugin",
|
||||
text: "Check out available community plugins to install.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Customize Admin
|
||||
|
||||
You can customize the admin to provide an interface to manage your custom features.
|
||||
|
||||
You can extend the Medusa Admin plugin to add widgets to existing pages, add new pages, or add setting pages.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Admin Widget",
|
||||
text: "Learn how to add widgets into existing admin pages.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Admin UI Routes",
|
||||
text: "Learn how to add new pages to your Medusa Admin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Admin Setting Page",
|
||||
text: "Learn how to add new page to the Medusa Admin settings.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Implement Role-Based Access Control
|
||||
|
||||
In your marketplace, you may need to implement role-based access control (RBAC) within stores. This restricts some users' permissions to specified functionalities or API Routes.
|
||||
|
||||
To implement this functionality, follow the RBAC recipe.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Implement Role-Based Access Control"
|
||||
text="Learn how to implement role-based access control (RBAC) in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Build a Storefront
|
||||
|
||||
Medusa provides a Next.js Starter. Since you've customized your Medusa project, you must either customize the existing Next.js Starter, or create a custom storefront.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Next.js Starter",
|
||||
text: "Install the Next.js Starter to customize it.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Custom Storefront",
|
||||
text: "Find resources to build a storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Deploy Marketplace
|
||||
|
||||
The Medusa Resources documentation includes deployment guides for a basic Medusa application. You can follow it to deploy your customized marketplace, as well.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Deploy Medusa application"
|
||||
text="Learn how to deploy your marketplace to different hosting providers."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -1,212 +0,0 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Multi-Region Store Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building a multi-region store with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
A multi-regional store allows merchants to sell across different countries. This includes supporting each country's tax rules, currency, available shipping and payment options, and more.
|
||||
|
||||
Medusa comes with multi-regional support out of the box. This recipe explains how to benefit from Medusa's features to create a multi-regional store.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Region Setup
|
||||
|
||||
With Medusa, you can create unlimited regions in your store. Each region has configurations managed through the Medusa Admin Dashboard or the Admin API.
|
||||
|
||||
### Currency
|
||||
|
||||
Merchants specify the currency of each region. Multiple regions can have the same currency, but a region has only one currency.
|
||||
|
||||
When customers view your products from a region, they see the prices in the region’s currency. As explained later, this also affects which prices are shown to the customer in a region.
|
||||
|
||||
### Tax rates and providers
|
||||
|
||||
At the regional level, merchants define a default tax rate and tax rates for specific conditions (for example, for a particular set of products). Each region also has a different tax provider that calculates the taxes. You can use Medusa's default tax provider or create a custom tax provider.
|
||||
|
||||
During checkout, the taxes are calculated based on the customer's region, tax configurations, and rates.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Taxes in Medusa Admin",
|
||||
text: "Learn how to manage taxes in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Taxes Using Admin APIs",
|
||||
text: "Learn how to manage tax settings using the Admin APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Tax Provider",
|
||||
text: "Learn how to create a tax provider.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
### Payment and Fulfillment Providers
|
||||
|
||||
Merchants choose which payment and fulfillment providers are available in each region. For example, one region can use Payment Provider A and B while another only uses Payment Provider B.
|
||||
|
||||
During checkout, customers only see the payment providers and shipping options configured for the region. This lets you give customers a localized experience that feels familiar and instills trust.
|
||||
|
||||
Medusa provides official plugins for payment and fulfillment providers. You can also create custom providers.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Providers in Medusa Admin",
|
||||
text: "Learn how to manage providers in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Available Payment Plugins",
|
||||
text: "Check out available payment plugins.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Payment Provider",
|
||||
text: "Learn how to create a payment provider.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Fulfillment Provider",
|
||||
text: "Learn how to create a fulfillment provider.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Prices Per Region and Currency
|
||||
|
||||
When a merchant sets the price of a product in Medusa, they specify the price per currency and region. Merchants also benefit from this pricing mechanism when adding sales or overriding prices for specific conditions using the Pricing Module's features.
|
||||
|
||||
Then, customers see the price of a product based on the region they're in, the region's currency, and whether any special pricing applies to their case. Medusa has a pricing strategy to show the customer the best price based on their context.
|
||||
|
||||
Using the tax-inclusive feature, merchants specify prices including taxes per currency and region. Medusa then calculates the tax amount applied to a line item in the cart based on the region's tax configurations.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Setting Product Prices in Medusa Admin",
|
||||
text: "Learn how to set a product prices in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Display Product Price in Storefront",
|
||||
text: "Learn how to display the correct product price in a storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Multi-Warehouse Support
|
||||
|
||||
Medusa's Inventory and Stock Location Modules provide multi-warehouse features that allow you to manage inventory across different locations. Merchants then control which location an item in an order is fulfilled, allowing them to keep a correct inventory count across locations and sales channels.
|
||||
|
||||
A multi-regional setup lets you manage your inventory through Medusa across the different regions you serve. Your products’ inventory and availability aren't bound to a single stock location, and customers are always shown accurate inventory information based on the location associated with their sales channel.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Multi-warehouse Overview",
|
||||
text: "Learn about the Multi-warehouse feature in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Stock Locations",
|
||||
text: "Learn how to manage stock locations in the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Inventory",
|
||||
text: "Learn how to manage inventory in the Medusa Admin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Multi-Lingual Setup
|
||||
|
||||
By integrating a third-party Content Management Systems (CMS), you benefit from rich content features including managing your content in multiple languages. This allows you to cater to customers’ different languages in the regions you serve.
|
||||
|
||||
Medusa provides an official Contentful plugin and a community Strapi plugin. You can also create a CMS plugin that integrates with your preferred CMS platform to provide multi-lingual features.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "CMS Plugins",
|
||||
text: "Learn about available CMS plugins.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "How to Create a Plugin",
|
||||
text: "Learn how to create a plugin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Automatic Location Detection
|
||||
|
||||
When you sell across regions, it's essential to detect the customer's region and provide them with the products, prices, and content relevant to them without requiring actions from their side.
|
||||
|
||||
Medusa provides an IP Lookup plugin that lets you detect a customer's location and region using [ipstack](https://ipstack.com/). You can also create a plugin that detects the location through a different logic.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "IP Lookup Plugin",
|
||||
text: "Install the IP Lookup plugin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "How to Create a Plugin",
|
||||
text: "Learn how to create a plugin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -1,179 +0,0 @@
|
||||
import { AcademicCapSolid, NextJs } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Personalized Products Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building digital products with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Personalized products are products that customers can customize based on their need. For example, they can upload an image to print on a shirt or provide a message to include in a letter.
|
||||
|
||||
Medusa’s modular architecture and customization tools allows you to create custom data models and expose new features through the REST APIs. You also have freedom in how you choose to implement the storefront, allowing you to build a unique experience around your products.
|
||||
|
||||
---
|
||||
|
||||
## Store Personalized Data
|
||||
|
||||
Most of the data models in Medusa’s commerce modules include a `metadata` attribute, which can be used to store custom data as key-value pairs.
|
||||
|
||||
The `Product` data model represents the main product, whereas the `ProductVariant` data model represents the different saleable options of that product. For example, a shirt is a `Product`, and each different color of the shirt is the `ProductVariant`. The `LineItem` data model is the product variant added to the cart.
|
||||
|
||||
So, you can use the `metadata` attribute of the `LineItem` data model to store the customer’s personalization.
|
||||
|
||||
For example, if you’re asking customers to enter a message to put in a letter they’re purchasing, use the `metadata` attribute of the `LineItem` data model to set the personalized information entered by the customer.
|
||||
|
||||
<Note>
|
||||
|
||||
Two line items in the cart having different `metadata` attributes are not considered the same item. So, each line item is managed separately and can have its own quantity.
|
||||
|
||||
</Note>
|
||||
|
||||
In more complex cases, you can extend data models, such as the `Product` data model, to add more attributes. You can also create new custom data models to hold your personalized data and logic.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Metadata Attribute",
|
||||
text: "Learn about the metadata attribute and how to use it.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Data Model",
|
||||
text: "Learn how to create a data model.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Extend a Data Model",
|
||||
text: "Learn how to extend a data model.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Build a Custom Storefront
|
||||
|
||||
Due to Medusa's modular architecture, there are no restrictions on what language or framework you use to build the storefront or what design or experience you provide customers. The storefront connects to the Medusa application using the Store API Routes.
|
||||
|
||||
You can build a unique experience around your products that focuses on the customer’s personalization capabilities.
|
||||
|
||||
Medusa provides a Next.js Starter storefront with basic ecommerce functionalities that can be customized. You can also build your own storefront and use Medusa’s client libraries or Store API Routes to communicate with the Medusa application.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Next.js Starter",
|
||||
text: "Learn about the Next.js Starter and how to install it.",
|
||||
startIcon: <NextJs />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Build Your Own Storefront",
|
||||
text: "Follow this roadmap to learn how to create a storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Pass Personalized Data to the Order
|
||||
|
||||
If you followed the basic approach of using the `metadata` attribute to store the personalized data, pass the personalization data when you add an item to the cart in the storefront using the [Add a line item](https://docs.medusajs.com/api/store#carts_postcartscartlineitems) API Route. This API Route accepts a `metadata` request body parameter that will be stored in the created line item’s `metadata` attribute.
|
||||
|
||||
In the case that you’ve created a custom data model or extended a data model, create a custom API Route that handles saving the personalization data. Then, call that API Route from the storefront before or after adding the item to the cart.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "https://docs.medusajs.com/api/store#carts_postcartscartlineitems",
|
||||
title: "Add a line item API Route",
|
||||
text: "Learn about the expected request parameters and response.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create API Route",
|
||||
text: "Learn how to create an API Route.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Access Personalized Data in the Order
|
||||
|
||||
Line items are associated with the placed order. So, if you stored the personalized data in the `metadata` of the line items, expand the `items` relation on the `Order` data model to retrieve the `metadata` attribute of the line items.
|
||||
|
||||
On the other hand, if you’ve created a custom data model or extended a data model, create a custom API route that handles retrieving the personalization data. Alternatively, if the data model you’ve created or customized is associated with the `Order` data model, expand it similarly to the `items` relation.
|
||||
|
||||
To show the personalized data in the Medusa Admin, extend the Medusa Admin to add a widget or a page to show the personalized data.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "https://docs.medusajs.com/api/store#expanding-fields",
|
||||
title: "Expanding Relations in API Requests",
|
||||
text: "Learn how to expand relations in API requests.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Expanding Relations in Services",
|
||||
text: "Learn how to expand relations in services.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Extending Medusa Admin",
|
||||
text: "Learn how to extend the Medusa Admin with widgets and more.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Fulfill Personalized Products in Orders
|
||||
|
||||
To access the order’s details and fulfill its personalized products, create a subscriber that listens to the `order.placed` event.
|
||||
|
||||
For example, if the customer personalizes a shirt to include an image or message, create a subscriber that listens to the `order.placed` event and sends the personalization to the printing third-party fulfillment service.
|
||||
|
||||
Alternatively, you can create a fulfillment service that integrates a third-party service or mechanism to fulfill the personalized product. This is useful if you want the fulfillment of personalized products to be embedded into Medusa’s existing order fulfillment.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber and listen to events.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Fulfillment Service",
|
||||
text: "Learn how to create a fulfillment provider in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
CreditCardSolid,
|
||||
ClockSolid,
|
||||
DocumentTextSolid,
|
||||
Stripe
|
||||
Stripe,
|
||||
PhotoSolid,
|
||||
BuildingsSolid
|
||||
} from "@medusajs/icons"
|
||||
|
||||
# Medusa Learning Resources
|
||||
@@ -43,11 +45,13 @@ Follow the [Medusa Book](https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app)
|
||||
{
|
||||
startIcon: <CheckCircleSolid />,
|
||||
title: "Order Module",
|
||||
href: "/commerce-modules/order",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <ShoppingCartSolid />,
|
||||
title: "Cart Module",
|
||||
href: "/commerce-modules/product",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
@@ -103,6 +107,49 @@ Follow the [Medusa Book](https://medusa-docs-v2-git-docs-v2-medusajs.vercel.app)
|
||||
|
||||
---
|
||||
|
||||
## Recipes
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
startIcon: <ShoppingCartSolid />,
|
||||
title: "Ecommerce",
|
||||
href: "/recipes/ecommerce",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <BuildingStorefront />,
|
||||
title: "Marketplace",
|
||||
href: "/recipes/marketplace",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <PhotoSolid />,
|
||||
title: "Digital Products",
|
||||
href: "/recipes/digital-products",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <PuzzleSolid />,
|
||||
title: "Integrate Ecommerce Stack",
|
||||
href: "/recipes/integrate-ecommerce-stack",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <ClockSolid />,
|
||||
title: "Commerce Automation",
|
||||
href: "/recipes/commerce-automation",
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
startIcon: <BuildingsSolid />,
|
||||
title: "B2B",
|
||||
href: "/recipes/b2b",
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## SDKs and Tools
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
@@ -0,0 +1,869 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
|
||||
export const metadata = {
|
||||
title: `B2B Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This recipe provides the general steps to implement a B2B store with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
In a B2B store, you provide different types of customers with relevant pricing, products, shopping experience, and more.
|
||||
|
||||
Medusa’s commerce modules, including Sales Channel, Customer, and Pricing modules facilitate implementing this setup. Medusa’s architecture and extendible nature allow you to customize your store based on your use case.
|
||||
|
||||
<Note title="Related use-case">
|
||||
|
||||
[Visionary: Frictionless B2B ecommerce with Medusa](https://medusajs.com/blog/visionary/)
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Sales Channel
|
||||
|
||||
Use sales channels to set product availability per channel. In this case, create a B2B sales channel that includes only B2B products.
|
||||
|
||||
You can create a sales channel through the Medusa Admin or Admin REST APIs.
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the sales channel using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#sales-channels_postsaleschannels",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the sales channel using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create a Publishable API Key
|
||||
|
||||
Use publishable API keys to specify the context of client requests:
|
||||
|
||||
- You associate the publishable API key with one or more sales channels.
|
||||
- In a client such as a storefront, you pass the publishable API key in the header of your requests.
|
||||
|
||||
Then, all products retrieved belong to the associated sales channel(s).
|
||||
|
||||
You can create a publishable API key through the Medusa Admin or the Admin REST APIs, then associate it with the B2B sales channel. Later, you'll use this key when developing your B2B storefront.
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
### Create Key
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the publishable API key using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#api-keys_postapikeys",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the publishable API key using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
### Associate Key with Sales Channel
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Associate the publishable API key with the B2B sales channel using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Associate the publishable API key with the B2B sales channel using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Add Products to B2B Sales Channel
|
||||
|
||||
You can create new products or add existing ones to the B2B sales channel using the Medusa Admin or Admin REST APIs.
|
||||
|
||||
### Create Products
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Using Medusa Admin",
|
||||
text: "Create products using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#products_postproducts",
|
||||
title: "Using REST APIs",
|
||||
text: "Create products using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
### Add Products to Sales Channel
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Using Medusa Admin",
|
||||
text: "Add the products to the B2B sales channel using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#sales-channels_postsaleschannelsidproductsbatchadd",
|
||||
title: "Using REST APIs",
|
||||
text: "Add the products to the B2B sales channel using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Module with Relationship to Customer Groups
|
||||
|
||||
Use customer groups to organize your customers into different groups. Then, you can apply different prices for each group.
|
||||
|
||||
This is useful in B2B sales, as you often negotiate special prices with each customer or company.
|
||||
|
||||
You can create a B2B module that adds necessary data models to represent a B2B company. Then, you link that company to a customer group. Any customer belonging to that group also belongs to the company, meaning they're a B2B customer.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create Module",
|
||||
text: "Learn how to create a module in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/basics/data-models",
|
||||
title: "Create Data Models",
|
||||
text: "Learn how to create data models.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Card
|
||||
href="!docs!/advanced-development/modules/module-relationships"
|
||||
title="Create Module Relationships"
|
||||
text="Learn how to create a relationship between modules."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example">
|
||||
In this section, you'll create a B2B module that has a `Company` data model. The `Company` data model has a relationship to the `CustomerGroup` data model of the Customer Module.
|
||||
|
||||
Start by creating the `src/modules/b2b` directory.
|
||||
|
||||
Then, create the file `src/modules/b2b/models/company.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/b2b/models/company.ts" highlights={[["23", "", "Field will be used to create a relationship to customer groups."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import { BaseEntity } from "@medusajs/utils"
|
||||
import {
|
||||
Entity,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class Company extends BaseEntity {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
city: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
country_code: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
customer_group_id?: string
|
||||
}
|
||||
```
|
||||
|
||||
This creates a `Company` data model with some relevant fields. Most importantly, it has a `customer_group_id` field. It'll later be used when creating the relationship to the `CustomerGroup` data model in the Customer Module.
|
||||
|
||||
Next, create the migration in the file `src/modules/b2b/migrations/Migration20240516081502.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/b2b/migrations/Migration20240516081502.ts"
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20240516081502 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "company" ("id" text not null, "name" text not null, "city" text not null, "country_code" text not null, "customer_group_id" text not null, constraint "company_pkey" primary key ("id"));');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "company" cascade;');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You'll run the migration to reflect the data model in the database after finishing the module definition.
|
||||
|
||||
Before creating the module's main service, create the file `src/types/b2b/index.ts` with some helper types:
|
||||
|
||||
```ts title="src/types/b2b/index.ts"
|
||||
import { CustomerGroupDTO } from "@medusajs/types"
|
||||
|
||||
export type CompanyDTO = {
|
||||
id: string
|
||||
name: string
|
||||
city: string
|
||||
country_code: string
|
||||
customer_group_id?: string
|
||||
customer_group?: CustomerGroupDTO
|
||||
}
|
||||
|
||||
export type CreateCompanyDTO = {
|
||||
name: string
|
||||
city: string
|
||||
country_code: string
|
||||
customer_group_id?: string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You can now create the module's main service at `src/modules/b2b/service.ts` with the following content:
|
||||
|
||||
export const mainServiceHighlights = [
|
||||
["42", "relationships", "Implement the relationship to the `CustomerGroup` data model in the Customer Module."],
|
||||
["56", "create", "Implement a create method to create a company."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/b2b/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { ModuleJoinerConfig, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { Company } from "./models/company";
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../../types/b2b";
|
||||
|
||||
type InjectedDependencies = {
|
||||
companyService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
Company: {
|
||||
dto: CompanyDTO
|
||||
}
|
||||
}
|
||||
|
||||
class B2bModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
CompanyDTO,
|
||||
AllModelsDTO
|
||||
>(Company, []) {
|
||||
companyService_: ModulesSdkTypes.InternalModuleService<Company>
|
||||
|
||||
constructor({ companyService }: InjectedDependencies) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.companyService_ = companyService
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return {
|
||||
serviceName: "b2bModuleService",
|
||||
alias: [
|
||||
{
|
||||
name: ["company"],
|
||||
args: {
|
||||
entity: Company.name
|
||||
}
|
||||
}
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.CUSTOMER,
|
||||
alias: "customer_group",
|
||||
primaryKey: "id",
|
||||
foreignKey: "customer_group_id",
|
||||
args: {
|
||||
methodSuffix: "CustomerGroups"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async create (data: CreateCompanyDTO): Promise<CompanyDTO> {
|
||||
const company = this.companyService_.create(data)
|
||||
|
||||
return company
|
||||
}
|
||||
}
|
||||
|
||||
export default B2bModuleService
|
||||
```
|
||||
|
||||
This creates a `B2bModuleService` that extends the service factory and implements:
|
||||
|
||||
- The module's relationship to the `CustomerGroup` data model in the Customer Module within the `__joinerConfig` method.
|
||||
- A `create` method to create a company.
|
||||
|
||||
Next, create the module definition at `src/modules/b2b/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/b2b/index.ts"
|
||||
import B2bModuleService from "./service";
|
||||
|
||||
export default {
|
||||
service: B2bModuleService,
|
||||
}
|
||||
```
|
||||
|
||||
Finally, add the module to the `modules` object in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
b2bModuleService: {
|
||||
resolve: "./modules/b2b",
|
||||
definition: {
|
||||
isQueryable: true
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can now run migrations with the following commands:
|
||||
|
||||
```bash npm2yarn
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
### Add Create Company API Route
|
||||
|
||||
To test out using the B2B Module, you'll add an API route to create a company.
|
||||
|
||||
Start by creating the file `src/workflows/create-company.ts` with the following content:
|
||||
|
||||
export const workflowHighlights = [
|
||||
["23", "tryToCreateCustomerGroupStep", "This step creates the customer group if its data is passed in the `customer_group` property."],
|
||||
["36", "createCustomerGroupsWorkflow", "Use the `createCustomerGroupsWorkflow` defined by Medusa to create the customer group."],
|
||||
["44", "", "Set the ID of the new customer group in the `customer_group_id` property so that it's added to the created company."],
|
||||
["54", "createCompanyStep", "This step creates the company."],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/create-company.ts" highlights={workflowHighlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
createWorkflow
|
||||
} from "@medusajs/workflows-sdk";
|
||||
import {
|
||||
createCustomerGroupsWorkflow
|
||||
} from "@medusajs/core-flows"
|
||||
import { CreateCustomerGroupDTO } from "@medusajs/types";
|
||||
import { CompanyDTO, CreateCompanyDTO } from "../types/b2b";
|
||||
import B2bModuleService from "../modules/b2b/service";
|
||||
|
||||
export type CreateCompanyWorkflowInput = CreateCompanyDTO & {
|
||||
customer_group?: CreateCustomerGroupDTO
|
||||
}
|
||||
|
||||
type CreateCompanyWorkflowOutput = {
|
||||
company: CompanyDTO
|
||||
}
|
||||
|
||||
type CreateCustomerGroupStepInput = CreateCompanyWorkflowInput
|
||||
|
||||
const tryToCreateCustomerGroupStep = createStep(
|
||||
"try-to-create-customer-group-step",
|
||||
async (
|
||||
{
|
||||
customer_group,
|
||||
...company
|
||||
}: CreateCustomerGroupStepInput,
|
||||
{ container }) => {
|
||||
if (!customer_group) {
|
||||
return new StepResponse({ company })
|
||||
}
|
||||
|
||||
// create customer group
|
||||
const { result } = await createCustomerGroupsWorkflow(
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
customersData: [customer_group]
|
||||
}
|
||||
})
|
||||
|
||||
company.customer_group_id = result[0].id
|
||||
|
||||
return new StepResponse({ company })
|
||||
}
|
||||
)
|
||||
|
||||
export type CreateCompanyStep = {
|
||||
companyData: CreateCompanyDTO
|
||||
}
|
||||
|
||||
const createCompanyStep = createStep(
|
||||
"create-company-step",
|
||||
async (
|
||||
{ companyData }: CreateCompanyStep,
|
||||
{ container }) => {
|
||||
const b2bModuleService: B2bModuleService = container
|
||||
.resolve(
|
||||
"b2bModuleService"
|
||||
)
|
||||
|
||||
const company = await b2bModuleService.create(
|
||||
companyData
|
||||
)
|
||||
|
||||
return new StepResponse({ company })
|
||||
}
|
||||
)
|
||||
|
||||
export const createCompanyWorkflow = createWorkflow<
|
||||
CreateCompanyWorkflowInput,
|
||||
CreateCompanyWorkflowOutput
|
||||
>(
|
||||
"create-company",
|
||||
function (input) {
|
||||
const {
|
||||
company: companyData
|
||||
} = tryToCreateCustomerGroupStep(input)
|
||||
|
||||
const company = createCompanyStep({ companyData })
|
||||
|
||||
return company
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You create a workflow with two steps:
|
||||
|
||||
1. The first one tries to create a customer group if its data is provided in the `customer_group` property and sets its value in the `customer_group_id` field.
|
||||
2. The second one creates the company.
|
||||
|
||||
Then, create the file `src/api/admin/b2b/company/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/admin/b2b/company/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/medusa";
|
||||
import {
|
||||
CreateCompanyWorkflowInput,
|
||||
createCompanyWorkflow
|
||||
} from "../../../../workflows/create-company"
|
||||
|
||||
type CreateCompanyReq = CreateCompanyWorkflowInput
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<CreateCompanyReq>,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await createCompanyWorkflow(req.scope)
|
||||
.run({
|
||||
input: req.body
|
||||
})
|
||||
|
||||
res.json({
|
||||
company: result.company
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The API route uses the workflow to create the company. It passes the request body as the workflow's input.
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test the API route, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Next, make sure you authenticate as an admin user as explained in [this Authentication guide](!api!/api/admin#authentication).
|
||||
|
||||
Then, send a `POST` request to the `/admin/b2b/company` API route:
|
||||
|
||||
```bash
|
||||
curl -X POST 'localhost:9000/admin/b2b/company' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer {jwt_token}' \
|
||||
--data '{
|
||||
"name": "Acme",
|
||||
"city": "London",
|
||||
"country_code": "gb",
|
||||
"customer_group": {
|
||||
"name": "B2B"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
This creates a company and its associated customer group.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can alternatively pass a `customer_group_id` to use an existing customer group.
|
||||
|
||||
</Note>
|
||||
|
||||
</Details>
|
||||
|
||||
## Add B2B Customers
|
||||
|
||||
After adding your B2B customer group, add B2B customers and assign them to the B2B customer group.
|
||||
|
||||
You can do that through the Medusa Admin or Admin REST APIs.
|
||||
|
||||
### Create Customers
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Create the customers using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#customers_postcustomers",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Create the customers using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
### Assign Customers to Groups
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Option 1: Use Medusa Admin",
|
||||
text: "Assign the customers to the B2B customer group using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#customer-groups_postcustomergroupsidcustomersbatch",
|
||||
title: "Option 2: Using the REST APIs",
|
||||
text: "Assign the customers to the B2B customer group using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create B2B Price List
|
||||
|
||||
Use price lists to set different prices for each B2B customer group, among other conditions.
|
||||
|
||||
You can create a price list using the Medusa Admin or the Admin REST APIs. Make sure to set the B2B customer group as a condition.
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Using Medusa Admin",
|
||||
text: "Create the price list using the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/api/admin#price-lists_postpricelists",
|
||||
title: "Using REST APIs",
|
||||
text: "Create the price list using the REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Create Custom Data Model
|
||||
|
||||
To implement a more advanced B2B sales flow, add custom data models such as `Company`, `Employee`, `Admin`, and `Buyer` to your B2B module.
|
||||
|
||||
This provides more granular control of your B2B sales and allows you to build features like privileges, limits, and more.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/data-models"
|
||||
title="Create a Data Model"
|
||||
text="Learn how to create a custom data model in a module."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Create an API Route to Check Customers
|
||||
|
||||
On the frontend clients communicating with your store, such as the storefront, you need to check whether the currently logged-in customer is a B2B customer.
|
||||
|
||||
The API route can check if the customer has any group with an associated company.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/api-routes"
|
||||
title="Create an API Route"
|
||||
text="Learn how to create an API Route in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example">
|
||||
|
||||
For example, create the API route `src/api/store/b2b/check-customer/route.ts` with the following content:
|
||||
|
||||
export const checkCustomerHighlights = [
|
||||
["16", "retrieve", "Retrieve the customer along with its groups."],
|
||||
["20", "list", "List the companies that have a customer group ID matching any of the customer's group IDs."],
|
||||
["25", "", "Return whether there are any companies associated with the customer's groups."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/b2b/check-customer/route.ts" highlights={checkCustomerHighlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
|
||||
import type { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk";
|
||||
import { ICustomerModuleService } from "@medusajs/types";
|
||||
import B2bModuleService from "../../../../modules/b2b/service";
|
||||
|
||||
export async function GET(
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const customerModuleService: ICustomerModuleService = req
|
||||
.scope.resolve(ModuleRegistrationName.CUSTOMER)
|
||||
const b2bModuleService: B2bModuleService = req.scope.resolve(
|
||||
"b2bModuleService"
|
||||
)
|
||||
|
||||
const customer = await customerModuleService.retrieve(req.auth.actor_id, {
|
||||
relations: ["groups"]
|
||||
})
|
||||
|
||||
const companies = await b2bModuleService.list({
|
||||
customer_group_id: customer.groups.map((group) => group.id)
|
||||
})
|
||||
|
||||
res.json({
|
||||
is_b2b: companies.length > 0
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This creates a `GET` API Route at `/store/b2b/check-customer` that:
|
||||
|
||||
1. Retrieves the customer along with its groups using the Customer Module's main service.
|
||||
2. Lists the companies that have a customer group ID matching any of the customer's group IDs.
|
||||
3. Return an `is_b2b` field whose value is `true` if there are any companies associated with the customer's groups.
|
||||
|
||||
Before using the API route, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { MiddlewaresConfig, authenticate } from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store/b2b*",
|
||||
middlewares: [
|
||||
authenticate("store", ["bearer", "session"])
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that only logged-in customers can use the API route.
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test out the API route:
|
||||
|
||||
1. Start the Medusa application.
|
||||
2. Obtain an authentication JWT token for a new customer. Do that by sending a `POST` request to the `/auth/store/emailpass` API Route:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/store/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "test@medusajs.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
3. Send a `POST` request to the `/store/customers` API route that registers the customer. Make sure to pass the authentication JWT token from the previous token in the header:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/store/customers' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {jwt_token}' \
|
||||
--data-raw '{
|
||||
"email": "test@medusajs.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
4. Add the customer to the B2B group as explained in a [previous section](#add-b2b-customers).
|
||||
5. Send a `GET` request to the `/store/b2b/check-customer` API route you created in this section:
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:9000/store/b2b/check-customer' \
|
||||
--header 'Authorization: Bearer {jwt_token}'
|
||||
```
|
||||
|
||||
You'll receive a JSON response as the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"is_b2b": true
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
## Customize Admin
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon!
|
||||
|
||||
</Note>
|
||||
|
||||
Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.
|
||||
|
||||
The Medusa Admin plugin can be extended to add widgets, new pages, and setting pages.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "!docs!/advanced-development/admin/widgets",
|
||||
title: "Create Admin Widget",
|
||||
text: "Learn how to add widgets into existing admin pages.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/advanced-development/admin/ui-routes",
|
||||
title: "Create Admin UI Routes",
|
||||
text: "Learn how to add new pages to your Medusa Admin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/advanced-development/admin/setting-pages",
|
||||
title: "Create Admin Setting Page",
|
||||
text: "Learn how to add new page to the Medusa Admin settings.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Customize Storefront
|
||||
|
||||
Medusa provides a Next.js storefront to use with your application. You can either customize it or build your own to represent your B2B features.
|
||||
|
||||
Use the publishable API key you associated with your B2B sales channel in the storefront to ensure only B2B products are retrieved.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "/nextjs-starter",
|
||||
title: "Next.js Storefront",
|
||||
text: "Learn how to install and customize the Next.js storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/storefront-development/tips",
|
||||
title: "Storefront Tips",
|
||||
text: "Find tips on developing a custom storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Card
|
||||
href="!api!/api/store#publishable-api-key"
|
||||
title="Use Publishable API Keys"
|
||||
text="Learn how to use the publishable API key in client requests."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
className="mt-1"
|
||||
/>
|
||||
File diff suppressed because it is too large
Load Diff
+1186
-785
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
import { AcademicCapSolid, UsersSolid, PuzzleSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Ecommerce Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This recipe provides the general steps to create an ecommerce store with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Businesses use ecommerce stores to:
|
||||
|
||||
- Provide an online catalog for customers.
|
||||
- Accept customer orders and payment.
|
||||
- Manage their store's data and logistics.
|
||||
|
||||
Medusa provides all essential commerce features out-of-the-box. Businesses can go live and start selling without making any adjustments. They can also power-up their store by integrating third-party services for payments, fulfillment, and more.
|
||||
|
||||
<Note title="Related use-case">
|
||||
|
||||
[How Tekla created an ecommerce store using Medusa](https://medusajs.com/blog/tekla/).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Install Ecommerce Store Powered by Medusa
|
||||
|
||||
Use the following command to install an ecommerce store with Medusa:
|
||||
|
||||
```bash
|
||||
npx create-medusa-app@preview --with-nextjs-starter
|
||||
```
|
||||
|
||||
This installs:
|
||||
|
||||
- The Medusa application, which is composed of a Node.js server and a Medusa Admin dashboard. The dashboard opens right after the installation finishes, where you can create a user and start managing your store's data.
|
||||
- A [Next.js starter storefront](../../nextjs-starter/page.mdx) that connects to your Medusa application to provide customers with ecommerce features.
|
||||
|
||||
---
|
||||
|
||||
## Integrate Third-Party Services
|
||||
|
||||
You can integrate third-party services and tools, customizing the architecture and commerce features of your store.
|
||||
|
||||
For example, you can integrate Stripe for payment, or SendGrid for notifications.
|
||||
|
||||
<Card
|
||||
href="/integrations"
|
||||
title="Integrations"
|
||||
text="Check out available integrations for your Medusa application."
|
||||
startIcon={<PuzzleSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Deploy the Medusa Application
|
||||
|
||||
Our documentation has step-by-step guides to deploy your Medusa application, Medusa Admin, and the Next.js starter storefront.
|
||||
|
||||
<Card
|
||||
href="/deployment"
|
||||
title="Deployment Guides"
|
||||
text="Learn how to deploy the Medusa application, Medusa Admin, and Next.js starter storefront."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Add Custom Features
|
||||
|
||||
Along with the extensive ecommerce features, Medusa also provides the architecture and tools to customize and add new features.
|
||||
|
||||
To learn how to develop customziations with Medusa, refer to the [Medusa documentation](!docs!)
|
||||
@@ -0,0 +1,365 @@
|
||||
import { AcademicCapSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Integrate Ecommerce Stack Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This recipe provides the general steps to integrate your ecommerce stack with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Integrating third-party systems, such as ERP or a CMS, into your ecommerce stack requires:
|
||||
|
||||
- Establishing connections with the different systems based on each of their APIs.
|
||||
- Building flows that span across multiple systems.
|
||||
- Maintaining data consistency and syncing between your systems.
|
||||
|
||||
Medusa’s architecture and functionalities allow you to integrate third-party systems and build flows around them. It also provides error-handling mechanisms and webhook capabilities that prevent data inconsistency between your systems.
|
||||
|
||||
---
|
||||
|
||||
## Integrate External System with a Module
|
||||
|
||||
To integrate an external system, such as an ERP, into your Medusa application, create a module whose service has methods to connect to the external system.
|
||||
|
||||
Then, resolve the module's main service in other resources, such as API routes or workflows, to perform actions in the external system.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/modules-and-services"
|
||||
title="Create a Module"
|
||||
text="Learn how to create a module in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Create a module integrating an ERP system">
|
||||
|
||||
This example showcases how to create a module that integrates to a dummy ERP system.
|
||||
|
||||
Start by creating the directory `src/modules/erp` for your module.
|
||||
|
||||
Then, create the file `src/modules/erp/service.ts` with the following content:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["4", "ErpModuleOptions", "The module's expected options."],
|
||||
["9", "client_", "The client to connect to the external system."],
|
||||
["12", "create", "Create the client using Axios. Can instead use an SDK if the external system has one."],
|
||||
["20", "getProductData", "This method retrieves the product's data from the ERP system."],
|
||||
["26", "createProduct", "This method creates a product in the ERP system."],
|
||||
["32", "deleteProduct", "This method deletes the product in the ERP system."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/erp/service.ts" highlights={serviceHighlights}
|
||||
import axios, { AxiosInstance } from "axios"
|
||||
import { ProductDTO } from "@medusajs/types"
|
||||
|
||||
type ErpModuleOptions = {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
class ErpModuleService {
|
||||
private client_: AxiosInstance
|
||||
|
||||
constructor({}, { apiKey }: ErpModuleOptions) {
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://api.erp-example.com`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getProductData (id: string) {
|
||||
const { data: erpProduct } = await this.client_.get(`/product/${id}`)
|
||||
|
||||
return erpProduct
|
||||
}
|
||||
|
||||
async createProduct(data: ProductDTO) {
|
||||
const { data: erpProduct } = await this.client_.post(`/product`, data)
|
||||
|
||||
return erpProduct
|
||||
}
|
||||
|
||||
async deleteProduct(id: string) {
|
||||
await this.client_.delete(`/product/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpModuleService
|
||||
```
|
||||
|
||||
This creates the module's main service. Few things to note:
|
||||
|
||||
- The module accepts an `apiKey` option, used to authenticate to the dummy ERP system. The module's main service accesses this option in the second parameter of the constructor.
|
||||
- The module uses axios to create a client in the constructor. The client is used in the service's methods when connecting to the ERP system. If the system you're integrating has an SDK, you can initialize it in the constructor, instead.
|
||||
- The `getProductData` method retrieves a product's details from the ERP system by sending a `GET` request using the client.
|
||||
- The `createProduct` method creates a product in the ERP system by sending a `POST` request using the client.
|
||||
- The `deleteProduct` method deletes a product in the ERP system by sending a `DELETE` request using the client.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can store the product's ID in the external system using the `metadata` field of the `Product` data model in the Product Module. Alternatively, you can create a [data model](!docs!/basics/data-models) in your module to store data related to the external system.
|
||||
|
||||
</Note>
|
||||
|
||||
Then, create the module's definition file at `src/modules/erp/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/erp/index.ts"
|
||||
import ErpModuleService from "./service";
|
||||
|
||||
export default {
|
||||
service: ErpModuleService
|
||||
}
|
||||
```
|
||||
|
||||
Finally, add the module to the `modules` object in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js" highlights={[["7", "ERP_API_KEY", "The environment variable holding the API key of the ERP system."]]}
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
erpModuleService: {
|
||||
resolve: "./modules/erp",
|
||||
options: {
|
||||
apiKey: process.env.ERP_API_KEY
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Build Flows Across Systems
|
||||
|
||||
With Medusa’s workflows, build flows with steps that perform actions across systems.
|
||||
|
||||
For example, you can create a workflow that creates a product in integrated systems like ERPs, WMSs, and CMSs.
|
||||
|
||||
Workflows can be executed from anywhere. So, taking the workflow described above, you can listen to the `product.created` event using a subscriber and execute the workflow whenever the event is triggered.
|
||||
|
||||

|
||||
|
||||
Workflows guarantee data consistency through their compensation feature. You can provide a compensation function to steps that roll back the actions of that step. Then, if an error occurs in any step, the actions of previous steps are rolled back using their compensation function.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/workflows"
|
||||
title="Workflows"
|
||||
text="Learn more about Workflows and how to create them."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Create products across systems with workflows">
|
||||
|
||||
For example, create the following workflow in `src/workflows/create-product.ts`:
|
||||
|
||||
export const workflowHighlights = [
|
||||
["18", "createInErpStep", "A step that creates a product in the ERP system."],
|
||||
["21", "erpModuleService", "Resolve the ERP Module's main service."],
|
||||
["24", "productModuleService", "Resolve the Product Module's main service."],
|
||||
["28", "retrieve", "Retrieve the created product's data."],
|
||||
["30", "createProduct", "Create the product in the ERP system."],
|
||||
["34", "update", "Update the product in Medusa with the ID of the ERP product."],
|
||||
["43", "erpId", "Pass the ERP product's ID to the compensation function."],
|
||||
["44", "productId", "Pass the product's ID to the compensation function."],
|
||||
["46", "", "Define a compensation function that rolls back changes when an error occurs."],
|
||||
["53", "deleteProduct", "Undo creating the product in the ERP system by deleting it."],
|
||||
["54", "update", "Update the product in Medusa to remove the ERP product's ID."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/create-product.ts" highlights={workflowHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import ErpModuleService from "../modules/erp/service"
|
||||
|
||||
type WorkflowInput = {
|
||||
productId: string
|
||||
}
|
||||
|
||||
type WorkflowOutput = {
|
||||
erpProduct: any
|
||||
}
|
||||
|
||||
const createInErpStep = createStep(
|
||||
"create-in-erp",
|
||||
async ({ productId }: WorkflowInput, { container }) => {
|
||||
const erpModuleService: ErpModuleService = container.resolve(
|
||||
"erpModuleService"
|
||||
)
|
||||
const productModuleService: IProductModuleService = container
|
||||
.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const createdProductData = await productModuleService
|
||||
.retrieve(productId)
|
||||
|
||||
const erpProduct = await erpModuleService.createProduct(
|
||||
createdProductData
|
||||
)
|
||||
|
||||
await productModuleService.update(productId, {
|
||||
metadata: {
|
||||
erp_id: erpProduct.id
|
||||
}
|
||||
})
|
||||
|
||||
return new StepResponse({
|
||||
erpProduct
|
||||
}, {
|
||||
erpId: erpProduct.id,
|
||||
productId
|
||||
})
|
||||
}, async ({ erpId, productId }, { container}) => {
|
||||
const erpModuleService: ErpModuleService = container.resolve(
|
||||
"erpModuleService"
|
||||
)
|
||||
const productModuleService: IProductModuleService = container
|
||||
.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
await erpModuleService.deleteProduct(erpId)
|
||||
await productModuleService.update(productId, {
|
||||
metadata: {}
|
||||
})
|
||||
})
|
||||
|
||||
const createProductWorkflow = createWorkflow<
|
||||
WorkflowInput, WorkflowOutput
|
||||
>("create-product-in-systems", function (input) {
|
||||
const erpData = createInErpStep(input)
|
||||
|
||||
return erpData
|
||||
})
|
||||
|
||||
export default createProductWorkflow
|
||||
```
|
||||
|
||||
This workflow has one step that:
|
||||
|
||||
- Retrieves the product's data using the Product Module's main service.
|
||||
- Create the product in the ERP system using the ERP Module's main service.
|
||||
- Updates the product in Medusa by setting the ID of the ERP product in the product's `metadata` field.
|
||||
|
||||
The step also has a compensation function that rolls back changes when an error occurs. It deletes the product in the ERP system and removes the ID of the ERP product in the Medusa product.
|
||||
|
||||
Then, create the subscriber at `src/subscribers/create-product.ts`:
|
||||
|
||||
```ts title="src/subscribers/create-product.ts"
|
||||
import type {
|
||||
SubscriberConfig,
|
||||
SubscriberArgs,
|
||||
} from "@medusajs/medusa"
|
||||
import createProductWorkflow from "../workflows/create-product"
|
||||
|
||||
export default async function handleProductUpdate({
|
||||
data, container
|
||||
}: SubscriberArgs<{id: string}>) {
|
||||
createProductWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
productId: "data" in data ? data.data.id : data.id
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Created product across systems.")
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "product.created",
|
||||
}
|
||||
```
|
||||
|
||||
The subscriber executes the workflow whenever the `product.created` event is triggered, passing it the ID of the created product.
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## Create Webhook Listeners
|
||||
|
||||
You can provide webhook listeners that your external systems call when their data is updated. This lets you synchronize data between your systems. To create webhook listeners, create an API route.
|
||||
|
||||
For example, suppose an administrator changes the product data in the ERP system. The system then sends a request to the webhook you define in your Medusa application, which updates the product data in the application.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/api-routes"
|
||||
title="Create an API Route"
|
||||
text="Learn how to create an API Route in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
<Details summaryContent="Example: Create a webhook listener for ERP changes">
|
||||
|
||||
For example, create the file `src/api/webhooks/erp/update/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/webhooks/erp/update/route.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
IProductModuleService,
|
||||
UpdateProductDTO
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ModuleRegistrationName
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
type WebhookReq = {
|
||||
id: string
|
||||
updatedData: UpdateProductDTO
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<WebhookReq>,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { id, updatedData} = req.body
|
||||
|
||||
const productService: IProductModuleService = req.scope
|
||||
.resolve(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
await productService.update(id, updatedData)
|
||||
|
||||
res.status(200)
|
||||
}
|
||||
```
|
||||
|
||||
This creates a webhook listener for an ERP system. It receives the ID of a product and its updated data, assuming that’s how your ERP system sends the data.
|
||||
|
||||
Then, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { MiddlewaresConfig } from "@medusajs/medusa"
|
||||
import { raw } from "body-parser"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
method: ["POST", "PUT"],
|
||||
matcher: "/webhooks/*",
|
||||
bodyParser: false,
|
||||
middlewares: [raw({ type: "application/json" })],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This replaces the default JSON middleware with the raw middleware, which is useful for webhook routes.
|
||||
|
||||
</Details>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Multi-Region Store Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This recipe provides the general steps to build a multi-region store with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
A multi-regional store allows merchants to sell across different countries. This includes supporting each country's tax rules, currency, available shipping and payment options, and more.
|
||||
|
||||
Medusa comes with multi-regional support out of the box. This recipe explains how to benefit from Medusa's features to create a multi-regional store.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Region Setup
|
||||
|
||||
In Medusa, you can create unlimited regions in your store. Each region has configurations managed through the Medusa Admin or the Admin REST APIs.
|
||||
|
||||
### Currency
|
||||
|
||||
Merchants specify the currency of each region. Multiple regions can have the same currency, but a region has only one currency.
|
||||
|
||||
When customers view your products from a region, they see the prices in the region’s currency.
|
||||
|
||||
### Tax rates and providers
|
||||
|
||||
Merchants define a default tax rate and tax rates for specific conditions (for example, for a particular set of products) in a region. Each region also has a different tax provider that calculates the taxes. You can use Medusa's default tax provider or create a custom tax provider.
|
||||
|
||||
During checkout, the taxes are calculated based on the customer's region, tax configurations, and rates.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Taxes in Medusa Admin",
|
||||
text: "Manage taxes in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "!api!/admin#tax-regions_posttaxregions",
|
||||
title: "Manage Taxes Using Admin APIs",
|
||||
text: "Manage tax settings using the Admin APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
<Card
|
||||
href="/references/tax/provider"
|
||||
title="Create Tax Provider"
|
||||
text="Learn how to create a tax provider."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
|
||||
### Payment and Fulfillment Providers
|
||||
|
||||
Merchants choose which payment and fulfillment providers are available in each region. For example, one region can use Payment Provider A and B while another only uses Payment Provider B.
|
||||
|
||||
During checkout, customers only see the payment providers and shipping options configured for the region. This lets you give customers a localized experience that feels familiar and instills trust.
|
||||
|
||||
Medusa provides official provider modules for payment and fulfillment. You can also create custom provider modules.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Providers in Medusa Admin",
|
||||
text: "Learn how to manage providers in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "/integrations",
|
||||
title: "Integrations",
|
||||
text: "Check out available integrations, including payment provider modules.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Prices Per Region and Currency
|
||||
|
||||
Merchants sets the price of a product price per currency and region. This also applies to adding sales or overriding prices for specific conditions using the Pricing Module's features.
|
||||
|
||||
Using the tax-inclusive feature, merchants can specify prices including taxes per currency and region. Medusa then calculates the tax amount applied to a line item in the cart based on the region's tax configurations.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Setting Product Prices in Medusa Admin",
|
||||
text: "Learn how to set a product prices in Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Display Product Price in Storefront",
|
||||
text: "Learn how to display the correct product price in a storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Multi-Warehouse Support
|
||||
|
||||
Medusa's Inventory and Stock Location Modules provide multi-warehouse features that allow merchants to manage inventory across different locations. Merchants then control which location an item in an order is fulfilled, allowing them to keep a correct inventory count across locations and sales channels.
|
||||
|
||||
A multi-regional setup lets merchants manage their inventory through Medusa across the different regions they serve. Customers are always shown accurate inventory information based on the location associated with their sales channel.
|
||||
|
||||
{/* TODO add links */}
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Stock Locations",
|
||||
text: "Learn how to manage stock locations in the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Manage Inventory",
|
||||
text: "Learn how to manage inventory in the Medusa Admin.",
|
||||
startIcon: <UsersSolid />,
|
||||
showLinkIcon: false,
|
||||
badge: {
|
||||
variant: "blue",
|
||||
children: "Guide Soon"
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Multi-Lingual Setup
|
||||
|
||||
By integrating a third-party Content Management Systems (CMS), you benefit from rich content features including managing your content in multiple languages. This allows you to cater to customers’ different languages in the regions you serve.
|
||||
|
||||
To integrate a third-party system, create a custom module whose main service connects to that third-party service.
|
||||
|
||||
<Card
|
||||
href="!docs!/basics/modules-and-services"
|
||||
title="Create a Module"
|
||||
text="Learn how to create a module in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
+26
-41
@@ -7,7 +7,7 @@ export const metadata = {
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building an omnichannel commerce with Medusa.
|
||||
This recipe provides the general steps to build an omnichannel store with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -21,26 +21,17 @@ Medusa’s modular architecture facilitates building omnichannel commerce, as yo
|
||||
|
||||
## Create Multiple Storefronts with One Server
|
||||
|
||||
when creating an omnichannel commerce, you build multiple webshops or mobile apps. All these storefronts should provide your customers with a similar experience, allow them to browse products, and place orders.
|
||||
When creating an omnichannel commerce, you build multiple webshops or mobile apps. All these storefronts should provide your customers with a similar experience, allow them to browse products, and place orders.
|
||||
|
||||
Medusa's commerce features are available as REST APIs that storefronts can access to provide these features for customers. This separation also gives you freedom in choosing the tech stack of the storefronts you’re creating.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Medusa’s Architecture",
|
||||
text: "Learn about Medusa's architecture and its ecosystem.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Store REST APIs",
|
||||
text: "Check out available Store REST APIs in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
<Card
|
||||
href="!api!/store"
|
||||
title="Store REST APIs"
|
||||
text="Check out available Store REST APIs in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
@@ -50,22 +41,22 @@ Businesses are no longer bound to sell in their stores. They can reach their cus
|
||||
|
||||
One example is marketplaces like Amazon. Customers searching through Amazon to find products are inadvertently searching through many third-party stores connected to Amazon’s marketplace.
|
||||
|
||||
You can implement this example in Medusa with the Sales Channel Module and a custom plugin. Use the Sales Channel Module's features to set different product availability across sales channels. Then, in the custom plugin, integrate with Amazon’s seller program and use their APIs to push your products on Amazon.
|
||||
You can implement this example in Medusa with the Sales Channel Module and a custom module. Use the Sales Channel Module's features to set different product availability across sales channels. Then, in the custom module, integrate with Amazon’s seller program and use their APIs to push your products on Amazon.
|
||||
|
||||
Another channel that attracts customer sales is social media. You can create a plugin that integrates with social media apps to show your products and sell them to customers.
|
||||
Another channel that attracts customer sales is social media. You can create a custom module that integrates with social media apps to show your products and sell them to customers.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "/commerce-modules/sales-channels",
|
||||
title: "Sales Channels",
|
||||
text: "Learn about the Sales Channel Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Plugin",
|
||||
text: "Learn how to create a plugin.",
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create a Module",
|
||||
text: "Learn how to create a module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
@@ -75,33 +66,27 @@ Another channel that attracts customer sales is social media. You can create a p
|
||||
|
||||
## Optimize Customer Experience
|
||||
|
||||
With Medusa’s modular architecture, businesses focus on bringing their brands to life without restrictions.
|
||||
|
||||
Implement the customer journey and design for your storefronts that leads to the best customer experience. The Medusa application doesn't impose any restrictions on how the frontend is built.
|
||||
|
||||
Medusa’s architecture also makes it easier to integrate any third-party services necessary to provide a better customer experience with a plugin. Storefronts then use the features provided by that plugin if they're exposed by API routes.
|
||||
Medusa’s architecture also makes it easier to integrate any third-party services to provide a better customer experience:
|
||||
|
||||
For example, if you integrate a search plugin like Algolia in the Medusa application, the storefronts use it through the [Search Products API Route](https://docs.medusajs.com/api/store#products_postproductssearch). Another example is installing the Stripe payment plugin in the Medusa application and using it in your storefronts to make payments before placing an order.
|
||||
- Create a module that integrates the third-party service.
|
||||
- Expose the module's features in API routes.
|
||||
- Send requests to these API routes from your storefront.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Plugin",
|
||||
text: "Learn how to create a plugin.",
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create a Module",
|
||||
text: "Learn how to create a module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Plugins Library",
|
||||
text: "Check out available community plugins.",
|
||||
startIcon: <BoltSolid />,
|
||||
href: "!docs!/basics/api-routes",
|
||||
title: "Create an API Route",
|
||||
text: "Learn how to create an API route.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
+69
-63
@@ -7,7 +7,7 @@ export const metadata = {
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in using Medusa as an Order Management System (OMS).
|
||||
This recipe provides an overview of Medusa's features and how to use it as an Order Management System (OMS).
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -29,7 +29,7 @@ Sales channels in your commerce ecosystem must route their orders into the OMS.
|
||||
|
||||

|
||||
|
||||
Medusa's [Store REST APIs](https://docs.medusajs.com/api/store) let you integrate a checkout experience in any storefront. Alternatively, you can use Medusa's [Draft Order APIs](https://docs.medusajs.com/api/admin#draft-orders) to place an order without direct involvement from the customer, such as when placing an order through a POS.
|
||||
Medusa's [Store REST APIs](!api!/store) let you integrate a checkout experience in any storefront. Alternatively, you can use Medusa's [Draft Order APIs](!api!/admin#draft-orders) to place an order without direct involvement from the customer, such as when placing an order through a POS.
|
||||
|
||||
In addition, you can customize the Medusa application to accept orders through a third-party checkout system. This gives you more flexibility over adding orders to Medusa.
|
||||
|
||||
@@ -37,21 +37,21 @@ For example, you can support importing orders into Medusa through a custom API R
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "https://docs.medusajs.com/api/store#carts",
|
||||
href: "!api!/store#carts",
|
||||
title: "Store REST APIs",
|
||||
text: "Learn how to use the Store REST APIs to create an order.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/basics/api-routes",
|
||||
title: "Create API Route",
|
||||
text: "Learn how to create a custom API Route.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/basics/scheduled-jobs",
|
||||
title: "Create Scheduled Jobs",
|
||||
text: "Learn how to create a scheduled job.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
@@ -63,26 +63,32 @@ For example, you can support importing orders into Medusa through a custom API R
|
||||
|
||||
## Route Orders to Third-party Fulfillment Services
|
||||
|
||||
To integrate third-party fulfillment providers with the Medusa application, create a fulfillment service or use a fulfillment plugin.
|
||||
To integrate third-party fulfillment providers with the Medusa application, create a fulfillment provider module.
|
||||
|
||||
Medusa uses the fulfillment service whenever a fulfillment action is performed, such as when a fulfillment is created for items in an order. The service's methods interact with the third-party provider to handle the desired fulfillment actions.
|
||||
Medusa uses the Fulfillment Module whenever a fulfillment action is performed, such as when a fulfillment is created for items in an order. The module's main service's methods use the associated fulfillment provider module to handle the desired fulfillment actions.
|
||||
|
||||

|
||||
|
||||
In addition, create a subscriber that listen to fulfillment-related events, such as the `order.fulfillment_created` event, to perform actions in the third-party fulfillment provider.
|
||||
In addition, create a subscriber that listen to fulfillment-related events, such as the `order.fulfillment_created` event, to perform actions with the third-party fulfillment provider.
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Emitting of event `order.fulfillment_created` isn't implemented yet.
|
||||
|
||||
</Note>
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Fulfillment Provider",
|
||||
href: "/references/fulfillment/provider",
|
||||
title: "Create a Fulfillment Provider Module",
|
||||
text: "Learn how to create a fulfillment provider in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Events",
|
||||
text: "Learn about the event bus and how to listen to events.",
|
||||
href: "!docs!/basics/events-and-subscribers",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn about create a subscriber",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
@@ -92,22 +98,28 @@ In addition, create a subscriber that listen to fulfillment-related events, such
|
||||
|
||||
## Process Payment with Third-Party Providers
|
||||
|
||||
To integrate third-party payment providers with the Medusa application, create a payment processor or use payment plugins.
|
||||
To integrate third-party payment providers with the Medusa application, create a payment provider module.
|
||||
|
||||
In addition, create a subscriber that listen to payment-related events, such as the `order.payment_captured` event, to perform actions in the third-party payment provider.
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Emitting of event `order.payment_captured` isn't implemented yet.
|
||||
|
||||
</Note>
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Payment Plugins",
|
||||
text: "Check out available official payment plugins.",
|
||||
startIcon: <BoltSolid />,
|
||||
href: "/references/payment/provider",
|
||||
title: "Create a Payment Provider Module",
|
||||
text: "Learn how to create a payment provider module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Payment Processor",
|
||||
text: "Learn how to create a payment processor.",
|
||||
href: "!docs!/basics/events-and-subscribers",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn about create a subscriber",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
@@ -123,71 +135,65 @@ When an order is placed, the item's quantity is reserved from the stock location
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Multi-warehouse",
|
||||
text: "Learn about the Multi-warehouse architecture and features.",
|
||||
href: "/commerce-modules/inventory",
|
||||
title: "Inventory Module",
|
||||
text: "Learn about the Inventory Module's concepts and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Sales Channels",
|
||||
text: "Learn about the Sales Channel architecture and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Plugin",
|
||||
text: "Learn how to create a plugin.",
|
||||
href: "/commerce-modules/stock-location",
|
||||
title: "Stock Location Module",
|
||||
text: "Learn about the Stock Location Module's concepts and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
<Card
|
||||
href="/commerce-modules/sales-channel"
|
||||
title="Sales Channel Module"
|
||||
text="Learn about the Sales Channel Module's concepts and features."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Handle Returns, Exchanges, and Edits
|
||||
## Handle Returns, Exchanges, and Changes
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
All types of order changes are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In Medusa, items in an order can be returned or exchanged; these can be created by the merchant or requested by the customer. A merchant can also edit an order to add, update, or delete items.
|
||||
|
||||
When changes are made to an order by any of the mentioned actions, the changes are reflected on the order's totals and associated inventory. The integrated fulfillment and payment providers are used if fulfillment or payment actions are required, such as fulfilling exchanged items.
|
||||
When changes are made to an order by any of the mentioned actions, the changes are reflected on the order's totals and associated inventory. The integrated fulfillment and payment provider modules are used if fulfillment or payment actions are required, such as fulfilling exchanged items.
|
||||
|
||||
Medusa also emits events related to these actions, such as `order.return_requested`. So, you can create a subscriber that listens to these events and perform asynchronous actions, such as communicating with third-party services.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
<Note type="soon">
|
||||
|
||||
Emitting of event `order.return_requested` isn't implemented yet.
|
||||
|
||||
</Note>
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Returns",
|
||||
text: "Learn about the Order Return architecture and features.",
|
||||
href: "/commerce-modules/order/order-change",
|
||||
title: "Order Changes",
|
||||
text: "Learn about how to use order changes.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Exchanges (Swaps)",
|
||||
text: "Learn about the Exchange or Swap architecture and features.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Edits",
|
||||
text: "Learn about the Order Edit feature and how it works.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Event Reference",
|
||||
text: "Check out triggered events in Medusa and their payloads.",
|
||||
href: "!docs!/basics/events-and-subscribers",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn about create a subscriber",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -0,0 +1,139 @@
|
||||
import { AcademicCapSolid, NextJs } from "@medusajs/icons"
|
||||
import { LearningPath } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Personalized Products Recipe`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This recipe provides the general steps to build personalized products in Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Personalized products are products that customers can customize based on their need. For example, they can upload an image to print on a shirt or provide a message to include in a letter.
|
||||
|
||||
In Medusa, you create a custom module defining data models, implementing custom features, and creating relationships to other modules' data models.
|
||||
|
||||
You also have freedom in how you choose to implement the storefront, allowing you to build a unique experience around your products.
|
||||
|
||||
---
|
||||
|
||||
## Store Personalized Data
|
||||
|
||||
The Cart Module's `LineItem` data model has a `metadata` field that holds any custom data. You can pass the customer's customization in that field when adding a product to the cart.
|
||||
|
||||
For example, if you’re asking customers to enter a message to put in a letter they’re purchasing, use the `metadata` attribute of the `LineItem` data model to set the personalized information entered by the customer:
|
||||
|
||||
```bash
|
||||
curl -X POST '{backend_url}/store/carts/{id}/line-items' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"variant_id": "variant_123",
|
||||
"quantity": 1,
|
||||
"metadata": {
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
Two line items in the cart having different `metadata` attributes are not considered the same item. So, each line item is managed separately and can have its own quantity.
|
||||
|
||||
</Note>
|
||||
|
||||
In more complex cases, you can create a custom module that stores and manages the personalization data models. You can also create a relationship between these data models and the `LineItem` data model.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create a Module",
|
||||
text: "Learn how to create a module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/basics/data-models",
|
||||
title: "Create a Data Model",
|
||||
text: "Learn how to create a data model.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Build a Custom Storefront
|
||||
|
||||
Due to Medusa's modular architecture, there are no restrictions on the framework you use to build the storefront, or design and experience you provide customers. The storefront connects to the Medusa application using the Store API Routes.
|
||||
|
||||
You can build a unique experience around your products that focuses on the customer’s personalization capabilities.
|
||||
|
||||
Medusa provides a Next.js Starter storefront with basic ecommerce functionalities that can be customized. You can also build your own storefront and use Medusa’s client libraries or Store API Routes to communicate with the Medusa application.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "/nextjs-starter",
|
||||
title: "Next.js Starter",
|
||||
text: "Learn about the Next.js Starter and how to install it.",
|
||||
startIcon: <NextJs />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/storefront-development/tips",
|
||||
title: "Build Your Own Storefront",
|
||||
text: "Find tips on how to create a storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Pass Personalized Data to the Order
|
||||
|
||||
If you store the personalized data using a custom module:
|
||||
|
||||
- Create a custom API Route that handles saving the personalization data.
|
||||
- Call that API Route from the storefront after adding the item to the cart.
|
||||
- Listen to the `order.placed` event to attach the personalized data to the Order Module's `LineItem` data model.
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
The `order.placed` event is currently not emitted.
|
||||
|
||||
</Note>
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "!docs!/basics/api-routes",
|
||||
title: "Create API Route",
|
||||
text: "Learn how to create an API route.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/basics/events-and-subscribers",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Fulfill Personalized Products in Orders
|
||||
|
||||
To fulfill your personalized products with a third-party service or custom logic, create a fulfillment provider module.
|
||||
|
||||
The Fulfillment Module registers your fulfillment provider module to use it to fulfill orders.
|
||||
|
||||
<Card
|
||||
href="/references/fulfillment/provider"
|
||||
title="Create a Fulfillment Service"
|
||||
text="Learn how to create a fulfillment provider in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
+47
-66
@@ -7,7 +7,7 @@ export const metadata = {
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in using Medusa as an Point-of-Sale (POS).
|
||||
This recipe provides the general steps to build a Point of Sale (POS) system with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -29,39 +29,23 @@ When you build a POS system, you must choose which programming framework, langua
|
||||
|
||||
Medusa's modular architecture removes any restrictions you may have while making this choice. Any client or front end can connect to the Medusa application using its headless REST APIs.
|
||||
|
||||

|
||||

|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Medusa’s Architecture",
|
||||
text: "Learn about Medusa's architecture and its ecosystem.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "https://docs.medusajs.com/api/admin",
|
||||
title: "Admin REST APIs",
|
||||
text: "Check out available Admin REST APIs in Medusa.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "JavaScript Client",
|
||||
text: "Learn about the JavaScript client and to use it.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
<Card
|
||||
href="!api!/admin"
|
||||
title="Admin REST APIs"
|
||||
text="Check out available Admin REST APIs in Medusa."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Integrate a Barcode Scanner
|
||||
|
||||
POS systems make the checkout process smoother by integrating a barcode scanner. Merchants can scan a product by its barcode to check its details or add it to the customer's purchase.
|
||||
POS systems make the checkout process smoother by integrating a barcode scanner. Merchants scan a product by its barcode to check its details or add it to the customer's purchase.
|
||||
|
||||
Medusa’s `ProductVariant` data model includes the necessary attributes to implement this integration, mainly the `barcode` attribute. Other notable attributes include `ean`, `upc`, and `hs_code`, among others.
|
||||
The Product Module's `ProductVariant` data model has the fields to implement this integration, mainly the `barcode` attribute. Other notable fields include `ean`, `upc`, and `hs_code`, among others.
|
||||
|
||||
To search through product variants by their barcode, create a custom API Route and call it within your POS.
|
||||
|
||||
@@ -88,13 +72,14 @@ To search through product variants by their barcode, create a custom API Route a
|
||||
|
||||
Here’s an example of creating a custom API Route at `/store/pos/search-barcode` that searches product variants by a barcode:
|
||||
|
||||
```ts title="src/api/store/pos/search-barcode/route.ts"
|
||||
```ts title="src/api/store/pos/search-barcode/route.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
ProductVariantService,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
@@ -108,13 +93,15 @@ To search through product variants by their barcode, create a custom API Route a
|
||||
)
|
||||
}
|
||||
// get product service
|
||||
const productVariantService = req.scope.resolve<
|
||||
ProductVariantService
|
||||
>("productVariantService")
|
||||
const productModuleService: IProductModuleService =
|
||||
req.scope.resolve(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
// retrieve product variants by barcode
|
||||
const productVariants = await productVariantService
|
||||
.list({
|
||||
const productVariants = await productModuleService
|
||||
.listVariants({
|
||||
// @ts-ignore
|
||||
barcode,
|
||||
})
|
||||
|
||||
@@ -142,21 +129,21 @@ This also opens the door for other business opportunities, such as an endless ai
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "/commerce-modules/inventory",
|
||||
title: "Inventory Module",
|
||||
text: "Learn about the Inventory Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "/commerce-modules/stock-location",
|
||||
title: "Stock Location Module",
|
||||
text: "Learn about the Stock Location Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "/commerce-modules/sales-channel",
|
||||
title: "Sales Channel Module",
|
||||
text: "Learn about the Sales Channel Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
@@ -170,22 +157,29 @@ This also opens the door for other business opportunities, such as an endless ai
|
||||
|
||||
Using Medusa's Customer Module, you can retrieve a customer's details from the Medusa application and place an order on the POS under their account. The customer can then view their order details on their online profile as if they had placed the order online.
|
||||
|
||||
In addition, using Medusa's Discount Module, store operators can create discounts on the fly for customers using the POS system and apply them to their orders.
|
||||
In addition, using Medusa's Promotion Module, store operators can create promotions on the fly for customers using the POS system and apply them to their orders.
|
||||
|
||||
You can also extend Medusa to provide features for a better customer experience, such as a rewards system or loyalty points.
|
||||
You can also create custom modules to provide features for a better customer experience, such as a rewards system or loyalty points.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "/commerce-modules/customer",
|
||||
title: "Customer Module",
|
||||
text: "Learn about the Customer Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Discount Module",
|
||||
text: "Learn about the Discount Module.",
|
||||
href: "/commerce-modules/promotion",
|
||||
title: "Promotion Module",
|
||||
text: "Learn about the Promotion Module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create a Module",
|
||||
text: "Learn how to create a module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
@@ -195,38 +189,25 @@ You can also extend Medusa to provide features for a better customer experience,
|
||||
|
||||
## Accept Payment, Place Order, and Use RMA Features
|
||||
|
||||
Medusa's architecture allows you to integrate any third-party payment processor for your POS and online storefront. For example, you can integrate [Stripe Terminal](https://stripe.com/terminal) to accept in-store payments.
|
||||
Medusa's architecture allows you to integrate any third-party payment provider for your POS and online storefront. For example, you can integrate [Stripe Terminal](https://stripe.com/terminal) to accept in-store payments.
|
||||
|
||||
Once you accept the payment, place an order in the POS system using the [Draft Order APIs](https://docs.medusajs.com/api/admin#draft-orders). Draft orders provide similar features to an online checkout experience, including discounts, payment processing, and more.
|
||||
Once you accept the payment, place an order in the POS system using the [Draft Order APIs](!api!/admin#draft-orders). Draft orders provide similar features to an online checkout experience, including discounts, payment processing, and more.
|
||||
|
||||
Then, merchants can view all orders coming from different sales channels using the Medusa Admin. This keeps logistics and order handling consistent and allows businesses to provide return and exchange features to online and in-store customers.
|
||||
Then, merchants can view all orders coming from different sales channels using the Medusa Admin. This keeps logistics and order handling consistent between online and in-store customers.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Payment Processor",
|
||||
text: "Learn how to create a payment processor.",
|
||||
href: "/references/payment/provider",
|
||||
title: "Create a Payment Provider Module",
|
||||
text: "Learn how to create a payment provider module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Draft Order Module",
|
||||
text: "Learn about the Draft Order module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Order Module",
|
||||
text: "Learn about the Order Module.",
|
||||
href: "!api!/admin",
|
||||
title: "Admin REST APIs",
|
||||
text: "Check out available Admin REST APIs.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
+41
-74
@@ -7,7 +7,7 @@ export const metadata = {
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this recipe, you'll find resources to guide you in building subscription-based purchases with Medusa.
|
||||
This recipe provides the general steps to build subscription-based purchase with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -21,38 +21,34 @@ For example, a customer can purchase a book subscription box for a period of thr
|
||||
|
||||
</Note>
|
||||
|
||||
<LearningPath pathName="subscriptions" />
|
||||
|
||||
---
|
||||
|
||||
## Save Subscription Details in the Database
|
||||
## Save Subscription Details
|
||||
|
||||
Subscriptions have details related to the subscription interval, subscription period, and more.
|
||||
|
||||
Based on the approach you choose to implement the subscription logic (which is discussed in the next section), you may need to store different data in your Medusa application.
|
||||
To store the subscription details, create a data model in a module. The module's main service provides data management feature of the data model.
|
||||
|
||||
To store the subscription details in a new table in the database, create a data model. You can also extend an existing data model in Medusa's commerce modules, such as the `Order` data model, to add details related to the subscription.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
title: "Create a Data Model",
|
||||
text: "Learn how to create a data model in Medusa.",
|
||||
href: "!docs!/basics/modules-and-services",
|
||||
title: "Create a Module",
|
||||
text: "Learn how to create a module.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Extend a Data Model",
|
||||
text: "Learn how to extend a data model in Medusa.",
|
||||
href: "!docs!/basics/data-models",
|
||||
title: "Create a Data Model",
|
||||
text: "Learn how to create a data model.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Decide on Subscription Approach
|
||||
## Implement Subscription Approach
|
||||
|
||||
There are different ways to implement subscriptions in your Medusa application. This recipe provides two options: using Stripe subscriptions, or implementing subscriptions logic within the application, independent of a specific payment provider.
|
||||
|
||||
@@ -62,46 +58,44 @@ Stripe provides a [subscription payments feature](https://stripe.com/docs/billin
|
||||
|
||||
This approach allows you to delegate the complications of implementing the subscription logic to Stripe, but doesn't support using other payment providers.
|
||||
|
||||
Although Medusa provides a Stripe plugin, it doesn't handle subscriptions. You can either use that plugin to add the subscription feature on top of it, or create a custom Stripe Subscription payment provider.
|
||||
Although Medusa provides a Stripe provider module, it doesn't handle subscriptions. You can create a custom Stripe Subscription payment module.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "https://github.com/medusajs/medusa/tree/develop/packages/medusa-payment-stripe",
|
||||
title: "Use Medusa's Stripe Plugin",
|
||||
text: "Check out Medusa's stripe plugin to build subscription on top of it.",
|
||||
startIcon: <Github />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Payment Processor",
|
||||
text: "Create a Stripe Subscription payment processor from scratch.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
<Card
|
||||
href="/references/payment/provider"
|
||||
title="Create Payment Provider Module"
|
||||
text="Learn how to create a payment provider module."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
### Option 2: Implement Subscription Logic
|
||||
### Option 2: Custom Subscription Logic
|
||||
|
||||
By implementing the subscription logic within your application, you have full control over the subscription logic. You'll also be independent of payment providers, providing customers with more than one payment provider option.
|
||||
By implementing the subscription logic within your application, you have full control over the subscription logic. You'll also be independent of payment providers, providing customers with more than one payment provider.
|
||||
|
||||
Implementing the logic depends on your use case, but you'll mainly implement the following:
|
||||
|
||||
1. Create a subscriber that listens to the `order.placed` event to save the subscription details or perform other actions.
|
||||
2. Create a scheduled job that checks daily for subscriptions that need renewal.
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
- The `order.placed` event isn't emitted yet.
|
||||
- Scheduled jobs are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/basics/events-and-subscribers",
|
||||
title: "Create a Subscriber",
|
||||
text: "Learn how to create a subscriber in Medusa.",
|
||||
text: "Learn how to create a subscriber.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/basics/scheduled-jobs",
|
||||
title: "Create a Scheduled Job",
|
||||
text: "Learn how to create a scheduled job in Medusa.",
|
||||
text: "Learn how to create a scheduled job.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
@@ -111,31 +105,24 @@ Implementing the logic depends on your use case, but you'll mainly implement the
|
||||
|
||||
## Customize Admin
|
||||
|
||||
You can customize the admin to provide an interface to manage your custom features.
|
||||
You can extend the admin to provide an interface to manage your custom features.
|
||||
|
||||
You can extend the Medusa Admin plugin to add widgets to existing pages, add new pages, or add setting pages.
|
||||
Extend the Medusa Admin to add widgets to existing pages or add new pages.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/advanced-development/admin/widget",
|
||||
title: "Create Admin Widget",
|
||||
text: "Learn how to add widgets into existing admin pages.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/advanced-development/admin/ui-routes",
|
||||
title: "Create Admin UI Routes",
|
||||
text: "Learn how to add new pages to your Medusa Admin.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Create Admin Setting Page",
|
||||
text: "Learn how to add new page to the Medusa Admin settings.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
@@ -145,39 +132,19 @@ You can extend the Medusa Admin plugin to add widgets to existing pages, add new
|
||||
|
||||
Medusa provides a Next.js Starter. Since you've customized your Medusa project, you must either customize the existing Next.js Starter, or create a custom storefront.
|
||||
|
||||
<CardList itemsPerRow={2} items={[
|
||||
<CardList items={[
|
||||
{
|
||||
href: "#",
|
||||
href: "/nextjs-starter",
|
||||
title: "Option 1: Use Next.js Starter",
|
||||
text: "Install the Next.js Starter to customize it.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
href: "!docs!/storefront-development/tips",
|
||||
title: "Option 2: Build Custom Storefront",
|
||||
text: "Find useful resources to build your own storefront.",
|
||||
text: "Find tips to build your own storefront.",
|
||||
startIcon: <AcademicCapSolid />,
|
||||
showLinkIcon: false
|
||||
}
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Deploy Medusa Application
|
||||
|
||||
The Medusa Resources documentation includes deployment guides for a basic Medusa application. You can follow it to deploy your customized marketplace, as well.
|
||||
|
||||
<Card
|
||||
href="#"
|
||||
title="Deploy Medusa Application"
|
||||
text="Learn how to deploy your subscription-based application to different hosting providers."
|
||||
startIcon={<AcademicCapSolid />}
|
||||
showLinkIcon={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
|
||||
@@ -99,54 +99,6 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/_plugins/storage/spaces/page.mdx",
|
||||
"pathname": "/_plugins/storage/spaces"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/b2b/page.mdx",
|
||||
"pathname": "/_recipes/b2b"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/commerce-automation/page.mdx",
|
||||
"pathname": "/_recipes/commerce-automation"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/digital-products/page.mdx",
|
||||
"pathname": "/_recipes/digital-products"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/ecommerce/page.mdx",
|
||||
"pathname": "/_recipes/ecommerce"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/integrate-ecommerce-stack/page.mdx",
|
||||
"pathname": "/_recipes/integrate-ecommerce-stack"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/marketplace/page.mdx",
|
||||
"pathname": "/_recipes/marketplace"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/multi-region-store/page.mdx",
|
||||
"pathname": "/_recipes/multi-region-store"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/omnichannel/page.mdx",
|
||||
"pathname": "/_recipes/omnichannel"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/oms/page.mdx",
|
||||
"pathname": "/_recipes/oms"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/personalized-products/page.mdx",
|
||||
"pathname": "/_recipes/personalized-products"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/pos/page.mdx",
|
||||
"pathname": "/_recipes/pos"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/_recipes/subscriptions/page.mdx",
|
||||
"pathname": "/_recipes/subscriptions"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/admin-widget-injection-zones/page.mdx",
|
||||
"pathname": "/admin-widget-injection-zones"
|
||||
@@ -867,6 +819,54 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/page.mdx",
|
||||
"pathname": "/"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/b2b/page.mdx",
|
||||
"pathname": "/recipes/b2b"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/commerce-automation/page.mdx",
|
||||
"pathname": "/recipes/commerce-automation"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/digital-products/page.mdx",
|
||||
"pathname": "/recipes/digital-products"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/ecommerce/page.mdx",
|
||||
"pathname": "/recipes/ecommerce"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/integrate-ecommerce-stack/page.mdx",
|
||||
"pathname": "/recipes/integrate-ecommerce-stack"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/marketplace/page.mdx",
|
||||
"pathname": "/recipes/marketplace"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/multi-region-store/page.mdx",
|
||||
"pathname": "/recipes/multi-region-store"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/omnichannel/page.mdx",
|
||||
"pathname": "/recipes/omnichannel"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/oms/page.mdx",
|
||||
"pathname": "/recipes/oms"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/personalized-products/page.mdx",
|
||||
"pathname": "/recipes/personalized-products"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/pos/page.mdx",
|
||||
"pathname": "/recipes/pos"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/recipes/subscriptions/page.mdx",
|
||||
"pathname": "/recipes/subscriptions"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/references/[...slug]/page.tsx",
|
||||
"pathname": "/references/[...slug]"
|
||||
|
||||
@@ -5827,6 +5827,98 @@ export const generatedSidebar = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"title": "Recipes",
|
||||
"hasTitleStyling": true,
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/b2b",
|
||||
"title": "B2B",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/commerce-automation",
|
||||
"title": "Commerce Automation",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/digital-products",
|
||||
"title": "Digital Products",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/ecommerce",
|
||||
"title": "Ecommerce",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/integrate-ecommerce-stack",
|
||||
"title": "Integrate Ecommerce Stack",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/marketplace",
|
||||
"title": "Marketplace",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/multi-region-store",
|
||||
"title": "Multi-Region Store",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/omnichannel",
|
||||
"title": "Omnichannel Store",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/oms",
|
||||
"title": "OMS",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/personalized-products",
|
||||
"title": "Personalized Products",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/pos",
|
||||
"title": "POS",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"path": "/recipes/subscriptions",
|
||||
"title": "Subscriptions",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
|
||||
@@ -1279,6 +1279,61 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "Recipes",
|
||||
hasTitleStyling: true,
|
||||
children: [
|
||||
{
|
||||
path: "/recipes/b2b",
|
||||
title: "B2B",
|
||||
},
|
||||
{
|
||||
path: "/recipes/commerce-automation",
|
||||
title: "Commerce Automation",
|
||||
},
|
||||
{
|
||||
path: "/recipes/digital-products",
|
||||
title: "Digital Products",
|
||||
},
|
||||
{
|
||||
path: "/recipes/ecommerce",
|
||||
title: "Ecommerce",
|
||||
},
|
||||
{
|
||||
path: "/recipes/integrate-ecommerce-stack",
|
||||
title: "Integrate Ecommerce Stack",
|
||||
},
|
||||
{
|
||||
path: "/recipes/marketplace",
|
||||
title: "Marketplace",
|
||||
},
|
||||
{
|
||||
path: "/recipes/multi-region-store",
|
||||
title: "Multi-Region Store",
|
||||
},
|
||||
{
|
||||
path: "/recipes/omnichannel",
|
||||
title: "Omnichannel Store",
|
||||
},
|
||||
{
|
||||
path: "/recipes/oms",
|
||||
title: "OMS",
|
||||
},
|
||||
{
|
||||
path: "/recipes/personalized-products",
|
||||
title: "Personalized Products",
|
||||
},
|
||||
{
|
||||
path: "/recipes/pos",
|
||||
title: "POS",
|
||||
},
|
||||
{
|
||||
path: "/recipes/subscriptions",
|
||||
title: "Subscriptions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "SDKs and Tools",
|
||||
hasTitleStyling: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import { ArrowUpRightOnBox } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
import { Link } from "@/components"
|
||||
import { Badge, BadgeProps, Link } from "@/components"
|
||||
|
||||
export type CardProps = {
|
||||
startIcon?: React.ReactNode
|
||||
@@ -14,6 +14,7 @@ export type CardProps = {
|
||||
children?: React.ReactNode
|
||||
showLinkIcon?: boolean
|
||||
isExternal?: boolean
|
||||
badge?: BadgeProps
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
@@ -27,6 +28,7 @@ export const Card = ({
|
||||
children,
|
||||
showLinkIcon = true,
|
||||
isExternal = false,
|
||||
badge,
|
||||
}: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
@@ -43,8 +45,11 @@ export const Card = ({
|
||||
<div className="flex items-start gap-docs_1 justify-between flex-1">
|
||||
<div className={clsx("flex flex-col", contentClassName)}>
|
||||
{title && (
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
<span className={clsx(badge && "flex gap-docs_0.5")}>
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
{badge && <Badge {...badge} />}
|
||||
</span>
|
||||
)}
|
||||
{text && (
|
||||
|
||||
@@ -5,9 +5,10 @@ import clsx from "clsx"
|
||||
type CardListProps = {
|
||||
items: CardProps[]
|
||||
itemsPerRow?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CardList = ({ items, itemsPerRow }: CardListProps) => {
|
||||
export const CardList = ({ items, itemsPerRow, className }: CardListProps) => {
|
||||
if (!itemsPerRow) {
|
||||
// if length of items is even, set to `2`, else set to `3`
|
||||
itemsPerRow = items.length === 1 ? 0 : items.length % 2 === 0 ? 2 : 3
|
||||
@@ -18,7 +19,8 @@ export const CardList = ({ items, itemsPerRow }: CardListProps) => {
|
||||
"grid gap-x-docs_1 auto-rows-fr gap-y-docs_1",
|
||||
itemsPerRow === 1 && "grid-cols-1",
|
||||
itemsPerRow === 2 && "md:grid-cols-2 grid-cols-1",
|
||||
itemsPerRow === 3 && "lg:grid-cols-3 md:grid-col-2 grid-cols-1"
|
||||
itemsPerRow === 3 && "lg:grid-cols-3 md:grid-col-2 grid-cols-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{items.map((item, key) => (
|
||||
|
||||
Reference in New Issue
Block a user