docs: update b2b recipe (#11213)

This commit is contained in:
Shahed Nasser
2025-01-29 16:11:21 +02:00
committed by GitHub
parent a16eaad6c9
commit 88202761a5
2 changed files with 72 additions and 586 deletions

View File

@@ -1,14 +1,19 @@
import { AcademicCapSolid, UsersSolid } from "@medusajs/icons"
import { BetaBadge } from "docs-ui"
export const metadata = {
title: `B2B Recipe`,
}
# {metadata.title} <BetaBadge text="Soon" tooltipText="This recipe is a work in progress." />
# {metadata.title}
This recipe provides the general steps to implement a B2B store with Medusa.
<Note title="Tip">
Medusa has a ready-to-use B2B starter that you install and use in [this GitHub repository](https://github.com/medusajs/b2b-starter-medusa).
</Note>
## Overview
In a B2B store, you provide different types of customers with relevant pricing, products, shopping experience, and more.
@@ -29,26 +34,12 @@ Use sales channels to set product availability per channel. In this case, create
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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#sales-channels_postsaleschannels",
title: "Option 2: Using the REST APIs",
text: "Create the sales channel using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#sales-channels_postsaleschannels"
title="Create Sales Channel"
text="Create the sales channel using the REST APIs."
icon={AcademicCapSolid}
/>
---
@@ -63,51 +54,23 @@ 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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#api-keys_postapikeys",
title: "Option 2: Using the REST APIs",
text: "Create the publishable API key using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#api-keys_postapikeys"
title="Create Publishable API Key"
text="Create the publishable API key using the REST APIs."
icon={AcademicCapSolid}
/>
### 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.",
icon: UsersSolid,
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.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#api-keys_postapikeysidsaleschannels"
title="Create Publishable API Key"
text="Create the publishable API key using the REST APIs."
icon={AcademicCapSolid}
/>
---
@@ -117,49 +80,21 @@ You can create new products or add existing ones to the B2B sales channel using
### Create Products
{/* TODO add links */}
<CardList items={[
{
href: "#",
title: "Using Medusa Admin",
text: "Create products using the Medusa Admin.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#products_postproducts",
title: "Using REST APIs",
text: "Create products using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#products_postproducts"
title="Create Products"
text="Create products using the REST APIs."
icon={AcademicCapSolid}
/>
### 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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#sales-channels_postsaleschannelsidproductsbatchadd",
title: "Using REST APIs",
text: "Add the products to the B2B sales channel using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#sales-channels_postsaleschannelsidproductsbatchadd"
title="Add to Sales Channel"
text="Add the products to the B2B sales channel using the REST APIs."
icon={AcademicCapSolid}
/>
---
@@ -169,7 +104,7 @@ Use customer groups to organize your customers into different groups. Then, you
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.
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, which is defined in the Customer Module. Any customer belonging to that group also belongs to the company, meaning they're a B2B customer.
<CardList items={[
{
@@ -186,302 +121,13 @@ You can create a B2B module that adds necessary data models to represent a B2B c
},
]} />
{/* <Card
href="!docs!/learn/fundamentals/modules/module-relationships"
title="Create Module Relationships"
text="Learn how to create a relationship between modules."
<Card
href="!docs!/learn/fundamentals/module-links"
title="Create Module Links"
text="Learn how to create links between modules."
icon={AcademicCapSolid}
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={[["8", "", "The property will be used to create a relationship to customer groups."]]}
import { model } from "@medusajs/framework/utils"
const Company = model.define("company", {
id: model.id().primaryKey(),
name: model.text(),
city: model.text(),
country_code: model.text(),
customer_group_id: model.text().nullable(),
})
export default Company
```
This creates a `Company` data model with some relevant properties. Most importantly, it has a `customer_group_id` property. 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.
Then, create the module's main service at `src/modules/b2b/service.ts` with the following content:
```ts title="src/modules/b2b/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Company from "./models/company"
class B2bModuleService extends MedusaService({
Company,
}){
// TODO add custom methods
}
export default B2bModuleService
```
This creates a `B2bModuleService` that extends the service factory, which generates data-management functionalities for the `Company` data model.
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"
import { Module } from "@medusajs/framework/utils"
export default Module("b2b", {
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 db:migrate
```
### 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/types/b2b/index.ts` with some helper types:
```ts title="src/types/b2b/index.ts"
import { CustomerGroupDTO } from "@medusajs/framework/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
}
```
Then, create 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/framework/workflows-sdk"
import {
createCustomerGroupsWorkflow,
} from "@medusajs/medusa/core-flows"
import { CreateCustomerGroupDTO } from "@medusajs/framework/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.createCompany(
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` property.
2. The second one creates the company.
Finally, 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!/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
@@ -491,49 +137,21 @@ 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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#customers_postcustomers",
title: "Option 2: Using the REST APIs",
text: "Create the customers using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#customers_postcustomers"
title="Create Customers"
text="Create the customers using the REST APIs."
icon={AcademicCapSolid}
/>
### 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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!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.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#customer-groups_postcustomergroupsidcustomersbatch"
title="Assign Customers to Groups"
text="Assign the customers to the B2B customer group using the REST APIs."
icon={AcademicCapSolid}
/>
---
@@ -543,26 +161,12 @@ Use price lists to set different prices for each B2B customer group, among other
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.",
icon: UsersSolid,
badge: {
variant: "blue",
children: "Guide Soon"
}
},
{
href: "!api!/admin#price-lists_postpricelists",
title: "Using REST APIs",
text: "Create the price list using the REST APIs.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!api!/admin#price-lists_postpricelists"
title="Create Price List"
text="Create the price list using the REST APIs."
icon={AcademicCapSolid}
/>
---
@@ -594,126 +198,6 @@ The API route can check if the customer has any group with an associated company
icon={AcademicCapSolid}
/>
{/* <Details summaryContent="Example">
For example, create the API route `src/api/store/b2b/check-customer/route.ts` with the following content:
export const checkCustomerHighlights = [
["19", "retrieveCustomer", "Retrieve the customer along with its groups."],
["26", "listCompanies", "List the companies that have a customer group ID matching any of the customer's group IDs."],
["31", "", "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 { Modules } from "@medusajs/framework/utils"
import { ICustomerModuleService } from "@medusajs/framework/types"
import B2bModuleService from "../../../../modules/b2b/service"
export async function GET(
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) {
const customerModuleService: ICustomerModuleService = req
.scope.resolve(Modules.CUSTOMER)
const b2bModuleService: B2bModuleService = req.scope.resolve(
"b2bModuleService"
)
const customer = await customerModuleService.retrieveCustomer(
req.auth_context.actor_id,
{
relations: ["groups"],
}
)
const companies = await b2bModuleService.listCompanies({
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
@@ -726,23 +210,25 @@ The Medusa Admin plugin can be extended to add widgets, new pages, and setting p
{
href: "!docs!/learn/fundamentals/admin/widgets",
title: "Create Admin Widget",
text: "Learn how to add widgets into existing admin pages.",
text: "Add widgets into existing admin pages.",
icon: AcademicCapSolid,
},
{
href: "!docs!/learn/fundamentals/admin/ui-routes",
title: "Create Admin UI Routes",
text: "Learn how to add new pages to your Medusa Admin.",
icon: AcademicCapSolid,
},
{
href: "!docs!/learn/fundamentals/admin/ui-routes#create-settings-page",
title: "Create Admin Setting Page",
text: "Learn how to add new page to the Medusa Admin settings.",
text: "Add new pages to your Medusa Admin.",
icon: AcademicCapSolid,
},
]} />
<Card
href="!docs!/learn/fundamentals/admin/ui-routes#create-settings-page"
title="Create Admin Setting Page"
text="Add new page to the Medusa Admin settings."
icon={AcademicCapSolid}
className="mt-1"
/>
---
## Customize Storefront

View File

@@ -110,7 +110,7 @@ export const generatedEditDates = {
"app/medusa-container-resources/page.mdx": "2025-01-06T11:19:35.623Z",
"app/medusa-workflows-reference/page.mdx": "2025-01-20T08:21:29.962Z",
"app/nextjs-starter/page.mdx": "2025-01-06T12:19:31.143Z",
"app/recipes/b2b/page.mdx": "2024-10-03T13:07:44.153Z",
"app/recipes/b2b/page.mdx": "2025-01-29T11:35:23.247Z",
"app/recipes/commerce-automation/page.mdx": "2024-10-16T08:52:01.585Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-01-13T11:31:35.362Z",
"app/recipes/digital-products/page.mdx": "2025-01-06T11:19:35.623Z",