docs: improved commerce modules [4/n] (#9517)
Improve pricing, product, and promotion modules docs [4/n]
This commit is contained in:
@@ -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, you’ll learn about the main concepts in the Pricing Module, and how data is stored and related.
|
||||
In this document, you’ll 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).
|
||||
|
||||

|
||||
|
||||
@@ -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, you’ll 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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 variant’s prices are stored as prices belonging to a price set.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
For exmaple, you create a price restricted to `10557` zip codes.
|
||||
|
||||
A price can have multiple price rules. For example:
|
||||

|
||||
|
||||

|
||||
A price can have multiple price rules.
|
||||
|
||||
For example, a price can be restricted by a region and a zip code.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Product Module
|
||||
|
||||
A product variant’s 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`.
|
||||
|
||||

|
||||
|
||||
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, you’ll learn about tax-inclusive pricing and how it's used during prices calculation.
|
||||
In this document, you’ll 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 product’s 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 product’s 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 attribute’s 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 region’s price preference’s `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 region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive.
|
||||
|
||||
@@ -13,13 +13,12 @@ In this guide, you’ll 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 variant’s prices are stored belonging to a price set.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Pricing Module
|
||||
|
||||
A product variant’s 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
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, you’ll learn about promotion actions and how they’re computed and used.
|
||||
In this document, you’ll learn about promotion actions and how they’re 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, you’ll 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 `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 rule’s
|
||||
|
||||
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`.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -13,16 +13,15 @@ In this document, you’ll 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.
|
||||
+4
-4
@@ -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.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user