docs: improved commerce modules [4/n] (#9517)

Improve pricing, product, and promotion modules docs

[4/n]
This commit is contained in:
Shahed Nasser
2024-10-16 12:34:36 +03:00
committed by GitHub
parent eb364834de
commit f6d3453e6d
27 changed files with 1767 additions and 341 deletions
@@ -18,7 +18,7 @@ You'll then learn how to:
<Note title="Tip">
Similar steps can bee applied to the `CustomerAddress` data model.
Similar steps can be applied to the `CustomerAddress` data model.
</Note>
@@ -270,7 +270,7 @@ In the workflow, you:
2. Create the `Custom` record using the `createCustomStep`.
3. Use the `when-then` utility to link the customer to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
You'll next call the workflow in the hook handler.
You'll next execute the workflow in the hook handler.
### Consume Workflow Hook
@@ -308,9 +308,9 @@ The hook handler executes the `createCustomFromCustomerWorkflow`, passing it its
To test it out, send a `POST` request to `/admin/customers` to create a customer, passing `custom_name` in `additional_data`:
```bash
curl --location 'localhost:9000/admin/customers' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {token}' \
curl -X POST 'localhost:9000/admin/customers' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
"email": "customer@gmail.com",
"additional_data": {
@@ -342,7 +342,7 @@ The `+` prefix in `+custom.*` indicates that the relation should be retrieved wi
For example:
```bash
curl -X POST 'localhost:9000/admin/customers/{customer_id}?fields=+custom.*' \
curl 'localhost:9000/admin/customers/{customer_id}?fields=+custom.*' \
-H 'Authorization: Bearer {token}'
```
@@ -670,8 +670,8 @@ To test it out, send a `POST` request to `/admin/customers/:id` to update a cust
```bash
curl -X POST 'localhost:9000/admin/customers/{customer_id}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"additional_data": {
"custom_name": "test3"
@@ -4,11 +4,13 @@ export const metadata = {
# {metadata.title}
In this document, youll learn about the main concepts in the Pricing Module, and how data is stored and related.
In this document, youll learn about the main concepts in the Pricing Module.
## Price Set
A [PriceSet](/references/pricing/models/PriceSet) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). Each of these prices are represented by the [Price data module](/references/pricing/models/Price).
A [PriceSet](/references/pricing/models/PriceSet) represents a collection of prices that are linked to a resource (for example, a product or a shipping option).
Each of these prices are represented by the [Price data module](/references/pricing/models/Price).
![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg)
@@ -16,6 +18,8 @@ A [PriceSet](/references/pricing/models/PriceSet) represents a collection of pri
## Price List
A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` properties, which indicate the date range in which a price list can be applied.
A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied.
A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.
Its associated prices are represented by the `Price` data model.
@@ -13,16 +13,15 @@ In this document, youll find common examples of how you can use the Pricing M
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -84,16 +83,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -130,16 +128,15 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -176,16 +173,15 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -255,7 +251,7 @@ export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -330,16 +326,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -0,0 +1,29 @@
export const metadata = {
title: `Links between Pricing Module and Other Modules`,
}
# {metadata.title}
This document showcases the module links defined between the Pricing Module and other commerce modules.
## Fulfillment Module
The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options.
Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.
![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)
---
## Product Module
The Product Module doesn't store or manage the prices of product variants.
Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variants prices are stored as prices belonging to a price set.
![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg)
So, when you want to add prices for a product variant, you create a price set and add the prices to it.
You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.
@@ -6,7 +6,7 @@ export const metadata = {
# {metadata.title}
The Pricing Module is the `@medusajs/medusa/pricing` NPM package that provides pricing-related features in your Medusa and Node.js applications.
The Pricing Module provides pricing-related features in your Medusa and Node.js applications.
## How to Use Pricing Module's Service
@@ -15,18 +15,33 @@ You can use the Pricing Module's main service by resolving from the Medusa conta
For example:
<CodeTabs groupId="resource-type">
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const pricingModuleService = container.resolve(
Modules.PRICING
)
const priceSets = await pricingModuleService.listPriceSets()
})
```
</CodeTab>
<CodeTab label="API Route" value="api-route">
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const pricingModuleService: IPricingModuleService = request.scope.resolve(
const pricingModuleService = request.scope.resolve(
Modules.PRICING
)
@@ -39,35 +54,17 @@ export async function GET(
</CodeTab>
<CodeTab label="Subscriber" value="subscribers">
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function subscriberHandler({ container }: SubscriberArgs) {
const pricingModuleService: IPricingModuleService = container.resolve(
const pricingModuleService = container.resolve(
Modules.PRICING
)
const priceSets = await pricingModuleService.listPriceSets()
}
```
</CodeTab>
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { IPricingModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const pricingModuleService: IPricingModuleService = container.resolve(
Modules.PRICING
)
const priceSets = await pricingModuleService.listPriceSets()
})
```
</CodeTab>
@@ -79,7 +76,7 @@ const step1 = createStep("step-1", async (_, { container }) => {
### Price Management
With the Pricing Module, store the prices of a resource and manage them through the main service's methods.
Store the prices of a resource and manage them through the main service's methods.
Prices are grouped in a price set, allowing you to add more than one price for a resource based on different conditions, such as currency code.
@@ -122,7 +119,9 @@ const priceSet = await pricingModuleService.addPrices({
### Price Lists
Price lists allow you to group prices and apply them only in specific conditions. You can also use them to override existing prices for the specified conditions.
Group prices and apply them only in specific conditions with price lists.
You can also use them to override existing prices for specified conditions, or create a sale.
```ts
const priceList = await pricingModuleService.createPriceLists([
@@ -6,7 +6,7 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how prices are calculated when you use the `calculatePrices` method of the Pricing Module's main service.
In this document, you'll learn how prices are calculated when you use the [calculatePrices method](/references/pricing/calculatePrices) of the Pricing Module's main service.
## calculatePrices Method
@@ -14,9 +14,7 @@ The [calculatePrices method](/references/pricing/calculatePrices) accepts as par
It returns a price object with the best matching price for each price set.
---
## Calculation Context
### Calculation Context
The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.
@@ -34,16 +32,18 @@ const price = await pricingModuleService.calculatePrices(
)
```
---
In this example, you retrieve the prices in a price set for the specified currency code and region ID.
## Returned Price Object
### Returned Price Object
For each price set, the method selects two prices:
For each price set, the `calculatePrices` method selects two prices:
- The calculated price: Either the best context-matching price that belongs to a price list or the same as the original price.
- The original price: Either the same as the calculated price if its price list is of type `override`, or the best context-matching price that doesn't belong to a price list.
- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
- An original price, which is either:
- The same price as the calculated price if the price list it belongs to is of type `override`;
- Or a price that doesn't belong to a price list and best matches the specified context.
Both prices are returned in an object along with the following properties:
Both prices are returned in an object that has the following properties:
<TypeList
types={[
@@ -8,24 +8,26 @@ In this document, you'll learn about price rules for price sets and price lists.
## Price Rule
Prices can be restricted by rules. Each rule of a price is represented by the [PriceRule data model](/references/pricing/models/PriceRule).
You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](/references/pricing/models/PriceRule).
The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.
![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg)
For exmaple, you create a price restricted to `10557` zip codes.
A price can have multiple price rules. For example:
![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg)
![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg)
A price can have multiple price rules.
For example, a price can be restricted by a region and a zip code.
![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg)
---
## Price List Rules
Rules that can be applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule). The `rules_count` property of a `PriceList` indicates how many rules are applied to it.
Rules applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule).
The `rules_count` property of a `PriceList` indicates how many rules are applied to it.
![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg)
@@ -1,23 +0,0 @@
export const metadata = {
title: `Relations between Pricing Module and Other Modules`,
}
# {metadata.title}
This document showcases the link modules defined between the Pricing Module and other commerce modules.
## Fulfillment Module
A shipping option's price is stored as a price set. Medusa defines a link module that builds a relationship between the `PriceSet` and `ShippingOption` data models.
![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)
---
## Product Module
A product variants prices are stored as prices belonging to a price set. Medusa defines a link module that builds a relationship between the `ProductVariant` and the `PriceSet`.
![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg)
So, when you want to add prices for a product variant, you create a price set and add the prices to it. You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.
@@ -4,19 +4,19 @@ export const metadata = {
# {metadata.title}
In this document, youll learn about tax-inclusive pricing and how it's used during prices calculation.
In this document, youll learn about tax-inclusive pricing and how it's used when calculating prices.
## What is Tax-Inclusive Pricing?
A tax-inclusive price is a price that includes taxes. The tax amount is calculated from the price rather than added to it.
A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it.
For example, if a products price is $50 and the tax rate is 2%, then the tax-inclusive price is $49, and the applied tax amount is $1.
For example, if a products price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1.
---
## How is Tax-Inclusive Pricing Set?
The `PricePreference` data model holds the tax-inclusive setting for a context. It has two properties that indicate the context:
The [PricePreference data model](/references/pricing/PricePreference) holds the tax-inclusive setting for a context. It has two properties that indicate the context:
- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`.
- `value`: The attributes value. For example, `reg_123` or `usd`.
@@ -55,6 +55,12 @@ To get accurate tax results, pass the `region_id` and / or `currency_code` in th
The `calculatePrices` method returns two properties related to tax-inclusivity:
<Note title="Tip">
Learn more about the returned properties in [this guide](../price-calculation/page.mdx#returned-price-object).
</Note>
- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive.
- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive.
@@ -65,10 +71,8 @@ A price is considered tax-inclusive if:
### Tax Context Precedence
If:
A regions price preferences `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if:
- both the `region_id` and `currency_code` are provided in the calculation context;
- the selected price belongs to the region;
- and the region has a price preference
Then, the regions price preferences `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive.
@@ -13,13 +13,12 @@ In this guide, youll find common examples of how you can use the Product Modu
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -94,13 +93,12 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -137,13 +135,12 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -184,13 +181,12 @@ export async function GET(
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -231,13 +227,12 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -274,13 +269,12 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab value="medusa" label="Medusa API Router">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -0,0 +1,684 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `Extend Product Data Model`,
}
# {metadata.title}
In this documentation, you'll learn how to extend a data model of the Product Module to add a custom property.
You'll create a `Custom` data model in a module. This data model will have a `custom_name` property, which is the property you want to add to the [Product data model](/references/product/models/Product) defined in the Product Module.
You'll then learn how to:
- Link the `Custom` data model to the `Product` data model.
- Set the `custom_name` property when a product is created or updated using Medusa's API routes.
- Retrieve the `custom_name` property with the product's details, in custom or existing API routes.
<Note title="Tip">
Similar steps can be applied to the `ProductVariant` or `ProductOption` data models.
</Note>
## Step 1: Define Custom Data Model
Consider you have a Hello Module defined in the `/src/modules/hello` directory.
<Note title="Tip">
If you don't have a module, follow [this guide](!docs!/basics/modules) to create one.
</Note>
To add the `custom_name` property to the `Product` data model, you'll create in the Hello Module a data model that has the `custom_name` property.
Create the file `src/modules/hello/models/custom.ts` with the following content:
```ts title="src/modules/hello/models/custom.ts"
import { model } from "@medusajs/framework/utils"
export const Custom = model.define("custom", {
id: model.id().primaryKey(),
custom_name: model.text(),
})
```
This creates a `Custom` data model that has the `id` and `custom_name` properties.
<Note title="Tip">
Learn more about data models in [this guide](!docs!/data-models).
</Note>
---
## Step 2: Define Link to Product Data Model
Next, you'll define a module link between the `Custom` and `Product` data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.
<Note title="Tip">
Learn more about module links in [this guide](!docs!/module-links).
</Note>
Create the file `src/links/product-custom.ts` with the following content:
```ts title="src/links/product-custom.ts"
import { defineLink } from "@medusajs/framework/utils";
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/medusa/product"
export default defineLink(
ProductModule.linkable.product,
HelloModule.linkable.custom,
)
```
This defines a link between the `Product` and `Custom` data models. Using this link, you'll later query data across the modules, and link records of each data model.
---
## Step 3: Generate and Run Migrations
<Prerequisites
items={[
{
text: "Module must be registered in medusa-config.js",
link: "!docs!/basics/modules#4-add-module-to-configurations"
}
]}
/>
To reflect the `Custom` data model in the database, generate a migration that defines the table to be created for it.
Run the following command in your Medusa project's root:
```bash
npx medusa db:generate helloModuleService
```
Where `helloModuleService` is your module's name.
Then, run the `db:migrate` command to run the migrations and create a table in the database for the link between the `Product` and `Custom` data models:
```bash
npx medusa db:migrate
```
A table for the link is now created in the database. You can now retrieve and manage the link between records of the data models.
---
## Step 4: Consume productsCreated Workflow Hook
When a product is created, you also want to create a `Custom` record and set the `custom_name` property, then create a link between the `Product` and `Custom` records.
To do that, you'll consume the [productsCreated](/references/medusa-workflows/createProductsWorkflow#productscreated) hook of the [createProductsWorkflow](/references/medusa-workflows/createProductsWorkflow). This workflow is executed in the [Create Product Admin API route](!api!/admin#products_postproducts)
<Note title="Tip">
Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks).
</Note>
The API route accepts in its request body an `additional_data` parameter. You can pass in it custom data, which is passed to the workflow hook handler.
### Add custom_name to Additional Data Validation
To pass the `custom_name` in the `additional_data` parameter, you must add a validation rule that tells the Medusa application about this custom property.
Create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { z } from "zod"
export default defineMiddlewares({
routes: [
{
method: "POST",
matcher: "/admin/products",
additionalDataValidator: {
custom_name: z.string().optional(),
},
},
],
})
```
The `additional_data` parameter validation is customized using the `defineMiddlewares` utility function. In the routes middleware configuration object, the `additionalDataValidator` property accepts [Zod](https://zod.dev/) validaiton rules.
In the snippet above, you add a validation rule indicating that `custom_name` is a string that can be passed in the `additional_data` object.
<Note title="Tip">
Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data).
</Note>
### Create Workflow to Create Custom Record
You'll now create a workflow that will be used in the hook handler.
This workflow will create a `Custom` record, then link it to the product.
Start by creating the step that creates the `Custom` record. Create the file `src/workflows/create-custom-from-product/steps/create-custom.ts` with the following content:
```ts title="src/workflows/create-custom-from-product/steps/create-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type CreateCustomStepInput = {
custom_name?: string
}
export const createCustomStep = createStep(
"create-custom",
async (data: CreateCustomStepInput, { container }) => {
if (!data.custom_name) {
return
}
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const custom = await helloModuleService.createCustoms(data)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
}
)
```
In the step, you resolve the Hello Module's main service and create a `Custom` record.
In the compensation function that undoes the step's actions in case of an error, you delete the created record.
<Note title="Tip">
Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function).
</Note>
Then, create the workflow at `src/workflows/create-custom-from-product/index.ts` with the following content:
```ts title="src/workflows/create-custom-from-product/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { ProductDTO } from "@medusajs/framework/types"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { createCustomStep } from "./steps/create-custom"
export type CreateCustomFromProductWorkflowInput = {
product: ProductDTO
additional_data?: {
custom_name?: string
}
}
export const createCustomFromProductWorkflow = createWorkflow(
"create-custom-from-product",
(input: CreateCustomFromProductWorkflowInput) => {
const customName = transform(
{
input
},
(data) => data.input.additional_data.custom_name || ""
)
const custom = createCustomStep({
custom_name: customName
})
when(({ custom }), ({ custom }) => custom !== undefined)
.then(() => {
createRemoteLinkStep([{
[Modules.PRODUCT]: {
product_id: input.product.id
},
[HELLO_MODULE]: {
custom_id: custom.id
}
}])
})
return new WorkflowResponse({
custom
})
}
)
```
The workflow accepts as an input the created product and the `additional_data` parameter passed in the request. This is the same input that the `productsCreated` hook accepts.
In the workflow, you:
1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
2. Create the `Custom` record using the `createCustomStep`.
3. Use the `when-then` utility to link the product to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
You'll next execute the workflow in the hook handler.
### Consume Workflow Hook
You can now consume the `productsCreated` hook, which is executed in the `createProductsWorkflow` after the product is created.
To consume the hook, create the file `src/workflow/hooks/product-created.ts` with the following content:
```ts title="src/workflow/hooks/product-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import {
createCustomFromProductWorkflow,
CreateCustomFromProductWorkflowInput
} from "../create-custom-from-product"
createProductsWorkflow.hooks.productsCreated(
async ({ products, additional_data }, { container }) => {
const workflow = createCustomFromProductWorkflow(container)
for (let product of products) {
await workflow.run({
input: {
product,
additional_data
} as CreateCustomFromProductWorkflowInput
})
}
}
)
```
The hook handler executes the `createCustomFromProductWorkflow`, passing it its input.
### Test it Out
To test it out, send a `POST` request to `/admin/products` to create a product, passing `custom_name` in `additional_data`:
```bash
curl -X POST 'localhost:9000/admin/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"title": "Shoes",
"additional_data": {
"custom_name": "test"
}
}'
```
Make sure to replace `{token}` with an admin user's JWT token. Learn how to retrieve it in the [API reference](!api!/admin#1-bearer-authorization-with-jwt-tokens).
The request will return the product's details. You'll learn how to retrive the `custom_name` property with the product's details in the next section.
---
## Step 5: Retrieve custom_name with Product Details
When you extend an existing data model through links, you also want to retrieve the custom properties with the data model.
### Retrieve in API Routes
To retrieve the `custom_name` property when you're retrieving the product through API routes, such as the [Get Product API Route](!api!/admin#products_getproductsid), pass in the `fields` query parameter `+custom.*`, which retrieves the linked `Custom` record's details.
<Note title="Tip">
The `+` prefix in `+custom.*` indicates that the relation should be retrieved with the default product fields. Learn more about selecting fields and relations in the [API reference](!api!/admin#select-fields-and-relations).
</Note>
For example:
```bash
curl 'localhost:9000/admin/products/{product_id}?fields=+custom.*' \
-H 'Authorization: Bearer {token}'
```
Make sure to replace `{product_id}` with the product's ID, and `{token}` with an admin user's JWT token.
Among the returned `product` object, you'll find a `custom` property which holds the details of the linked `Custom` record:
```json
{
"product": {
// ...
"custom": {
"id": "01J9NP7ANXDZ0EAYF0956ZE1ZA",
"custom_name": "test",
"created_at": "2024-10-08T09:09:06.877Z",
"updated_at": "2024-10-08T09:09:06.877Z",
"deleted_at": null
}
}
}
```
### Retrieve using Query
You can also retrieve the `Custom` record linked to a product in your code using [Query](!docs!/advanced-development/module-links/query).
For example:
```ts
const { data: [product] } = await query.graph({
entity: "product",
fields: ["*", "custom.*"],
filters: {
id: product_id,
},
})
```
Learn more about how to use Query in [this guide](!docs!/advanced-development/module-links/query).
---
## Step 6: Consume productsUpdated Workflow Hook
Similar to the `productsCreated` hook, you'll consume the [productsUpdated](/references/medusa-workflows/updateProductsWorkflow#productsUpdated) hook of the [updateProductsWorkflow](/references/medusa-workflows/updateProductsWorkflow) to update `custom_name` when the product is updated.
The `updateProductsWorkflow` is executed by the [Update Product API route](!api!/admin#products_postproductsid), which accepts the `additional_data` parameter to pass custom data to the hook.
### Add custom_name to Additional Data Validation
To allow passing `custom_name` in the `additional_data` parameter of the update product route, add in `src/api/middlewares.ts` a new route middleware configuration object:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { z } from "zod"
export default defineMiddlewares({
routes: [
// ...
{
method: "POST",
matcher: "/admin/products/:id",
additionalDataValidator: {
custom_name: z.string().nullish(),
},
},
],
})
```
The validation schema is the similar to that of the Create Product API route, except you can pass a `null` value for `custom_name` to remove or unset the `custom_name`'s value.
### Create Workflow to Update Custom Record
Next, you'll create a workflow that creates, updates, or deletes `Custom` records based on the provided `additional_data` parameter:
1. If `additional_data.custom_name` is set and it's `null`, the `Custom` record linked to the product is deleted.
2. If `additional_data.custom_name` is set and the product doesn't have a linked `Custom` record, a new record is created and linked to the product.
3. If `additional_data.custom_name` is set and the product has a linked `Custom` record, the `custom_name` property of the `Custom` record is updated.
Start by creating the step that updates a `Custom` record. Create the file `src/workflows/update-custom-from-product/steps/update-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-product/steps/update-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { HELLO_MODULE } from "../../../modules/hello"
import HelloModuleService from "../../../modules/hello/service"
type UpdateCustomStepInput = {
id: string
custom_name: string
}
export const updateCustomStep = createStep(
"update-custom",
async ({ id, custom_name }: UpdateCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const prevData = await helloModuleService.retrieveCustom(id)
const custom = await helloModuleService.updateCustoms({
id,
custom_name,
})
return new StepResponse(custom, prevData)
},
async (prevData, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.updateCustoms(prevData)
}
)
```
In this step, you update a `Custom` record. In the compensation function, you revert the update.
Next, you'll create the step that deletes a `Custom` record. Create the file `src/workflows/update-custom-from-product/steps/delete-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-product/steps/delete-custom.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Custom } from "../../../modules/hello/models/custom"
import { InferTypeOf } from "@medusajs/framework/types"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type DeleteCustomStepInput = {
custom: InferTypeOf<typeof Custom>
}
export const deleteCustomStep = createStep(
"delete-custom",
async ({ custom }: DeleteCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.createCustoms(custom)
}
)
```
In this step, you delete a `Custom` record. In the compensation function, you create it again.
Finally, you'll create the workflow. Create the file `src/workflows/update-custom-from-product/index.ts` with the following content:
```ts title="src/workflows/update-custom-from-product/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { ProductDTO } from "@medusajs/framework/types"
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, dismissRemoteLinkStep, useRemoteQueryStep } from "@medusajs/medusa/core-flows"
import { createCustomStep } from "../create-custom-from-cart/steps/create-custom"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { deleteCustomStep } from "./steps/delete-custom"
import { updateCustomStep } from "./steps/update-custom"
export type UpdateCustomFromProductStepInput = {
product: ProductDTO
additional_data?: {
custom_name?: string | null
}
}
export const updateCustomFromProductWorkflow = createWorkflow(
"update-custom-from-product",
(input: UpdateCustomFromProductStepInput) => {
const productData = useRemoteQueryStep({
entry_point: "product",
fields: ["custom.*"],
variables: {
filters: {
id: input.product.id
}
},
list: false
})
// TODO create, update, or delete Custom record
}
)
```
The workflow accepts the same input as the `productsUpdated` workflow hook handler would.
In the workflow, you retrieve the product's linked `Custom` record using Query.
Next, replace the `TODO` with the following:
```ts title="src/workflows/update-custom-from-product/index.ts"
const created = when({
input,
productData
}, (data) =>
!data.productData.custom &&
data.input.additional_data?.custom_name?.length > 0
)
.then(() => {
const custom = createCustomStep({
custom_name: input.additional_data.custom_name
})
createRemoteLinkStep([{
[Modules.PRODUCT]: {
product_id: input.product.id
},
[HELLO_MODULE]: {
custom_id: custom.id
}
}])
return custom
})
// TODO update, or delete Custom record
```
Using the `when-then` utility, you check if the product doesn't have a linked `Custom` record and the `custom_name` property is set. If so, you create a `Custom` record and link it to the product.
To create the `Custom` record, you use the `createCustomStep` you created in an earlier section.
Next, replace the new `TODO` with the following:
```ts title="src/workflows/update-custom-from-product/index.ts"
const deleted = when({
input,
productData
}, (data) =>
data.productData.custom && (
data.input.additional_data?.custom_name === null ||
data.input.additional_data?.custom_name.length === 0
)
)
.then(() => {
deleteCustomStep({
custom: productData.custom
})
dismissRemoteLinkStep({
[HELLO_MODULE]: {
custom_id: productData.custom.id
}
})
return productData.custom.id
})
// TODO delete Custom record
```
Using the `when-then` utility, you check if the product has a linked `Custom` record and `custom_name` is `null` or an empty string. If so, you delete the linked `Custom` record and dismiss its links.
Finally, replace the new `TODO` with the following:
```ts title="src/workflows/update-custom-from-product/index.ts"
const updated = when({
input,
productData
}, (data) => data.productData.custom && data.input.additional_data?.custom_name?.length > 0)
.then(() => {
const custom = updateCustomStep({
id: productData.custom.id,
custom_name: input.additional_data.custom_name
})
return custom
})
return new WorkflowResponse({
created,
updated,
deleted
})
```
Using the `when-then` utility, you check if the product has a linked `Custom` record and `custom_name` is passed in the `additional_data`. If so, you update the linked `Custom` recod.
You return in the workflow response the created, updated, and deleted `Custom` record.
### Consume productsUpdated Workflow Hook
You can now consume the `productsUpdated` and execute the workflow you created.
Create the file `src/workflows/hooks/product-updated.ts` with the following content:
```ts title="src/workflows/hooks/product-updated.ts"
import { updateProductsWorkflow } from "@medusajs/medusa/core-flows"
import {
UpdateCustomFromProductStepInput,
updateCustomFromProductWorkflow
} from "../update-custom-from-product"
updateProductsWorkflow.hooks.productsUpdated(
async ({ products, additional_data }, { container }) => {
const workflow = updateCustomFromProductWorkflow(container)
for (let product of products) {
await workflow.run({
input: {
product,
additional_data
} as UpdateCustomFromProductStepInput
})
}
}
)
```
In the workflow hook handler, you execute the workflow, passing it the hook's input.
### Test it Out
To test it out, send a `POST` request to `/admin/products/:id` to update a product, passing `custom_name` in `additional_data`:
```bash
curl -X POST 'localhost:9000/admin/products/{product_id}?fields=+custom.*' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"additional_data": {
"custom_name": "test 2"
}
}'
```
Make sure to replace `{product_id}` with the product's ID, and `{token}` with the JWT token of an admin user.
The request will return the product's details with the updated `custom` linked record.
@@ -14,7 +14,7 @@ In this document, you'll learn how to calculate a product variant's price with t
You'll need the following resources for the taxes calculation:
1. Query to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx).
1. [Query](!docs!/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx).
2. The Tax Module's main service to get the tax lines for each product.
```ts
@@ -37,6 +37,12 @@ const taxModuleService = container.resolve(
After resolving the resources, use Query to retrieve the products with the variants' prices for a context:
<Note>
Learn more about retrieving product variants' prices for a context in [this guide](../price/page.mdx).
</Note>
```ts
import { QueryContext } from "@medusajs/framework/utils"
@@ -63,12 +69,6 @@ const { data: products } = await query.graph({
})
```
<Note>
Learn more about retrieving product variants' prices for a context in [this guide](../price/page.mdx).
</Note>
---
## Step 2: Get Tax Lines for Products
@@ -1,5 +1,5 @@
---
sidebar_label: "Get Product Variant Prices"
sidebar_label: "Get Variant Prices"
---
export const metadata = {
@@ -8,7 +8,7 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to retrieve product variant prices in the Medusa application using the [Query](!docs!/advanced-development/module-links/query).
In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](!docs!/advanced-development/module-links/query).
<Note title="Why use Query?">
@@ -57,11 +57,11 @@ Learn more about prices calculation in [this Pricing Module documentation](../..
To retrieve calculated prices of variants based on a context, retrieve the products using Query and:
- Pass `variants.calculated_price.*` in the `fields` property.
- Pass a `context` property in the object parameter. Its value is an object of objects to sets the context for the retrieved fields.
- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields.
For example:
```ts highlights={[["6"], ["12"], ["13"], ["14"], ["15"], ["16"], ["17"]]}
```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]}
import { QueryContext } from "@medusajs/framework/utils"
// ...
@@ -0,0 +1,39 @@
export const metadata = {
title: `Links between Product Module and Other Modules`,
}
# {metadata.title}
This document showcases the module links defined between the Product Module and other commerce modules.
## Pricing Module
The Product Module doesn't provide pricing-related features.
Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variants prices are stored belonging to a price set.
![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg)
So, to add prices for a product variant, create a price set and add the prices to it.
---
## Sales Channel Module
The Sales Channel Module provides functionalities to manage multiple selling channels in your store.
Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels.
![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg)
---
## Inventory Module
The Inventory Module provides inventory-management features for any stock-kept item.
Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details.
![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)
When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.
@@ -6,7 +6,7 @@ export const metadata = {
# {metadata.title}
The Product Module is the `@medusajs/medusa/product` NPM package that provides product-related features in your Medusa and Node.js applications.
The Product Module provides product-related features in your Medusa and Node.js applications.
## How to Use Product Module's Service
@@ -15,15 +15,30 @@ You can use the Product Module's main service by resolving from the Medusa conta
For example:
<CodeTabs groupId="resource-type">
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const productModuleService = container.resolve(
Modules.PRODUCT
)
const products = await productModuleService.listProducts()
})
```
</CodeTab>
<CodeTab label="API Route" value="api-route">
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(request: MedusaRequest, res: MedusaResponse) {
const productModuleService: IProductModuleService = request.scope.resolve(
const productModuleService = request.scope.resolve(
Modules.PRODUCT
)
@@ -36,35 +51,17 @@ export async function GET(request: MedusaRequest, res: MedusaResponse) {
</CodeTab>
<CodeTab label="Subscriber" value="subscribers">
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function subscriberHandler({ container }: SubscriberArgs) {
const productModuleService: IProductModuleService = container.resolve(
const productModuleService = container.resolve(
Modules.PRODUCT
)
const products = await productModuleService.listProducts()
}
```
</CodeTab>
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const productModuleService: IProductModuleService = container.resolve(
Modules.PRODUCT
)
const products = await productModuleService.listProducts()
})
```
</CodeTab>
@@ -1,53 +0,0 @@
export const metadata = {
title: `Relations between Product Module and Other Modules`,
}
# {metadata.title}
This document showcases the link modules defined between the Product Module and other commerce modules.
## Cart Module
A cart's line item is associated with a product and its variant. Medusa defines a link module that builds a relationship between the `Cart`, `Product`, and `ProductVariant` data models.
![A diagram showcasing an example of how data models from the Cart and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716546229/Medusa%20Resources/cart-product_x82x9j.jpg)
---
## Order Module
An order's line item is associated with the purchased product and its variant. Medusa defines a link module that builds a relationship between the `LineItem`, `Product`, and `ProductVariant` data models.
![A diagram showcasing an example of how data models from the Order and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716556100/Medusa%20Resources/order-product_l6ylte.jpg)
---
## Pricing Module
A product variants prices are stored as money amounts belonging to a price set. Medusa defines a link module that builds a relationship between the `ProductVariant` and the `PriceSet` data models.
![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg)
So, to add prices for a product variant, create a price set and add the prices as money amounts to it.
Learn more about the `PriceSet` data model in the [Pricing Concepts](../../pricing/concepts/page.mdx#price-list)
---
## Sales Channel Module
A product can have different availability in different sales channels. Medusa defines a link module that builds a relationship between the `Product` and `SalesChannel` data models.
![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg)
---
## Inventory Module
Each product variant has different inventory details. Medusa defines a link module that builds a relationship between the `ProductVariant` and `InventoryItem` data models.
![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)
When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.
Learn more about the `InventoryItem` data model in the [Inventory Concepts](../../inventory/concepts/page.mdx#inventoryitem)
@@ -4,7 +4,7 @@ export const metadata = {
# {metadata.title}
In this document, youll learn about promotion actions and how theyre computed and used.
In this document, youll learn about promotion actions and how theyre computed using the [computeActions method](/references/promotion/computeActions).
## computeActions Method
@@ -32,7 +32,7 @@ export interface AddItemAdjustmentAction {
}
```
This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module.
This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module.
<Note>
@@ -57,7 +57,7 @@ export interface RemoveItemAdjustmentAction {
}
```
This action means that a new record should be removed of the `LineItemAdjustment` with the specified ID in the `adjustment_id` property.
This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property.
<Note>
@@ -81,7 +81,7 @@ export interface AddShippingMethodAdjustment {
}
```
This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module.
This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module.
<Note>
@@ -105,7 +105,7 @@ export interface RemoveShippingMethodAdjustment {
}
```
When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` with the specified ID in the `adjustment_id` property.
When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property.
<Note>
@@ -10,7 +10,7 @@ In this document, youll learn about the main promotion and rule concepts in t
## What is a Promotion?
A promotion, represented by the [Promotion data model](/references/promotion/models/Promotion), represents a discount applied on cart items, shipping methods, or entire orders.
A promotion, represented by the [Promotion data model](/references/promotion/models/Promotion), is a discount that can be applied on cart items, shipping methods, or entire orders.
A promotion has two types:
@@ -72,15 +72,23 @@ A promotion has two types:
</Table.Body>
</Table>
---
## PromotionRule
A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](/references/promotion/models/PromotionRule). For example, you can create a promotion that only customers of the `VIP` customer group can use.
A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](/references/promotion/models/PromotionRule).
For example, you can create a promotion that only customers of the `VIP` customer group can use.
![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg)
A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.
A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied.
When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.
For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.
When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself.
For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.
---
@@ -90,8 +98,8 @@ The `PromotionRule`'s `operator` property adds more flexibility to the rules
For example, to restrict the promotion to only `VIP` and `B2B` customer groups:
- Add a `PromotionRule` with its `attribute` property set to `customer_group_id` and `operator` property to `in`.
- Add two `PromotionRuleValue` associated with the rule: one with the value `VIP` and the other `B2B`.
- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`.
- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`.
![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg)
@@ -13,16 +13,15 @@ In this document, youll find common examples of how you can use the Promotion
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const promotionModuleService: IPromotionModuleService = request.scope.resolve(
const promotionModuleService = request.scope.resolve(
Modules.PROMOTION
)
@@ -78,16 +77,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const promotionModuleService: IPromotionModuleService = request.scope.resolve(
const promotionModuleService = request.scope.resolve(
Modules.PROMOTION
)
@@ -135,16 +133,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function POST(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const promotionModuleService: IPromotionModuleService = request.scope.resolve(
const promotionModuleService = request.scope.resolve(
Modules.PROMOTION
)
@@ -213,16 +210,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const promotionModuleService: IPromotionModuleService = request.scope.resolve(
const promotionModuleService = request.scope.resolve(
Modules.PROMOTION
)
@@ -0,0 +1,696 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `Extend Promotion Data Model`,
}
# {metadata.title}
In this documentation, you'll learn how to extend a data model of the Promotion Module to add a custom property.
You'll create a `Custom` data model in a module. This data model will have a `custom_name` property, which is the property you want to add to the [Promotion data model](/references/promotion/models/Promotion) defined in the Promotion Module.
You'll then learn how to:
- Link the `Custom` data model to the `Promotion` data model.
- Set the `custom_name` property when a promotion is created or updated using Medusa's API routes.
- Retrieve the `custom_name` property with the promotion's details, in custom or existing API routes.
<Note title="Tip">
Similar steps can be applied to the `Campaign` data model.
</Note>
## Step 1: Define Custom Data Model
Consider you have a Hello Module defined in the `/src/modules/hello` directory.
<Note title="Tip">
If you don't have a module, follow [this guide](!docs!/basics/modules) to create one.
</Note>
To add the `custom_name` property to the `Promotion` data model, you'll create in the Hello Module a data model that has the `custom_name` property.
Create the file `src/modules/hello/models/custom.ts` with the following content:
```ts title="src/modules/hello/models/custom.ts"
import { model } from "@medusajs/framework/utils"
export const Custom = model.define("custom", {
id: model.id().primaryKey(),
custom_name: model.text(),
})
```
This creates a `Custom` data model that has the `id` and `custom_name` properties.
<Note title="Tip">
Learn more about data models in [this guide](!docs!/data-models).
</Note>
---
## Step 2: Define Link to Promotion Data Model
Next, you'll define a module link between the `Custom` and `Promotion` data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.
<Note title="Tip">
Learn more about module links in [this guide](!docs!/module-links).
</Note>
Create the file `src/links/promotion-custom.ts` with the following content:
```ts title="src/links/promotion-custom.ts"
import { defineLink } from "@medusajs/framework/utils";
import HelloModule from "../modules/hello"
import PromotionModule from "@medusajs/medusa/promotion"
export default defineLink(
PromotionModule.linkable.promotion,
HelloModule.linkable.custom,
)
```
This defines a link between the `Promotion` and `Custom` data models. Using this link, you'll later query data across the modules, and link records of each data model.
---
## Step 3: Generate and Run Migrations
<Prerequisites
items={[
{
text: "Module must be registered in medusa-config.js",
link: "!docs!/basics/modules#4-add-module-to-configurations"
}
]}
/>
To reflect the `Custom` data model in the database, generate a migration that defines the table to be created for it.
Run the following command in your Medusa project's root:
```bash
npx medusa db:generate helloModuleService
```
Where `helloModuleService` is your module's name.
Then, run the `db:migrate` command to run the migrations and create a table in the database for the link between the `Promotion` and `Custom` data models:
```bash
npx medusa db:migrate
```
A table for the link is now created in the database. You can now retrieve and manage the link between records of the data models.
---
## Step 4: Consume promotionsCreated Workflow Hook
When a promotion is created, you also want to create a `Custom` record and set the `custom_name` property, then create a link between the `Promotion` and `Custom` records.
To do that, you'll consume the [promotionsCreated](/references/medusa-workflows/createPromotionsWorkflow#promotionsCreated) hook of the [createPromotionsWorkflow](/references/medusa-workflows/createPromotionsWorkflow). This workflow is executed in the [Create Promotion Admin API route](!api!/admin#promotions_postpromotions)
<Note title="Tip">
Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks).
</Note>
The API route accepts in its request body an `additional_data` parameter. You can pass in it custom data, which is passed to the workflow hook handler.
### Add custom_name to Additional Data Validation
To pass the `custom_name` in the `additional_data` parameter, you must add a validation rule that tells the Medusa application about this custom property.
Create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { z } from "zod"
export default defineMiddlewares({
routes: [
{
method: "POST",
matcher: "/admin/promotions",
additionalDataValidator: {
custom_name: z.string().optional(),
},
},
],
})
```
The `additional_data` parameter validation is customized using the `defineMiddlewares` utility function. In the routes middleware configuration object, the `additionalDataValidator` property accepts [Zod](https://zod.dev/) validaiton rules.
In the snippet above, you add a validation rule indicating that `custom_name` is a string that can be passed in the `additional_data` object.
<Note title="Tip">
Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data).
</Note>
### Create Workflow to Create Custom Record
You'll now create a workflow that will be used in the hook handler.
This workflow will create a `Custom` record, then link it to the promotion.
Start by creating the step that creates the `Custom` record. Create the file `src/workflows/create-custom-from-promotion/steps/create-custom.ts` with the following content:
```ts title="src/workflows/create-custom-from-promotion/steps/create-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type CreateCustomStepInput = {
custom_name?: string
}
export const createCustomStep = createStep(
"create-custom",
async (data: CreateCustomStepInput, { container }) => {
if (!data.custom_name) {
return
}
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const custom = await helloModuleService.createCustoms(data)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
}
)
```
In the step, you resolve the Hello Module's main service and create a `Custom` record.
In the compensation function that undoes the step's actions in case of an error, you delete the created record.
<Note title="Tip">
Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function).
</Note>
Then, create the workflow at `src/workflows/create-custom-from-promotion/index.ts` with the following content:
```ts title="src/workflows/create-custom-from-promotion/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { PromotionDTO } from "@medusajs/framework/types"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { createCustomStep } from "./steps/create-custom"
export type CreateCustomFromPromotionWorkflowInput = {
promotion: PromotionDTO
additional_data?: {
custom_name?: string
}
}
export const createCustomFromPromotionWorkflow = createWorkflow(
"create-custom-from-promotion",
(input: CreateCustomFromPromotionWorkflowInput) => {
const customName = transform(
{
input
},
(data) => data.input.additional_data.custom_name || ""
)
const custom = createCustomStep({
custom_name: customName
})
when(({ custom }), ({ custom }) => custom !== undefined)
.then(() => {
createRemoteLinkStep([{
[Modules.PROMOTION]: {
promotion_id: input.promotion.id
},
[HELLO_MODULE]: {
custom_id: custom.id
}
}])
})
return new WorkflowResponse({
custom
})
}
)
```
The workflow accepts as an input the created promotion and the `additional_data` parameter passed in the request. This is the same input that the `promotionsCreated` hook accepts.
In the workflow, you:
1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
2. Create the `Custom` record using the `createCustomStep`.
3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
You'll next execute the workflow in the hook handler.
### Consume Workflow Hook
You can now consume the `promotionsCreated` hook, which is executed in the `createPromotionsWorkflow` after the promotion is created.
To consume the hook, create the file `src/workflow/hooks/promotion-created.ts` with the following content:
```ts title="src/workflow/hooks/promotion-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createPromotionsWorkflow } from "@medusajs/medusa/core-flows"
import {
createCustomFromPromotionWorkflow,
CreateCustomFromPromotionWorkflowInput
} from "../create-custom-from-promotion"
createPromotionsWorkflow.hooks.promotionsCreated(
async ({ promotions, additional_data }, { container }) => {
const workflow = createCustomFromPromotionWorkflow(container)
for (let promotion of promotions) {
await workflow.run({
input: {
promotion,
additional_data
} as CreateCustomFromPromotionWorkflowInput
})
}
}
)
```
The hook handler executes the `createPromotionsWorkflow`, passing it its input.
### Test it Out
To test it out, send a `POST` request to `/admin/promotions` to create a promotion, passing `custom_name` in `additional_data`:
```bash
curl --location 'localhost:9000/admin/promotions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"additional_data": {
"custom_name": "test"
},
"code": "50OFF",
"type": "standard",
"application_method": {
"description": "My promotion",
"value": 50,
"currency_code": "usd",
"max_quantity": 1,
"type": "percentage",
"target_type": "items",
"apply_to_quantity": 0,
"buy_rules_min_quantity": 1,
"allocation": "each"
}
}'
```
Make sure to replace `{token}` with an admin user's JWT token. Learn how to retrieve it in the [API reference](!api!/admin#1-bearer-authorization-with-jwt-tokens).
The request will return the promotion's details. You'll learn how to retrive the `custom_name` property with the promotion's details in the next section.
---
## Step 5: Retrieve custom_name with Promotion Details
When you extend an existing data model through links, you also want to retrieve the custom properties with the data model.
### Retrieve in API Routes
To retrieve the `custom_name` property when you're retrieving the promotion through API routes, such as the [Get Promotion API Route](!api!/admin#promotions_getpromotionsid), pass in the `fields` query parameter `+custom.*`, which retrieves the linked `Custom` record's details.
<Note title="Tip">
The `+` prefix in `+custom.*` indicates that the relation should be retrieved with the default promotion fields. Learn more about selecting fields and relations in the [API reference](!api!/admin#select-fields-and-relations).
</Note>
For example:
```bash
curl 'localhost:9000/admin/promotions/{promotion_id}?fields=+custom.*' \
-H 'Authorization: Bearer {token}'
```
Make sure to replace `{promotion_id}` with the promotion's ID, and `{token}` with an admin user's JWT token.
Among the returned `promotion` object, you'll find a `custom` property which holds the details of the linked `Custom` record:
```json
{
"promotion": {
// ...
"custom": {
"id": "01J9NP7ANXDZ0EAYF0956ZE1ZA",
"custom_name": "test",
"created_at": "2024-10-08T09:09:06.877Z",
"updated_at": "2024-10-08T09:09:06.877Z",
"deleted_at": null
}
}
}
```
### Retrieve using Query
You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/advanced-development/module-links/query).
For example:
```ts
const { data: [promotion] } = await query.graph({
entity: "promotion",
fields: ["*", "custom.*"],
filters: {
id: promotion_id,
},
})
```
Learn more about how to use Query in [this guide](!docs!/advanced-development/module-links/query).
---
## Step 6: Consume promotionsUpdated Workflow Hook
Similar to the `promotionsCreated` hook, you'll consume the [promotionsUpdated](/references/medusa-workflows/updatePromotionsWorkflow#promotionsUpdated) hook of the [updatePromotionsWorkflow](/references/medusa-workflows/updatePromotionsWorkflow) to update `custom_name` when the promotion is updated.
The `updatePromotionsWorkflow` is executed by the [Update Promotion API route](!api!/admin#promotions_postpromotionsid), which accepts the `additional_data` parameter to pass custom data to the hook.
### Add custom_name to Additional Data Validation
To allow passing `custom_name` in the `additional_data` parameter of the update promotion route, add in `src/api/middlewares.ts` a new route middleware configuration object:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { z } from "zod"
export default defineMiddlewares({
routes: [
// ...
{
method: "POST",
matcher: "/admin/promotions/:id",
additionalDataValidator: {
custom_name: z.string().nullish(),
},
},
],
})
```
The validation schema is the similar to that of the Create Promotion API route, except you can pass a `null` value for `custom_name` to remove or unset the `custom_name`'s value.
### Create Workflow to Update Custom Record
Next, you'll create a workflow that creates, updates, or deletes `Custom` records based on the provided `additional_data` parameter:
1. If `additional_data.custom_name` is set and it's `null`, the `Custom` record linked to the promotion is deleted.
2. If `additional_data.custom_name` is set and the promotion doesn't have a linked `Custom` record, a new record is created and linked to the promotion.
3. If `additional_data.custom_name` is set and the promotion has a linked `Custom` record, the `custom_name` property of the `Custom` record is updated.
Start by creating the step that updates a `Custom` record. Create the file `src/workflows/update-custom-from-promotion/steps/update-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-promotion/steps/update-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { HELLO_MODULE } from "../../../modules/hello"
import HelloModuleService from "../../../modules/hello/service"
type UpdateCustomStepInput = {
id: string
custom_name: string
}
export const updateCustomStep = createStep(
"update-custom",
async ({ id, custom_name }: UpdateCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const prevData = await helloModuleService.retrieveCustom(id)
const custom = await helloModuleService.updateCustoms({
id,
custom_name,
})
return new StepResponse(custom, prevData)
},
async (prevData, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.updateCustoms(prevData)
}
)
```
In this step, you update a `Custom` record. In the compensation function, you revert the update.
Next, you'll create the step that deletes a `Custom` record. Create the file `src/workflows/update-custom-from-promotion/steps/delete-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-promotion/steps/delete-custom.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Custom } from "../../../modules/hello/models/custom"
import { InferTypeOf } from "@medusajs/framework/types"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type DeleteCustomStepInput = {
custom: InferTypeOf<typeof Custom>
}
export const deleteCustomStep = createStep(
"delete-custom",
async ({ custom }: DeleteCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.createCustoms(custom)
}
)
```
In this step, you delete a `Custom` record. In the compensation function, you create it again.
Finally, you'll create the workflow. Create the file `src/workflows/update-custom-from-promotion/index.ts` with the following content:
```ts title="src/workflows/update-custom-from-promotion/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { PromotionDTO } from "@medusajs/framework/types"
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, dismissRemoteLinkStep, useRemoteQueryStep } from "@medusajs/medusa/core-flows"
import { createCustomStep } from "../create-custom-from-cart/steps/create-custom"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { deleteCustomStep } from "./steps/delete-custom"
import { updateCustomStep } from "./steps/update-custom"
export type UpdateCustomFromPromotionStepInput = {
promotion: PromotionDTO
additional_data?: {
custom_name?: string | null
}
}
export const updateCustomFromPromotionWorkflow = createWorkflow(
"update-custom-from-promotion",
(input: UpdateCustomFromPromotionStepInput) => {
const promotionData = useRemoteQueryStep({
entry_point: "promotion",
fields: ["custom.*"],
variables: {
filters: {
id: input.promotion.id
}
},
list: false
})
// TODO create, update, or delete Custom record
}
)
```
The workflow accepts the same input as the `promotionsUpdated` workflow hook handler would.
In the workflow, you retrieve the promotion's linked `Custom` record using Query.
Next, replace the `TODO` with the following:
```ts title="src/workflows/update-custom-from-promotion/index.ts"
const created = when({
input,
promotionData
}, (data) =>
!data.promotionData.custom &&
data.input.additional_data?.custom_name?.length > 0
)
.then(() => {
const custom = createCustomStep({
custom_name: input.additional_data.custom_name
})
createRemoteLinkStep([{
[Modules.PROMOTION]: {
promotion_id: input.promotion.id
},
[HELLO_MODULE]: {
custom_id: custom.id
}
}])
return custom
})
// TODO update, or delete Custom record
```
Using the `when-then` utility, you check if the promotion doesn't have a linked `Custom` record and the `custom_name` property is set. If so, you create a `Custom` record and link it to the promotion.
To create the `Custom` record, you use the `createCustomStep` you created in an earlier section.
Next, replace the new `TODO` with the following:
```ts title="src/workflows/update-custom-from-promotion/index.ts"
const deleted = when({
input,
promotionData
}, (data) =>
data.promotionData.custom && (
data.input.additional_data?.custom_name === null ||
data.input.additional_data?.custom_name.length === 0
)
)
.then(() => {
deleteCustomStep({
custom: promotionData.custom
})
dismissRemoteLinkStep({
[HELLO_MODULE]: {
custom_id: promotionData.custom.id
}
})
return promotionData.custom.id
})
// TODO delete Custom record
```
Using the `when-then` utility, you check if the promotion has a linked `Custom` record and `custom_name` is `null` or an empty string. If so, you delete the linked `Custom` record and dismiss its links.
Finally, replace the new `TODO` with the following:
```ts title="src/workflows/update-custom-from-promotion/index.ts"
const updated = when({
input,
promotionData
}, (data) => data.promotionData.custom && data.input.additional_data?.custom_name?.length > 0)
.then(() => {
const custom = updateCustomStep({
id: promotionData.custom.id,
custom_name: input.additional_data.custom_name
})
return custom
})
return new WorkflowResponse({
created,
updated,
deleted
})
```
Using the `when-then` utility, you check if the promotion has a linked `Custom` record and `custom_name` is passed in the `additional_data`. If so, you update the linked `Custom` recod.
You return in the workflow response the created, updated, and deleted `Custom` record.
### Consume promotionsUpdated Workflow Hook
You can now consume the `promotionsUpdated` and execute the workflow you created.
Create the file `src/workflows/hooks/promotion-updated.ts` with the following content:
```ts title="src/workflows/hooks/promotion-updated.ts"
import { updatePromotionsWorkflow } from "@medusajs/medusa/core-flows"
import {
UpdateCustomFromPromotionStepInput,
updateCustomFromPromotionWorkflow
} from "../update-custom-from-promotion"
updatePromotionsWorkflow.hooks.promotionsUpdated(
async ({ promotions, additional_data }, { container }) => {
const workflow = updateCustomFromPromotionWorkflow(container)
for (let promotion of promotions) {
await workflow.run({
input: {
promotion,
additional_data
} as UpdateCustomFromPromotionStepInput
})
}
}
)
```
In the workflow hook handler, you execute the workflow, passing it the hook's input.
### Test it Out
To test it out, send a `POST` request to `/admin/promotions/:id` to update a promotion, passing `custom_name` in `additional_data`:
```bash
curl -X POST 'localhost:9000/admin/promotions/{promotion_id}?fields=+custom.*' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"additional_data": {
"custom_name": "test 2"
}
}'
```
Make sure to replace `{promotion_id}` with the promotion's ID, and `{token}` with the JWT token of an admin user.
The request will return the promotion's details with the updated `custom` linked record.
@@ -1,14 +1,14 @@
export const metadata = {
title: `Relations between Promotion Module and Other Modules`,
title: `Links between Promotion Module and Other Modules`,
}
# {metadata.title}
This document showcases the link modules defined between the Promotion Module and other commerce modules.
This document showcases the module links defined between the Promotion Module and other commerce modules.
## Cart Module
A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link module that builds a relationship between the `Cart`, `LineItemAdjustment`, and `Promotion` data models.
A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart`, `LineItemAdjustment`, and `Promotion` data models.
![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg)
@@ -16,6 +16,6 @@ A promotion can be applied on line items and shipping methods of a cart. Medusa
## Order Module
An order is associated with the promotion applied on it. Medusa defines a link module that builds a relationship between the `Order` and `Promotion` data models.
An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.
![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg)
@@ -6,7 +6,7 @@ export const metadata = {
# {metadata.title}
The Promotion Module is the `@medusajs/medusa/promotion` NPM package that provides promotion-related features in your Medusa and Node.js applications.
The Promotion Module provides promotion-related features in your Medusa and Node.js applications.
## How to Use the Promotion Module's Service
@@ -15,18 +15,33 @@ You can use the Promotion Module's main service by resolving from the Medusa con
For example:
<CodeTabs groupId="resource-type">
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const promotionModuleService = container.resolve(
Modules.PROMOTION
)
const promotions = await promotionModuleService.listPromotions()
})
```
</CodeTab>
<CodeTab label="API Route" value="api-route">
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
request: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const promotionModuleService: IPromotionModuleService = request.scope.resolve(
const promotionModuleService = request.scope.resolve(
Modules.PROMOTION
)
@@ -39,35 +54,17 @@ export async function GET(
</CodeTab>
<CodeTab label="Subscriber" value="subscribers">
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function subscriberHandler({ container }: SubscriberArgs) {
const promotionModuleService: IPromotionModuleService = container.resolve(
const promotionModuleService = container.resolve(
Modules.PROMOTION
)
const promotions = await promotionModuleService.listPromotions()
}
```
</CodeTab>
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { IPromotionModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const promotionModuleService: IPromotionModuleService = container.resolve(
Modules.PROMOTION
)
const promotions = await promotionModuleService.listPromotions()
})
```
</CodeTab>
@@ -98,7 +95,9 @@ const promotion = await promotionModuleService.createPromotions({
### Flexible Promotion Rules
A promotion has rules that restricts when it's applied. For example, you can create a promotion that's only applied to VIP customers.
A promotion has rules that restricts when it's applied.
For example, you can create a promotion that's only applied to VIP customers.
```ts
const promotion = await promotionModuleService.createPromotions({
+24 -17
View File
@@ -60,29 +60,26 @@ export const generatedEditDates = {
"app/commerce-modules/payment/page.mdx": "2024-10-09T10:39:37.362Z",
"app/commerce-modules/pricing/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/pricing/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/pricing/concepts/page.mdx": "2024-07-01T16:34:13+00:00",
"app/commerce-modules/pricing/examples/page.mdx": "2024-09-30T08:43:53.164Z",
"app/commerce-modules/pricing/price-calculation/page.mdx": "2024-07-26T10:09:41+03:00",
"app/commerce-modules/pricing/price-rules/page.mdx": "2024-07-01T16:34:13+00:00",
"app/commerce-modules/pricing/relations-to-other-modules/page.mdx": "2024-05-29T11:08:06+00:00",
"app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx": "2024-07-18T19:03:37+02:00",
"app/commerce-modules/pricing/page.mdx": "2024-09-30T08:43:53.164Z",
"app/commerce-modules/pricing/concepts/page.mdx": "2024-10-09T13:37:25.678Z",
"app/commerce-modules/pricing/examples/page.mdx": "2024-10-09T13:32:48.501Z",
"app/commerce-modules/pricing/price-calculation/page.mdx": "2024-10-09T13:43:14.038Z",
"app/commerce-modules/pricing/price-rules/page.mdx": "2024-10-09T13:38:47.112Z",
"app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx": "2024-10-09T13:48:23.261Z",
"app/commerce-modules/pricing/page.mdx": "2024-10-09T13:26:26.401Z",
"app/commerce-modules/product/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/product/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/product/examples/page.mdx": "2024-09-30T08:43:53.164Z",
"app/commerce-modules/product/guides/price/page.mdx": "2024-09-30T08:43:53.165Z",
"app/commerce-modules/product/guides/price-with-taxes/page.mdx": "2024-09-30T08:43:53.165Z",
"app/commerce-modules/product/relations-to-other-modules/page.mdx": "2024-06-26T07:55:59+00:00",
"app/commerce-modules/product/page.mdx": "2024-09-30T08:43:53.165Z",
"app/commerce-modules/product/examples/page.mdx": "2024-10-09T13:59:32.887Z",
"app/commerce-modules/product/guides/price/page.mdx": "2024-10-09T14:02:24.737Z",
"app/commerce-modules/product/guides/price-with-taxes/page.mdx": "2024-10-09T14:04:20.900Z",
"app/commerce-modules/product/page.mdx": "2024-10-09T13:59:11.554Z",
"app/commerce-modules/promotion/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/promotion/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/promotion/actions/page.mdx": "2024-06-26T07:55:59+00:00",
"app/commerce-modules/promotion/actions/page.mdx": "2024-10-09T14:49:01.645Z",
"app/commerce-modules/promotion/application-method/page.mdx": "2024-06-26T07:55:59+00:00",
"app/commerce-modules/promotion/campaign/page.mdx": "2024-05-29T11:08:06+00:00",
"app/commerce-modules/promotion/concepts/page.mdx": "2024-06-26T07:55:59+00:00",
"app/commerce-modules/promotion/examples/page.mdx": "2024-09-30T08:43:53.166Z",
"app/commerce-modules/promotion/relations-to-other-modules/page.mdx": "2024-05-29T11:08:06+00:00",
"app/commerce-modules/promotion/page.mdx": "2024-09-30T08:43:53.166Z",
"app/commerce-modules/promotion/concepts/page.mdx": "2024-10-09T14:50:50.255Z",
"app/commerce-modules/promotion/examples/page.mdx": "2024-10-09T14:46:47.191Z",
"app/commerce-modules/promotion/page.mdx": "2024-10-09T14:46:26.982Z",
"app/commerce-modules/region/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/region/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/region/examples/page.mdx": "2024-09-30T08:43:53.166Z",
@@ -2229,6 +2226,16 @@ export const generatedEditDates = {
"app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2024-10-08T08:05:36.596Z",
"app/commerce-modules/cart/extend/page.mdx": "2024-10-08T11:22:22.523Z",
"app/commerce-modules/cart/links-to-other-modules/page.mdx": "2024-10-08T08:22:35.190Z",
"app/commerce-modules/auth/reset-password/page.mdx": "2024-09-25T09:36:26.592Z",
"app/storefront-development/customers/reset-password/page.mdx": "2024-09-25T10:21:46.647Z",
"app/commerce-modules/customer/extend/page.mdx": "2024-10-09T14:43:37.836Z",
"app/commerce-modules/fulfillment/links-to-other-modules/page.mdx": "2024-10-08T14:58:24.935Z",
"app/commerce-modules/inventory/links-to-other-modules/page.mdx": "2024-10-08T15:18:30.109Z",
"app/commerce-modules/pricing/links-to-other-modules/page.mdx": "2024-10-09T13:51:49.986Z",
"app/commerce-modules/product/extend/page.mdx": "2024-10-09T14:43:54.303Z",
"app/commerce-modules/product/links-to-other-modules/page.mdx": "2024-10-09T14:14:09.401Z",
"app/commerce-modules/promotion/extend/page.mdx": "2024-10-09T15:17:01.513Z",
"app/commerce-modules/promotion/links-to-other-modules/page.mdx": "2024-10-09T14:51:37.194Z",
"app/commerce-modules/order/edit/page.mdx": "2024-10-09T08:50:05.334Z",
"app/commerce-modules/order/links-to-other-modules/page.mdx": "2024-10-09T11:23:05.488Z",
"app/commerce-modules/order/order-change/page.mdx": "2024-10-09T09:59:40.745Z",
+20 -12
View File
@@ -387,6 +387,10 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx",
"pathname": "/commerce-modules/pricing/examples"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx",
"pathname": "/commerce-modules/pricing/links-to-other-modules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/pricing/page.mdx",
"pathname": "/commerce-modules/pricing"
@@ -399,10 +403,6 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx",
"pathname": "/commerce-modules/pricing/price-rules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/pricing/relations-to-other-modules/page.mdx",
"pathname": "/commerce-modules/pricing/relations-to-other-modules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx",
"pathname": "/commerce-modules/pricing/tax-inclusive-pricing"
@@ -411,6 +411,10 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/commerce-modules/product/examples/page.mdx",
"pathname": "/commerce-modules/product/examples"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/product/extend/page.mdx",
"pathname": "/commerce-modules/product/extend"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx",
"pathname": "/commerce-modules/product/guides/price"
@@ -420,12 +424,12 @@ export const filesMap = [
"pathname": "/commerce-modules/product/guides/price-with-taxes"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/product/page.mdx",
"pathname": "/commerce-modules/product"
"filePath": "/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx",
"pathname": "/commerce-modules/product/links-to-other-modules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx",
"pathname": "/commerce-modules/product/relations-to-other-modules"
"filePath": "/www/apps/resources/app/commerce-modules/product/page.mdx",
"pathname": "/commerce-modules/product"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx",
@@ -448,12 +452,16 @@ export const filesMap = [
"pathname": "/commerce-modules/promotion/examples"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/promotion/page.mdx",
"pathname": "/commerce-modules/promotion"
"filePath": "/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx",
"pathname": "/commerce-modules/promotion/extend"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/promotion/relations-to-other-modules/page.mdx",
"pathname": "/commerce-modules/promotion/relations-to-other-modules"
"filePath": "/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx",
"pathname": "/commerce-modules/promotion/links-to-other-modules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/promotion/page.mdx",
"pathname": "/commerce-modules/promotion"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/region/examples/page.mdx",
+23 -7
View File
@@ -4637,8 +4637,8 @@ export const generatedSidebar = [
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/pricing/relations-to-other-modules",
"title": "Relation to Modules",
"path": "/commerce-modules/pricing/links-to-other-modules",
"title": "Links to Other Modules",
"children": []
}
]
@@ -5117,6 +5117,14 @@ export const generatedSidebar = [
"title": "Examples",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/product/extend",
"title": "Extend Module",
"children": []
},
{
"loaded": true,
"isPathHref": true,
@@ -5127,8 +5135,8 @@ export const generatedSidebar = [
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/product/relations-to-other-modules",
"title": "Relation to Modules",
"path": "/commerce-modules/product/links-to-other-modules",
"title": "Links to Other Modules",
"children": []
}
]
@@ -5145,7 +5153,7 @@ export const generatedSidebar = [
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/product/guides/price",
"title": "Get Product Variant Prices",
"title": "Get Variant Prices",
"children": []
},
{
@@ -5864,6 +5872,14 @@ export const generatedSidebar = [
"title": "Examples",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/promotion/extend",
"title": "Extend Module",
"children": []
},
{
"loaded": true,
"isPathHref": true,
@@ -5906,8 +5922,8 @@ export const generatedSidebar = [
"loaded": true,
"isPathHref": true,
"type": "link",
"path": "/commerce-modules/promotion/relations-to-other-modules",
"title": "Relation to Modules",
"path": "/commerce-modules/promotion/links-to-other-modules",
"title": "Links to Modules",
"children": []
}
]
+15
View File
@@ -74,6 +74,21 @@ const nextConfig = {
destination: "/commerce-modules/inventory/links-to-other-modules",
permanent: true,
},
{
source: "/commerce-modules/pricing/relations-to-other-modules",
destination: "/commerce-modules/pricing/links-to-other-modules",
permanent: true,
},
{
source: "/commerce-modules/product/relations-to-other-modules",
destination: "/commerce-modules/product/links-to-other-modules",
permanent: true,
},
{
source: "/commerce-modules/promotion/relations-to-other-modules",
destination: "/commerce-modules/promotion/links-to-other-modules",
permanent: true,
},
]
},
// Redirects shouldn't be necessary anymore since we have remark / rehype
+16 -6
View File
@@ -851,8 +851,8 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
{
type: "link",
path: "/commerce-modules/pricing/relations-to-other-modules",
title: "Relation to Modules",
path: "/commerce-modules/pricing/links-to-other-modules",
title: "Links to Other Modules",
},
],
},
@@ -909,14 +909,19 @@ export const sidebar = sidebarAttachHrefCommonOptions([
path: "/commerce-modules/product/examples",
title: "Examples",
},
{
type: "link",
path: "/commerce-modules/product/extend",
title: "Extend Module",
},
{
type: "sub-category",
title: "Concepts",
children: [
{
type: "link",
path: "/commerce-modules/product/relations-to-other-modules",
title: "Relation to Modules",
path: "/commerce-modules/product/links-to-other-modules",
title: "Links to Other Modules",
},
],
},
@@ -978,6 +983,11 @@ export const sidebar = sidebarAttachHrefCommonOptions([
path: "/commerce-modules/promotion/examples",
title: "Examples",
},
{
type: "link",
path: "/commerce-modules/promotion/extend",
title: "Extend Module",
},
{
type: "sub-category",
title: "Concepts",
@@ -1004,8 +1014,8 @@ export const sidebar = sidebarAttachHrefCommonOptions([
},
{
type: "link",
path: "/commerce-modules/promotion/relations-to-other-modules",
title: "Relation to Modules",
path: "/commerce-modules/promotion/links-to-other-modules",
title: "Links to Modules",
},
],
},