docs: updates to use DML and other changes (#7834)

- Change existing data model guides and add new ones for DML
- Change module's docs around service factory + remove guides that are now necessary
- Hide/remove all mentions of module relationships, or label them as coming soon.
- Change all data model creation snippets to use DML
- use `property` instead of `field` when referring to a data model's properties.
- Fix all snippets in commerce module guides to use new method suffix (no more main model methods)
- Rework recipes, removing/hiding a lot of sections as a lot of recipes are incomplete with the current state of DML.


### Other changes

- Highlight fixes in some guides
- Remove feature flags guide
- Fix code block styles when there are no line numbers.

### Upcoming changes in other PRs

- Re-generate commerce module references (for the updates in the method names)
- Ensure that the data model references are generated correctly for models using DML.
- (probably at a very later point) revisit recipes
This commit is contained in:
Shahed Nasser
2024-06-26 07:55:59 +00:00
committed by GitHub
parent 62dacdda75
commit 0462cc5acf
126 changed files with 1808 additions and 14242 deletions
+90 -147
View File
@@ -8,6 +8,12 @@ export const metadata = {
This recipe provides the general steps to implement a B2B store with Medusa.
<Note type="soon" title="In Development">
This recipe is a work in progress, as some features are not ready yet in Medusa V2.
</Note>
## Overview
In a B2B store, you provide different types of customers with relevant pricing, products, shopping experience, and more.
@@ -180,6 +186,12 @@ This is useful in B2B sales, as you often negotiate special prices with each cus
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.
<Note type="soon">
Module Relationships is coming soon.
</Note>
<CardList items={[
{
href: "!docs!/basics/modules-and-services",
@@ -197,50 +209,37 @@ You can create a B2B module that adds necessary data models to represent a B2B c
},
]} />
<Card
{/* <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">
{/* <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"
```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/utils"
@Entity()
export class Company extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id!: string
const Company = model.define("company", {
id: model.id(),
name: model.text(),
city: model.text(),
country_code: model.text(),
customer_group_id: model.text().nullable(),
})
@Property({ columnType: "text" })
name: string
@Property({ columnType: "text" })
city: string
@Property({ columnType: "text" })
country_code: string
@Property({ columnType: "text" })
customer_group_id?: string
}
export default Company
```
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.
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:
@@ -261,106 +260,22 @@ You can create a B2B module that adds necessary data models to represent a B2B c
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."]
]
Then, create the module's main service at `src/modules/b2b/service.ts` with the following content:
```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>
```ts title="src/modules/b2b/service.ts"
import { MedusaService } from "@medusajs/utils"
import Company from "./models/company"
class B2bModuleService extends MedusaService({
Company,
}){
// TODO add custom methods
}
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.
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:
@@ -398,7 +313,30 @@ export const mainServiceHighlights = [
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:
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/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."],
@@ -419,7 +357,7 @@ export const workflowHighlights = [
import { CreateCustomerGroupDTO } from "@medusajs/types"
import { CompanyDTO, CreateCompanyDTO } from "../types/b2b"
import B2bModuleService from "../modules/b2b/service"
export type CreateCompanyWorkflowInput = CreateCompanyDTO & {
customer_group?: CreateCustomerGroupDTO
}
@@ -471,7 +409,7 @@ export const workflowHighlights = [
"b2bModuleService"
)
const company = await b2bModuleService.create(
const company = await b2bModuleService.createCompany(
companyData
)
@@ -498,10 +436,10 @@ export const workflowHighlights = [
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.
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.
Then, create the file `src/api/admin/b2b/company/route.ts` with the following content:
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 {
@@ -566,7 +504,7 @@ export const workflowHighlights = [
</Note>
</Details>
</Details> */}
## Add B2B Customers
@@ -687,18 +625,21 @@ The API route can check if the customer has any group with an associated company
showLinkIcon={false}
/>
<Details summaryContent="Example">
{/* <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."]
["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 type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
import B2bModuleService from "../../../../modules/b2b/service"
@@ -713,11 +654,14 @@ export const checkCustomerHighlights = [
"b2bModuleService"
)
const customer = await customerModuleService.retrieve(req.auth.actor_id, {
relations: ["groups"],
})
const customer = await customerModuleService.retrieveCustomer(
req.auth_context.actor_id,
{
relations: ["groups"],
}
)
const companies = await b2bModuleService.list({
const companies = await b2bModuleService.listCompanies({
customer_group_id: customer.groups.map((group) => group.id),
})
@@ -736,7 +680,10 @@ export const checkCustomerHighlights = [
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"
import {
MiddlewaresConfig,
authenticate,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
@@ -796,16 +743,12 @@ export const checkCustomerHighlights = [
}
```
</Details>
</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.
@@ -851,9 +794,9 @@ Use the publishable API key you associated with your B2B sales channel in the st
showLinkIcon: false
},
{
href: "!docs!/storefront-development/tips",
title: "Storefront Tips",
text: "Find tips on developing a custom storefront.",
href: "/storefront-development",
title: "Storefront Development",
text: "Find guides for your storefront development.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
@@ -9,6 +9,12 @@ export const metadata = {
This recipe provides the general steps to implement a B2B store with Medusa.
<Note type="soon" title="In Development">
This recipe is a work in progress, as some features are not ready yet in Medusa V2.
</Note>
## 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.
@@ -49,13 +55,6 @@ The `inventory-item.updated` event is currently not emitted.
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "!docs!/advanced-development/modules/module-relationships",
title: "Module Relationships",
text: "Learn how to create module relationships.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
]} />
<CardList items={[
@@ -86,37 +85,27 @@ The `inventory-item.updated` event is currently not emitted.
Then, create the file `src/modules/restock-notification/models/restock-notification.ts` with the following content:
export const restockModelHighlights = [
["14", "email", "The email of the customer to send the notification to when the item is restocked."],
["17", "variant_id", "The ID of the variant the customer is subscribed to."],
["20", "sales_channel_id", "The ID of the sales channel the customer is viewing the product variant from."]
["5", "email", "The email of the customer to send the notification to when the item is restocked."],
["6", "variant_id", "The ID of the variant the customer is subscribed to."],
["7", "sales_channel_id", "The ID of the sales channel the customer is viewing the product variant from."]
]
```ts title="src/modules/restock-notification/models/restock-notification.ts" highlights={restockModelHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { BaseEntity } from "@medusajs/utils"
import {
Entity,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity()
export class RestockNotification extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id!: string
```ts title="src/modules/restock-notification/models/restock-notification.ts" highlights={restockModelHighlights}
import { model } from "@medusajs/utils"
@Property({ columnType: "text" })
email: string
const RestockNotification = model.define("restock_notification", {
id: model.id(),
email: model.text(),
variant_id: model.text(),
sales_channel_id: model.text(),
})
@Property({ columnType: "text" })
variant_id: string
@Property({ columnType: "text" })
sales_channel_id: string
}
export default RestockNotification
```
This creates a `RestockNotification` data model with the following fields:
This creates a `RestockNotification` data model with the following properties:
- `id`: An automatically generated ID.
- `email`: The email of the customer to send the notification to when the item is restocked.
- `variant_id`: The ID of the variant the customer is subscribed to. This will later be used to form a relationship with the `ProductVariant` data model of the Product Module.
- `sales_channel_id`: The ID of the sales channel the customer is viewing the product variant from. This will later be used to form a relationship with the `SalesChannel` data model of the Sales Channel Module.
@@ -142,117 +131,23 @@ export const restockModelHighlights = [
```
You'll run the migration to reflect the changes on the database after finishing the module's definition.
Now, create the file `src/types/restock-notification/index.ts` that holds common types to be used in the module's main service:
```ts title="src/types/restock-notification/index.ts"
import { ProductVariantDTO, SalesChannelDTO } from "@medusajs/types"
export type RestockNotificationDTO = {
id: string
email: string
variant_id: string
sales_channel_id: string
variant?: ProductVariantDTO
sales_channel?: SalesChannelDTO
}
export type CreateRestockNotificationDTO = {
email: string
variant_id: string
sales_channel_id: string
}
```
Then, create the module's main service at `src/modules/restock-notification/service.ts` with the following content:
export const restockModuleService = [
["17", "abstractModuleServiceFactory", "Extend the service factory to have basic data management features."],
["43", "", "Define a relationship to the `ProductVariant` data model of the Product Module."],
["52", "", "Define a relationship to the `SalesChannel` data model of the Sales Channel Module."],
["61", "create", "Implement the method to create a restock notification."]
]
```ts title="src/modules/restock-notification/service.ts"
import { MedusaService } from "@medusajs/utils"
import RestockNotification from "./models/restock-notification"
```ts title="src/modules/restock-notification/service.ts" highlights={restockModuleService} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { Modules, ModulesSdkUtils } from "@medusajs/utils"
import { RestockNotification } from "./models/restock-notification"
import { ModuleJoinerConfig, ModulesSdkTypes } from "@medusajs/types"
import { CreateRestockNotificationDTO, RestockNotificationDTO } from "../../types/restock-notification"
type InjectedDependencies = {
restockNotificationService: ModulesSdkTypes.InternalModuleService<any>
class RestockNotificationModuleService extends MedusaService({
RestockNotification,
}){
// TODO add custom methods
}
type AllModelDTOs = {
RestockNotification: {
dto: RestockNotificationDTO
}
}
class RestockNotificationModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
RestockNotificationDTO,
AllModelDTOs
>(RestockNotification, []) {
restockNotificationService_: ModulesSdkTypes.InternalModuleService<RestockNotification>
constructor({ restockNotificationService }: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.restockNotificationService_ = restockNotificationService
}
__joinerConfig(): ModuleJoinerConfig {
return {
serviceName: "restockNotificationModuleService",
alias: [
{
name: "restock_notification",
args: {
entity: RestockNotification.name,
},
},
],
relationships: [
{
serviceName: Modules.PRODUCT,
alias: "variant",
primaryKey: "id",
foreignKey: "variant_id",
args: {
methodSuffix: "Variants",
},
},
{
serviceName: Modules.SALES_CHANNEL,
alias: "sales_channel",
primaryKey: "id",
foreignKey: "sales_channel_id",
},
],
}
}
async create(data: CreateRestockNotificationDTO): Promise<RestockNotificationDTO> {
const restockNotification = await this.restockNotificationService_.create(
data
)
return restockNotification
}
}
export default RestockNotificationModuleService
```
In the module's main service, you:
- Extend the service factory to have basic data management features.
- Define a relationship to the `ProductVariant` data model of the Product Module.
- Define a relationship to the `SalesChannel` data model of the Sales Channel Module.
- Implement the `create` method to create a restock notification.
The module's main service extends the service factory which generates basic management features for the `RestockNotification` data model.
Next, create the module's definition file `src/modules/restock-notification/index.ts` with the following content:
@@ -290,18 +185,19 @@ export const restockModuleService = [
Create the file `src/api/store/restock-notification/route.ts` with the following content:
```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import RestockNotificationModuleService
from "../../../modules/restock-notification/service"
import {
CreateRestockNotificationDTO,
} from "../../../types/restock-notification"
type RestockNotificationReq = CreateRestockNotificationDTO
type RestockNotificationReq = {
email: string
variant_id: string
sales_channel_id: string
}
export async function POST(
req: MedusaRequest<RestockNotificationReq>,
@@ -312,7 +208,7 @@ export const restockModuleService = [
"restockNotificationModuleService"
)
await restockNotificationModuleService.create(
await restockNotificationModuleService.createRestockNotifications(
req.body
)
@@ -326,13 +222,15 @@ export const restockModuleService = [
### Create Inventory Item Updated Subscriber
To handle the sending of the restock notifications, create a subscriber that listens to the `inventory-item.updated` event, then sends a notification using the Notification Module to subscribed emails.
<Note type="soon">
The `inventory-item.updated` event is currently not emitted. The subscriber will only work once the event is emitted.
The `inventory-item.updated` event is currently not emitted.
</Note>
To handle the sending of the restock notifications, create the file `src/subscribers/inventory-item-update.ts` with the following content:
{/* To handle the sending of the restock notifications, create the file `src/subscribers/inventory-item-update.ts` with the following content: */}
export const subscriberHighlights = [
["48", "inventoryVariantLinkService", "Retrieve an instance of the link service for the product-variant-inventory-item link module."],
@@ -343,19 +241,19 @@ export const subscriberHighlights = [
["93", "salesChannelLocations", "Retrieve the stock locations linked to the restock notification's sales channel."],
["107", "availableQuantity", "Retrieve the available quantity of the variant in the retrieved stock locations."],
["116", "continue", "Only send the notification if the available quantity is greater than `0`"],
["119", "create", "Send the notification to the customer using the Notification Module."],
["119", "createNotifications", "Send the notification to the customer using the Notification Module."],
["122", '"test_template"', "Replace with the actual template used for sending the email."],
["123", "data", "The data to send along to the third-party service sending the notification."],
["131", "delete", "Delete the restock notification to not send the notification again."]
["131", "deleteRestockNotifications", "Delete the restock notification to not send the notification again."]
]
```ts title="src/subscribers/inventory-item-update.ts" highlights={subscriberHighlights} collapsibleLines="1-23" expandButtonLabel="Show Imports"
{/* ```ts title="src/subscribers/inventory-item-update.ts" highlights={subscriberHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa"
import {
IInventoryServiceNext,
IInventoryService,
INotificationModuleService,
RemoteQueryFunction,
} from "@medusajs/types"
@@ -364,9 +262,6 @@ export const subscriberHighlights = [
Modules,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
RestockNotificationDTO,
} from "../types/restock-notification"
import {
RemoteLink,
} from "@medusajs/modules-sdk"
@@ -388,7 +283,7 @@ export const subscriberHighlights = [
RestockNotificationModuleService = container.resolve(
"restockNotificationModuleService"
)
const inventoryModuleService: IInventoryServiceNext =
const inventoryModuleService: IInventoryService =
container.resolve(Modules.INVENTORY)
const notificationModuleService: INotificationModuleService =
container.resolve(
@@ -430,7 +325,7 @@ export const subscriberHighlights = [
},
})
const restockNotifications: RestockNotificationDTO[] =
const restockNotifications =
await remoteQuery(restockQuery)
const salesChannelLocationService = remoteLink.getLinkModule(
@@ -468,7 +363,7 @@ export const subscriberHighlights = [
continue
}
notificationModuleService.create({
notificationModuleService.createNotifications({
to: restockNotification.email,
channel: "email",
template: "test_template",
@@ -480,7 +375,8 @@ export const subscriberHighlights = [
})
// delete the restock notification
await restockNotificationModuleService.delete(restockNotification.id)
await restockNotificationModuleService
.deleteRestockNotifications(restockNotification.id)
}
}
@@ -497,7 +393,7 @@ export const subscriberHighlights = [
- Retrieve the restock notifications of those variants.
- For each restock notification, you:
- Retrieve its quantity based on the stock location associated with the restock notification's sales channel.
- If the quantity is greater than `0`, you send a notification using the Notification Module and delete the restock notification.
- If the quantity is greater than `0`, you send a notification using the Notification Module and delete the restock notification. */}
</Details>
@@ -509,11 +405,11 @@ Customer support is essential to build a store's brand and customer loyalty. Thi
You can use the Notification Module to send notifications when an action is triggered, such as when a customer or their order is updated.
<Note title="Tip">
{/* <Note title="Tip">
The [Events reference](../../events-reference/page.mdx) shows an extensive list of events triggered by the each commerce module.
</Note>
</Note> */}
Medusa also provides Notification Provider Modules that integrate with third-party services, such as SendGrid.
@@ -564,28 +460,39 @@ Scheduled jobs are coming soon.
For example, create the file `src/workflows/sync-products.ts` with the following content:
export const syncProductsWorkflowHighlight = [
["9", "retrieveStoreStep", "A step that retrieves the store by its ID."],
["25", "retrieveProductsToUpdateStep", "A step that retrieves the products to update based on a last update date."],
["45", "syncProductsStep", "A step to sync the product with a third-party service."],
["48", "productSyncModuleService", "Assuming this is a custom module's main service that provides connection to the third-party service."],
["52", "productsBeforeSync", "Retrieve old product data from third-party service for compensation function."],
["57", "sync", "Sync the product data in the third-party service."],
["61", "", "Pass products data before sync to compensation function."],
["64", "", "A compensation function to revert the sync when an error occurs."],
["70", "sync", "Revert the product's data in the third-party service to its old data before the synchronization."],
["79", "updateStoreLastSyncStep", "A step to update the `last_sync_data` of the store."],
["85", "prevLastSyncDate", "Retrieve the previous value of `last_sync_date` to pass it to compensation function."],
["87", "update", "Update the `last_sync_date` of the store."],
["95", "", "Pass previous last sync date to compensation function."],
["98", "", "A compensation function to revert the update of `last_sync_data` if an error occurs."],
["114", "syncProductsWorkflow", "Define the workflow that uses the above steps."]
["20", "retrieveStoreStep", "A step that retrieves the store by its ID."],
["36", "retrieveProductsToUpdateStep", "A step that retrieves the products to update based on a last update date."],
["56", "syncProductsStep", "A step to sync the product with a third-party service."],
["59", "productSyncModuleService", "Assuming this is a custom module's main service that provides connection to the third-party service."],
["63", "productsBeforeSync", "Retrieve old product data from third-party service for compensation function."],
["68", "sync", "Sync the product data in the third-party service."],
["72", "", "Pass products data before sync to compensation function."],
["75", "", "A compensation function to revert the sync when an error occurs."],
["81", "sync", "Revert the product's data in the third-party service to its old data before the synchronization."],
["90", "updateStoreLastSyncStep", "A step to update the `last_sync_data` of the store."],
["96", "prevLastSyncDate", "Retrieve the previous value of `last_sync_date` to pass it to compensation function."],
["98", "update", "Update the `last_sync_date` of the store."],
["106", "", "Pass previous last sync date to compensation function."],
["109", "", "A compensation function to revert the update of `last_sync_data` if an error occurs."],
["125", "syncProductsWorkflow", "Define the workflow that uses the above steps."]
]
```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight}
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService, IStoreModuleService, ProductDTO, StoreDTO } from "@medusajs/types"
import { StepResponse, createStep, createWorkflow } from "@medusajs/workflows-sdk"
```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight} collapsibleLines="1-16" expandButtonLabel="Show Imports"
import {
ModuleRegistrationName
} from "@medusajs/modules-sdk"
import {
IProductModuleService,
IStoreModuleService,
ProductDTO,
StoreDTO
} from "@medusajs/types"
import {
StepResponse,
createStep,
createWorkflow
} from "@medusajs/workflows-sdk"
type RetrieveStoreStepInput = {
id: string
}
@@ -596,7 +503,7 @@ export const syncProductsWorkflowHighlight = [
const storeModuleService: IStoreModuleService =
container.resolve(ModuleRegistrationName.STORE)
const store = await storeModuleService.retrieve(id)
const store = await storeModuleService.retrieveStore(id)
return new StepResponse({ store })
}
@@ -612,7 +519,7 @@ export const syncProductsWorkflowHighlight = [
const productModuleService: IProductModuleService =
container.resolve(ModuleRegistrationName.PRODUCT)
const products = await productModuleService.list({
const products = await productModuleService.listProducts({
updated_at: {
$gt: last_sync_date,
},
@@ -633,7 +540,7 @@ export const syncProductsWorkflowHighlight = [
"productSyncModuleService"
)
const productsBeforeSync = await productSyncModuleService.list({
const productsBeforeSync = await productSyncModuleService.listProductSyncs({
id: products.map((product) => product.id),
})
@@ -668,7 +575,7 @@ export const syncProductsWorkflowHighlight = [
const prevLastSyncDate = store.metadata.last_sync_date
await storeModuleService.update(store.id, {
await storeModuleService.updateStores(store.id, {
metadata: {
last_sync_date: (new Date()).toString(),
},
@@ -683,7 +590,7 @@ export const syncProductsWorkflowHighlight = [
const storeModuleService: IStoreModuleService =
container.resolve(ModuleRegistrationName.STORE)
await storeModuleService.update(id, {
await storeModuleService.updateStores(id, {
metadata: {
last_sync_date,
},
@@ -722,7 +629,7 @@ export const syncProductsWorkflowHighlight = [
This creates a workflow with the following steps:
1. Retrieve the store by its ID.
2. Retrieve products to update based on the last date and time the products were synced. The last sync date is retrieved from the store's `metadata` field.
2. Retrieve products to update based on the last date and time the products were synced. The last sync date is retrieved from the store's `metadata` property.
3. Sync the retrieved products with a third-party service. It's assumed that the connection to the third-party service is implemented within a custom module's main service.
4. Update the last sync date of the store to the current date.
@@ -742,7 +649,15 @@ The `order.placed` event is currently not emitted.
</Note>
<CardList items={[
<Card
href="!docs!/basics/events-and-subscribers"
title="Create a Subscriber"
text="Learn how to create a subscriber in Medusa."
startIcon={<AcademicCapSolid />}
showLinkIcon={false}
/>
{/* <CardList items={[
{
href: "!docs!/basics/events-and-subscribers",
title: "Create a Subscriber",
@@ -757,7 +672,7 @@ The `order.placed` event is currently not emitted.
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
]} />
]} /> */}
---
@@ -786,13 +701,6 @@ Medusa's commerce features are geared towards automating RMA flows and ensuring
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "/events-reference",
title: "Events Reference",
text: "Check out triggered events by each commerce module.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
]} />
---
@@ -891,7 +799,7 @@ The `order.placed` event is currently not emitted.
}
// retrieve the order
const order = await orderModuleService.retrieve(orderId)
const order = await orderModuleService.retrieveOrder(orderId)
if (!order ||
!order.customer_id ||
@@ -901,7 +809,7 @@ The `order.placed` event is currently not emitted.
return
}
const [, count] = await orderModuleService.listAndCount({
const [, count] = await orderModuleService.listAndCountOrders({
customer_id: order.customer_id,
})
@@ -948,13 +856,6 @@ Scheduled jobs are coming soon.
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "/events-reference",
title: "Events Reference",
text: "Check out triggered events in each commerce module.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "!docs!/basics/scheduled-jobs",
title: "Scheduled Jobs",
@@ -969,18 +870,18 @@ Scheduled jobs are coming soon.
For example, create the file `src/subscribers/send-products-newsletter.ts` with the following content:
export const newsletterHighlights = [
["35", "store", "Retrieve the first store in the application."],
["37", "products", "Retrieve the products created since the last newsletter send date."],
["43", "", "Check whether more than 10 products have been created before proceeding."],
["47", "customers", "Retrieve all customers, assuming they're considered subscribed."],
["49", "create", "Send a notification (newsletter) to each customer using the Notification Module."],
["52", '"email"', "Send the notification through the email channel."],
["53", '"newsletter_template"', "Specify the template name in the third-party service (for example, SendGrid)."],
["55", "products", "Pass the created products to the template."],
["60", "update", "Update the store's `last_newsletter_send_date` field with the current date."]
["33", "store", "Retrieve the first store in the application."],
["35", "products", "Retrieve the products created since the last newsletter send date."],
["41", "", "Check whether more than 10 products have been created before proceeding."],
["45", "customers", "Retrieve all customers, assuming they're considered subscribed."],
["47", "createNotifications", "Send a notification (newsletter) to each customer using the Notification Module."],
["50", '"email"', "Send the notification through the email channel."],
["51", '"newsletter_template"', "Specify the template name in the third-party service (for example, SendGrid)."],
["53", "products", "Pass the created products to the template."],
["58", "updateStores", "Update the store's `last_newsletter_send_date` property with the current date."]
]
```ts title="src/subscribers/send-products-newsletter.ts" highlights={newsletterHighlights} collapsibleLines="1-16" expandButtonLabel="Show Imports"
```ts title="src/subscribers/send-products-newsletter.ts" highlights={newsletterHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
@@ -988,13 +889,11 @@ export const newsletterHighlights = [
import {
ModuleRegistrationName,
} from "@medusajs/modules-sdk"
import {
NotificationModuleService,
} from "@medusajs/notification"
import {
ICustomerModuleService,
IProductModuleService,
IStoreModuleService,
INotificationModuleService,
} from "@medusajs/types"
export default async function productCreateHandler({
@@ -1011,13 +910,13 @@ export const newsletterHighlights = [
container.resolve(ModuleRegistrationName.CUSTOMER)
const notificationModuleService:
NotificationModuleService = container.resolve(
INotificationModuleService = container.resolve(
ModuleRegistrationName.NOTIFICATION
)
const store = (await storeModuleService.list())[0]
const store = (await storeModuleService.listStores())[0]
const products = await productModuleService.list({
const products = await productModuleService.listProducts({
created_at: {
$gt: store.metadata.last_newsletter_send_date,
},
@@ -1027,9 +926,9 @@ export const newsletterHighlights = [
return
}
const customers = await customerModuleService.list()
const customers = await customerModuleService.listCustomers()
await notificationModuleService.create(
await notificationModuleService.createNotifications(
customers.map((customer) => ({
to: customer.email,
channel: "email",
@@ -1040,7 +939,7 @@ export const newsletterHighlights = [
}))
)
await storeModuleService.update(store.id, {
await storeModuleService.updateStores(store.id, {
metadata: {
last_newsletter_send_date: (new Date()).toString(),
},
@@ -1055,7 +954,7 @@ export const newsletterHighlights = [
In the subscriber function, you:
1. Retrieve the first store in our application.
2. Retrieve products created since the last time a newsletter is sent. The last send date is stored in the store's `metadata` field.
2. Retrieve products created since the last time a newsletter is sent. The last send date is stored in the store's `metadata` property.
3. If the count of last created products is less than 10, stop execution.
4. Retrieve all customers. Here, it's assumed that all customers are considered subscribed for simplicity.
5. Use the Notification Module to send a notification to the customer. This uses the Notification Provider Module configured for the `email` channel.
File diff suppressed because it is too large Load Diff
@@ -102,7 +102,7 @@ export const serviceHighlights = [
<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.
You can store the product's ID in the external system using the `metadata` property 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>
@@ -203,13 +203,13 @@ export const workflowHighlights = [
.resolve(ModuleRegistrationName.PRODUCT)
const createdProductData = await productModuleService
.retrieve(productId)
.retrieveProduct(productId)
const erpProduct = await erpModuleService.createProduct(
createdProductData
)
await productModuleService.update(productId, {
await productModuleService.updateProducts(productId, {
metadata: {
erp_id: erpProduct.id
}
@@ -229,7 +229,7 @@ export const workflowHighlights = [
.resolve(ModuleRegistrationName.PRODUCT)
await erpModuleService.deleteProduct(erpId)
await productModuleService.update(productId, {
await productModuleService.updateProducts(productId, {
metadata: {}
})
})
@@ -249,7 +249,7 @@ export const workflowHighlights = [
- 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.
- Updates the product in Medusa by setting the ID of the ERP product in the product's `metadata` property.
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.
@@ -334,7 +334,7 @@ For example, suppose an administrator changes the product data in the ERP system
ModuleRegistrationName.PRODUCT
)
await productService.update(id, updatedData)
await productService.updateProducts(id, updatedData)
res.status(200)
}
@@ -34,16 +34,22 @@ In a marketplace, an admin user has a store where they manage their products and
Create a Marketplace Module that holds and manages these relationships.
<Note type="soon">
Module Relationships is coming soon.
</Note>
<CardList items={[
{
href: "/v2/basics/modules-and-services",
href: "!docs!/basics/modules-and-services",
title: "Create a Module",
text: "Learn how to create a module",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "/v2/advanced-development/modules/module-relationships",
href: "!docs!/advanced-development/modules/module-relationships",
title: "Module Relationships",
text: "Create relationships between modules.",
startIcon: <AcademicCapSolid />,
@@ -60,80 +66,57 @@ Create a Marketplace Module that holds and manages these relationships.
Then, create the file `src/modules/marketplace/models/store-user.ts` with the following content:
```ts title="src/modules/marketplace/models/store-user.ts"
import { BaseEntity } from "@medusajs/utils"
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
@Entity()
class StoreUser extends BaseEntity {
@PrimaryKey()
id!: string
@Property({ columnType: "text" })
store_id: string
@Property({ columnType: "text" })
user_id: string
}
import { model } from "@medusajs/utils"
const StoreUser = model.define("store_user", {
id: model.id(),
store_id: model.text(),
user_id: model.text(),
})
export default StoreUser
```
This creates a `StoreUser` data model with the `store_id` and `user_id` fields. These fields will be used later to establish relationships to the Store and User modules.
This creates a `StoreUser` data model with the `store_id` and `user_id` properties.
{/* These properties will be used later to establish relationships to the Store and User modules. */}
Next, create the file `src/modules/marketplace/models/store-product.ts` with the following content:
```ts title="src/modules/marketplace/models/store-product.ts"
import { BaseEntity } from "@medusajs/utils"
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
@Entity()
class StoreProduct extends BaseEntity {
@PrimaryKey()
id!: string
@Property({ columnType: "text" })
store_id: string
@Property({ columnType: "text" })
product_id: string
}
import { model } from "@medusajs/utils"
const StoreProduct = model.define("store_user", {
id: model.id(),
store_id: model.text(),
product_id: model.text(),
})
export default StoreProduct
```
This creates a `StoreProduct` data model with the `store_id` and `product_id` fields. These fields will be used later to establish relationships to the Store and Product modules.
This creates a `StoreProduct` data model with the `store_id` and `product_id` properties.
{/* These properties will be used later to establish relationships to the Store and Product modules. */}
Finally, create the file `src/modules/marketplace/models/store-order.ts` with the following content:
```ts title="src/modules/marketplace/models/store-order.ts"
import { BaseEntity } from "@medusajs/utils"
import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
@Entity()
class StoreOrder extends BaseEntity {
@PrimaryKey()
id!: string
@Property({ columnType: "text" })
store_id: string
@Property({ columnType: "text" })
order_id: string
@Property({ columnType: "text" })
parent_order_id?: string
}
import { model } from "@medusajs/utils"
const StoreOrder = model.define("store_user", {
id: model.id(),
store_id: model.text(),
order_id: model.text(),
parent_order_id: model.text(),
})
export default StoreOrder
```
This creates a `StoreOrder` data model with the `store_id`, `order_id`, and `parent_order_id` fields. The `store_id` and `order_id` fields will be used to establish relationships to the Store and Order modules. Youll learn about the use of `parent_order_id` in a later section.
This creates a `StoreOrder` data model with the `store_id`, `order_id`, and `parent_order_id` properties.
<Note title="Tip">
You can add relationships to data models of other modules in a similar manner.
</Note>
{/* The `store_id` and `order_id` properties will be used to establish relationships to the Store and Order modules. Youll learn about the use of `parent_order_id` in a later section. */}
To reflect these changes on the database, create the migration `src/modules/marketplace/migrations/Migration20240514143248.ts` with the following content:
@@ -159,244 +142,34 @@ Create a Marketplace Module that holds and manages these relationships.
}
}
```
Youll run the migration after registering the module in the Medusa configurations.
Next, youll create types that youll use throughout your customizations.
Create the file `src/types/marketplace/index.ts` with the following content:
```ts title="src/types/marketplace/index.ts"
import {
StoreDTO,
UserDTO,
ProductDTO,
OrderDTO,
} from "@medusajs/types"
export type StoreUserDTO = {
id: string
store_id: string
user_id: string
store?: StoreDTO
user?: UserDTO
}
export type StoreProductDTO = {
id: string
store_id: string
product_id: string
store?: StoreDTO
product?: ProductDTO
}
export type StoreOrderDTO = {
id: string
store_id: string
order_id: string
parent_order_id?: string
store?: StoreDTO
order?: OrderDTO
}
export type CreateStoreUserDTO = {
store_id: string
user_id: string
}
export type CreateStoreProductDTO = {
store_id: string
product_id: string
}
export type CreateStoreOrderDTO = {
store_id: string
order_id: string
parent_order_id?: string
}
```
Then, create the modules main service at `src/modules/marketplace/service.ts` with the following content:
export const mainServiceHighlights = [
["48", "abstractModuleServiceFactory", "Extends the service factory to generate basic data management features."],
["102", "relationships", "Defines relationships to the Store, User, Product, and Order modules."],
["131", "create", "Method to create a `StoreUser`."],
["141", "createStoreProduct", "Method to create a `StoreProduct`."],
["150", "createStoreOrder", "Method to create a `StoreOrder`."]
["6", "MedusaService", "Extends the service factory to generate data management features."]
]
```ts title="src/modules/marketplace/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-17" expandButtonLabel="Show Imports"
import { ModulesSdkUtils, Modules } from "@medusajs/utils"
```ts title="src/modules/marketplace/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { MedusaService } from "@medusajs/utils"
import StoreUser from "./models/store-user"
import StoreProduct from "./models/store-product"
import StoreOrder from "./models/store-order"
import {
CreateStoreOrderDTO,
CreateStoreProductDTO,
CreateStoreUserDTO,
StoreOrderDTO,
StoreProductDTO,
StoreUserDTO,
} from "../../types/marketplace"
import {
ModuleJoinerConfig,
ModulesSdkTypes,
} from "@medusajs/types"
type InjectedDependencies = {
storeUserService: ModulesSdkTypes.InternalModuleService<
any
>
storeProductService: ModulesSdkTypes.InternalModuleService<
any
>
storeOrderService: ModulesSdkTypes.InternalModuleService<
any
>
}
type AllModelsDTO = {
StoreUser: {
dto: StoreUserDTO
},
StoreProduct: {
dto: StoreProductDTO
},
StoreOrder: {
dto: StoreOrderDTO
}
}
const generateMethodsFor = [
class MarketplaceModuleService extends MedusaService({
StoreUser,
StoreProduct,
StoreOrder,
]
class MarketplaceModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
StoreUserDTO,
AllModelsDTO
>(
StoreUser, generateMethodsFor
) {
storeUserService_: ModulesSdkTypes.InternalModuleService<
StoreUser
>
storeProductService_: ModulesSdkTypes.InternalModuleService<
StoreProduct
>
storeOrderService_: ModulesSdkTypes.InternalModuleService<
StoreOrder
>
constructor({
storeUserService,
storeProductService,
storeOrderService,
}: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.storeUserService_ = storeUserService
this.storeProductService_ = storeProductService
this.storeOrderService_ = storeOrderService
}
__joinerConfig(): ModuleJoinerConfig {
return {
serviceName: "marketplaceModuleService",
alias: [
{
name: ["store_user"],
args: {
entity: StoreUser.name,
},
},
{
name: ["store_product"],
args: {
entity: StoreProduct.name,
methodSuffix: "StoreProducts",
},
},
{
name: ["store_order"],
args: {
entity: StoreOrder.name,
methodSuffix: "StoreOrders",
},
},
],
relationships: [
{
serviceName: Modules.STORE,
alias: "store",
primaryKey: "id",
foreignKey: "store_id",
},
{
serviceName: Modules.USER,
alias: "user",
primaryKey: "id",
foreignKey: "user_id",
},
{
serviceName: Modules.PRODUCT,
alias: "product",
primaryKey: "id",
foreignKey: "product_id",
},
{
serviceName: Modules.ORDER,
alias: "order",
primaryKey: "id",
foreignKey: "order_id",
},
],
}
}
async create(
data: CreateStoreUserDTO
): Promise<StoreUserDTO> {
const storeUser = await this.storeUserService_.create(
data
)
return storeUser
}
async createStoreProduct(
data: CreateStoreProductDTO
): Promise<StoreProductDTO> {
const storeProduct = await this.storeProductService_
.create(data)
return storeProduct
}
async createStoreOrder(
data: CreateStoreOrderDTO
): Promise<StoreOrderDTO> {
const storeOrder = await this.storeOrderService_
.create(data)
return storeOrder
}
}
}){
// TODO add custom methods
}
export default MarketplaceModuleService
```
The modules main service:
- Extends the service factory to generate basic data management features.
- Defines relationships to the Store, User, Product, and Order modules.
- Defines `create` methods for the `StoreUser`, `StoreProduct`, and `StoreOrder` data models.
The modules main service extends the service factory to generate data management features for the `StoreUser`, `StoreProduct`, and `StoreOrder` data models.
Finally, create the module definition at `src/modules/marketplace/index.ts` with the following content:
@@ -439,39 +212,49 @@ export const mainServiceHighlights = [
To attach admin users to their own stores, create a subscriber that listens to the `user.created` event and attaches the user to the store.
<Note type="soon">
- The `user.created` event is currently not emitted.
- Module Relationships is coming soon.
</Note>
<Card
href="/v2/basics/events-and-subscribers"
href="!docs!/basics/events-and-subscribers"
title="Create a Subscriber"
text="Learn how to create a subscriber in Medusa."
startIcon={<AcademicCapSolid />}
showLinkIcon={false}
/>
<Details summaryContent="Example">
{/* <Details summaryContent="Example">
Create the file `src/subscribers/user-created.ts` with the following content:
export const userSubscriberHighlights = [
["10", "", "The event data payload with the created user's ID."],
["24", "", "Retrieve the created user."],
["26", "", "Create a store for that user using the Store Module."],
["30", "", "Create a relationship between the user and the store by creating a `StoreUser` record."]
["13", "data", "The event data payload with the created user's ID."],
["27", "retrieveUser", "Retrieve the created user."],
["29", "createStores", "Create a store for that user using the Store Module."],
["33", "createStoreUsers", "Create a relationship between the user and the store by creating a `StoreUser` record."]
]
```ts title="src/subscribers/user-created.ts" highlights={userSubscriberHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa"
```ts title="src/subscribers/user-created.ts" highlights={userSubscriberHighlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
IUserModuleService,
IStoreModuleService,
} from "@medusajs/types"
import MarketplaceModuleService from "../modules/marketplace/service"
export default async function userCreatedHandler({
data,
container,
}: SubscriberArgs<{ id: string }>) {
const { id } = data.data || { data }
const { id } = "data" in data ? data.data : data
const userModuleService: IUserModuleService = container.resolve(
ModuleRegistrationName.USER
)
@@ -481,21 +264,21 @@ export const userSubscriberHighlights = [
const marketplaceModuleService: MarketplaceModuleService = container.resolve(
"marketplaceModuleService"
)
const user = await userModuleService.retrieve(id)
const store = await storeModuleService.create({
const user = await userModuleService.retrieveUser(id)
const store = await storeModuleService.createStores({
name: `${user.first_name}'s Store`,
})
const storeUser = await marketplaceModuleService.create({
const storeUser = await marketplaceModuleService.createStoreUsers({
store_id: store.id,
user_id: user.id,
})
console.log(`Created StoreUser ${storeUser.id}`)
}
export const config: SubscriberConfig = {
event: "user.created",
}
@@ -515,7 +298,7 @@ export const userSubscriberHighlights = [
At the end of the output, you should see the message `Created StoreUser {store_user_id}` where the `{store_user_id}` is the ID of the created `StoreUser`.
</Details>
</Details> */}
---
@@ -523,37 +306,47 @@ export const userSubscriberHighlights = [
Similar to the previous section, to attach products to stores, create a subscriber that listens to the `product.created` event. In the subscriber, you attach the product to the store its created in.
<Note type="soon">
- The `product.created` event is currently not emitted.
- Module Relationships is coming soon.
</Note>
<Card
href="/v2/basics/events-and-subscribers"
href="!docs!/basics/events-and-subscribers"
title="Create a Subscriber"
text="Learn how to create a subscriber in Medusa."
startIcon={<AcademicCapSolid />}
showLinkIcon={false}
/>
<Details summaryContent="Example">
{/* <Details summaryContent="Example">
Create the file `src/subscribers/product-created.ts` with the following content:
export const productSubscriberHighlights = [
["21", "", "Retrieve the created product."],
["23", "", "This subscriber requires the store ID to be set in `product.metadata.store_id`. If not, the subscriber ends execution."],
["27", "", "Create a relationship between the product and the store by creating a `StoreProduct` record."]
["24", "retrieveProduct", "Retrieve the created product."],
["26", "", "This subscriber requires the store ID to be set in `product.metadata.store_id`. If not, the subscriber ends execution."],
["30", "createStoreProducts", "Create a relationship between the product and the store by creating a `StoreProduct` record."]
]
```ts title="src/subscribers/product-created.ts" highlights={productSubscriberHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa"
```ts title="src/subscribers/product-created.ts" highlights={productSubscriberHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
IProductModuleService,
} from "@medusajs/types"
import MarketplaceModuleService from "../modules/marketplace/service"
export default async function productCreateHandler({
data,
container,
}: SubscriberArgs<{ id: string }>) {
const { id } = data.data || data
const { id } = "data" in data ? data.data : data
const productModuleService: IProductModuleService = container.resolve(
ModuleRegistrationName.PRODUCT
)
@@ -561,19 +354,19 @@ export const productSubscriberHighlights = [
const marketplaceModuleService: MarketplaceModuleService = container.resolve(
"marketplaceModuleService"
)
const product = await productModuleService.retrieve(id)
const product = await productModuleService.retrieveProduct(id)
if (!product.metadata?.store_id) {
return
}
await marketplaceModuleService.createStoreProduct({
await marketplaceModuleService.createStoreProducts({
store_id: product.metadata.store_id as string,
product_id: id,
})
}
export const config: SubscriberConfig = {
event: "product.created",
}
@@ -607,7 +400,7 @@ export const productSubscriberHighlights = [
This returns the created product. In the next section, youll implement the API route to fetch the products of a store.
</Details>
</Details> */}
---
@@ -616,16 +409,22 @@ export const productSubscriberHighlights = [
To allow admin users to view their stores products, create an API route that uses the remote query to fetch the products based on the logged-in users store.
<Note type="soon">
Retrieving module relationship data using the remote query is coming soon.
</Note>
<CardList items={[
{
href: "/v2/basics/api-routes",
href: "!docs!/basics/api-routes",
title: "API Routes",
text: "Learn how to create an API Route in Medusa.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "/v2/advanced-development/modules/remote-query",
href: "!docs!/advanced-development/modules/remote-query",
title: "Remote Query",
text: "Use the remote query to fetch data across modules.",
startIcon: <AcademicCapSolid />,
@@ -633,14 +432,14 @@ To allow admin users to view their stores products, create an API route that
},
]} />
<Details summaryContent="Example">
{/* <Details summaryContent="Example">
Create the file `src/api/admin/marketplace/products/route.ts` with the following content:
export const productRoutesHighlights = [
["26", "", "Retrieve the store of the logged-in user."],
["37", "", "Build a query that retrieves the products of that store."],
["49", "", "Retrieve the products using remote query."]
["26", "storeUsers", "Retrieve the store of the logged-in user."],
["38", "query", "Build a query that retrieves the products of that store."],
["50", "remoteQuery", "Retrieve the products using remote query."]
]
```ts title="src/api/admin/marketplace/products/route.ts" highlights={productRoutesHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
@@ -656,7 +455,7 @@ export const productRoutesHighlights = [
} from "@medusajs/utils"
import MarketplaceModuleService
from "../../../../modules/marketplace/service"
export async function GET(
req: AuthenticatedMedusaRequest,
res: MedusaResponse
@@ -668,18 +467,19 @@ export const productRoutesHighlights = [
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const storeUsers = await marketplaceModuleService.list({
user_id: req.auth.actor_id,
})
const storeUsers = await marketplaceModuleService
.listStoreUsers({
user_id: req.auth_context.actor_id,
})
if (!storeUsers.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"This user doesn't have an associated store."
)
}
const query = remoteQueryObjectFromString({
entryPoint: "store_product",
fields: [
@@ -691,9 +491,9 @@ export const productRoutesHighlights = [
},
},
})
const result = await remoteQuery(query)
res.json({
store_id: storeUsers[0].store_id,
products: result.map((data) => data.product),
@@ -741,39 +541,39 @@ export const productRoutesHighlights = [
This will return the product you created in the previous section, if the `{jwt_token}` belongs to the user of the same store ID.
</Details>
</Details> */}
---
## Split Orders Based on Stores
An order may contain items from different stores. To ensure that users can only view and manage their orders, create a subscriber that listens to the `order.placed` event and handles splitting the order into multiple orders based on the items stores.
<Note type="soon">
While this section showcases the implementation, the `order.placed` event is still not emitted in Medusa V2.
The `order.placed` event is still not emitted in Medusa V2.
</Note>
An order may contain items from different stores. To ensure that users can only view and manage their orders, create a subscriber that listens to the `order.placed` event and handles splitting the order into multiple orders based on the items stores.
<Card
href="/v2/basics/events-and-subscribers"
href="!docs!/basics/events-and-subscribers"
title="Create a Subscriber"
text="Learn how to create a subscriber in Medusa."
startIcon={<AcademicCapSolid />}
showLinkIcon={false}
/>
<Details summaryContent="Example">
{/* <Details summaryContent="Example">
Create the file `src/subscribers/order-created.ts` with the following content:
export const orderSubscriberHighlights = [
["35", "", "Loop over the created orders items."],
["57", "", "Group the items by their store ID."],
["72", "", "If the items have the same store ID, then associate the created order with the store."],
["88", "", "If there are items from more than one store in the order, create child orders and associate each of them with the store."],
["91", "parent_order_id", "The `parent_order_id` field in the `StoreOrder` data model points to the original order."]
["72", "createStoreOrders", "If the items have the same store ID, then associate the created order with the store."],
["88", "createStoreOrders", "If there are items from more than one store in the order, create child orders and associate each of them with the store."],
["91", "parent_order_id", "The `parent_order_id` property in the `StoreOrder` data model points to the original order."]
]
```ts title="src/subscribers/order-created.ts" highlights={orderSubscriberHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
@@ -789,12 +589,12 @@ export const orderSubscriberHighlights = [
import MarketplaceModuleService
from "../modules/marketplace/service"
import { createOrdersWorkflow } from "@medusajs/core-flows"
export default async function orderCreatedHandler({
data,
container,
}: SubscriberArgs<{ id: string }>) {
const { id } = data.data || data
const { id } = "data" in data ? data.data : data
const orderModuleService: IOrderModuleService =
container.resolve(
ModuleRegistrationName.ORDER
@@ -804,23 +604,23 @@ export const orderSubscriberHighlights = [
container.resolve(
"marketplaceModuleService"
)
const storeToOrders: Record<string, CreateOrderDTO> = {}
const order = await orderModuleService.retrieve(id, {
const order = await orderModuleService.retrieveOrder(id, {
relations: ["items"],
})
await Promise.all(order.items?.map(async (item) => {
const storeProduct = await marketplaceModuleService
.listStoreProducts({
product_id: item.product_id,
})
if (!storeProduct.length) {
return
}
const storeId = storeProduct[0].store_id
if (!storeToOrders[storeId]) {
@@ -830,32 +630,32 @@ export const orderSubscriberHighlights = [
items: [],
}
}
const { id, ...itemDetails } = item
storeToOrders[storeId].items.push(itemDetails)
}))
const storeToOrdersKeys = Object.keys(storeToOrders)
if (!storeToOrdersKeys.length) {
return
}
if (
storeToOrdersKeys.length === 1 &&
storeToOrders[0].items.length === order.items.length
) {
// The order is composed of items from one store, so
// associate the order as-is with the store.
await marketplaceModuleService.createStoreOrder({
await marketplaceModuleService.createStoreOrders({
store_id: storeToOrdersKeys[0],
order_id: order.id,
})
return
}
// create store orders for each child order
await Promise.all(
storeToOrdersKeys.map(async (storeId) => {
@@ -863,8 +663,8 @@ export const orderSubscriberHighlights = [
.run({
input: storeToOrders[storeId],
})
await marketplaceModuleService.createStoreOrder({
await marketplaceModuleService.createStoreOrders({
store_id: storeId,
order_id: result.id,
parent_order_id: order.id,
@@ -872,7 +672,7 @@ export const orderSubscriberHighlights = [
})
)
}
export const config: SubscriberConfig = {
event: "order.placed",
}
@@ -883,13 +683,13 @@ export const orderSubscriberHighlights = [
- Loop over the created orders items.
- Group the items by their store ID.
- If the items have the same store ID, then associate the created order with the store.
- If there are items from more than one store in the order, create child orders and associate each of them with the store. Here, you use the `parent_order_id` field in the `StoreOrder` data model to point to the original order.
- If there are items from more than one store in the order, create child orders and associate each of them with the store. Here, you use the `parent_order_id` property in the `StoreOrder` data model to point to the original order.
To test this out, create an order in your store. That will run the subscriber and create the child orders.
The next section covers how to retrieve the stores orders.
</Details>
</Details> */}
---
@@ -898,16 +698,22 @@ export const orderSubscriberHighlights = [
Similar to products, to allow admin users to view their stores orders, create an API route that uses the remote query to fetch the orders based on the logged-in users store.
<Note type="soon">
Retrieving module relationship data using the remote query is coming soon.
</Note>
<CardList items={[
{
href: "/v2/basics/api-routes",
href: "!docs!/basics/api-routes",
title: "API Routes",
text: "Learn how to create an API Route in Medusa.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "/v2/advanced-development/modules/remote-query",
href: "!docs!/advanced-development/modules/remote-query",
title: "Remote Query",
text: "Use the remote query to fetch data across modules.",
startIcon: <AcademicCapSolid />,
@@ -915,7 +721,7 @@ Similar to products, to allow admin users to view their stores orders, create
},
]} />
<Details summaryContent="Example">
{/* <Details summaryContent="Example">
Create the file `src/api/admin/marketplace/orders/route.ts` with the following content:
@@ -937,7 +743,7 @@ export const orderRoutesHighlights = [
} from "@medusajs/utils"
import MarketplaceModuleService
from "../../../../modules/marketplace/service"
export async function GET(
req: AuthenticatedMedusaRequest,
res: MedusaResponse
@@ -949,11 +755,11 @@ export const orderRoutesHighlights = [
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const storeUsers = await marketplaceModuleService.list({
user_id: req.auth.actor_id,
const storeUsers = await marketplaceModuleService.listStoreUsers({
user_id: req.auth_context.actor_id,
})
const query = remoteQueryObjectFromString({
entryPoint: "store_order",
fields: [
@@ -965,14 +771,13 @@ export const orderRoutesHighlights = [
},
},
})
const result = await remoteQuery(query)
res.json({
orders: result.map((data) => data.order),
})
}
```
This creates a `GET` API route at `/admin/marketplace/orders`. In the API route, you:
@@ -990,7 +795,7 @@ export const orderRoutesHighlights = [
This will return the orders you created in the previous section if the `{jwt_token}` belongs to the user of the same store ID.
</Details>
</Details> */}
---
@@ -114,15 +114,11 @@ Using the tax-inclusive feature, merchants can specify prices including taxes pe
}
},
{
href: "#",
href: "/storefront-development/products/price",
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"
}
},
]} />
@@ -21,7 +21,7 @@ You also have freedom in how you choose to implement the storefront, allowing yo
## 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.
The Cart Module's `LineItem` data model has a `metadata` property that holds any custom data. You can pass the customer's customization in the request body's `metadata` field when adding a product to the cart.
For example, if youre asking customers to enter a message to put in a letter theyre purchasing, use the `metadata` attribute of the `LineItem` data model to set the personalized information entered by the customer:
@@ -81,9 +81,9 @@ Medusa provides a Next.js Starter storefront with basic ecommerce functionalitie
showLinkIcon: false
},
{
href: "!docs!/storefront-development/tips",
title: "Build Your Own Storefront",
text: "Find tips on how to create a storefront.",
href: "/storefront-development",
title: "Storefront Development",
text: "Find guides for your storefront development.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
+4 -4
View File
@@ -45,7 +45,7 @@ Medusa's modular architecture removes any restrictions you may have while making
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.
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.
The Product Module's `ProductVariant` data model has the properties to implement this integration, mainly the `barcode` attribute. Other notable properties 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.
@@ -53,14 +53,14 @@ To search through product variants by their barcode, create a custom API Route a
<CardList itemsPerRow={2} items={[
{
href: "#",
href: "/commerce-modules/product",
title: "Product Module",
text: "Learn about the Product Module.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
},
{
href: "https://docs.medusajs.com/api/admin",
href: "!docs!/basics/api-routes",
title: "Create API Route",
text: "Learn how to create an API Route.",
startIcon: <AcademicCapSolid />,
@@ -100,7 +100,7 @@ To search through product variants by their barcode, create a custom API Route a
// retrieve product variants by barcode
const productVariants = await productModuleService
.listVariants({
.listProductVariants({
// @ts-ignore
barcode,
})
@@ -141,9 +141,9 @@ Medusa provides a Next.js Starter. Since you've customized your Medusa project,
showLinkIcon: false
},
{
href: "!docs!/storefront-development/tips",
href: "/storefront-development",
title: "Option 2: Build Custom Storefront",
text: "Find tips to build your own storefront.",
text: "Find guides for your storefront development.",
startIcon: <AcademicCapSolid />,
showLinkIcon: false
}