From 88202761a5ee8b7e5c0ba3edf529e65957594cc1 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 29 Jan 2025 16:11:21 +0200 Subject: [PATCH] docs: update b2b recipe (#11213) --- www/apps/resources/app/recipes/b2b/page.mdx | 656 +++----------------- www/apps/resources/generated/edit-dates.mjs | 2 +- 2 files changed, 72 insertions(+), 586 deletions(-) diff --git a/www/apps/resources/app/recipes/b2b/page.mdx b/www/apps/resources/app/recipes/b2b/page.mdx index 66ae087b5640f..9726234407e92 100644 --- a/www/apps/resources/app/recipes/b2b/page.mdx +++ b/www/apps/resources/app/recipes/b2b/page.mdx @@ -1,14 +1,19 @@ import { AcademicCapSolid, UsersSolid } from "@medusajs/icons" -import { BetaBadge } from "docs-ui" export const metadata = { title: `B2B Recipe`, } -# {metadata.title} +# {metadata.title} This recipe provides the general steps to implement a B2B store with Medusa. + + +Medusa has a ready-to-use B2B starter that you install and use in [this GitHub repository](https://github.com/medusajs/b2b-starter-medusa). + + + ## 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 */} - - + --- @@ -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 - + ### Associate Key with Sales Channel -{/* TODO add links */} - - + --- @@ -117,49 +80,21 @@ You can create new products or add existing ones to the B2B sales channel using ### Create Products -{/* TODO add links */} - - + ### Add Products to Sales Channel -{/* TODO add links */} - - + --- @@ -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. -{/* */} - -{/*
- 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 { - 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 { - 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, - 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. - - - - You can alternatively pass a `customer_group_id` to use an existing customer group. - - - -
*/} +/> ## Add B2B Customers @@ -491,49 +137,21 @@ You can do that through the Medusa Admin or Admin REST APIs. ### Create Customers -{/* TODO add links */} - - + ### Assign Customers to Groups -{/* TODO add links */} - - + --- @@ -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 */} - - + --- @@ -594,126 +198,6 @@ The API route can check if the customer has any group with an associated company icon={AcademicCapSolid} /> -{/*
- - 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 - } - ``` - -
*/} - --- ## 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, }, ]} /> + + --- ## Customize Storefront diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 2f0b70aa76573..f475cebc92932 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -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",