From f6d3453e6d571797d5d6fe73b90a495b1b5d4b0f Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 16 Oct 2024 12:34:36 +0300 Subject: [PATCH] docs: improved commerce modules [4/n] (#9517) Improve pricing, product, and promotion modules docs [4/n] --- .../commerce-modules/customer/extend/page.mdx | 16 +- .../pricing/concepts/page.mdx | 10 +- .../pricing/examples/page.mdx | 47 +- .../pricing/links-to-other-modules/page.mdx | 29 + .../app/commerce-modules/pricing/page.mdx | 59 +- .../pricing/price-calculation/page.mdx | 20 +- .../pricing/price-rules/page.mdx | 14 +- .../relations-to-other-modules/page.mdx | 23 - .../pricing/tax-inclusive-pricing/page.mdx | 18 +- .../product/examples/page.mdx | 54 +- .../commerce-modules/product/extend/page.mdx | 684 +++++++++++++++++ .../product/guides/price-with-taxes/page.mdx | 14 +- .../product/guides/price/page.mdx | 8 +- .../product/links-to-other-modules/page.mdx | 39 + .../app/commerce-modules/product/page.mdx | 53 +- .../relations-to-other-modules/page.mdx | 53 -- .../promotion/actions/page.mdx | 10 +- .../promotion/concepts/page.mdx | 20 +- .../promotion/examples/page.mdx | 36 +- .../promotion/extend/page.mdx | 696 ++++++++++++++++++ .../page.mdx | 8 +- .../app/commerce-modules/promotion/page.mdx | 57 +- www/apps/resources/generated/edit-dates.mjs | 41 +- www/apps/resources/generated/files-map.mjs | 32 +- www/apps/resources/generated/sidebar.mjs | 30 +- www/apps/resources/next.config.mjs | 15 + www/apps/resources/sidebar.mjs | 22 +- 27 files changed, 1767 insertions(+), 341 deletions(-) create mode 100644 www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx delete mode 100644 www/apps/resources/app/commerce-modules/pricing/relations-to-other-modules/page.mdx create mode 100644 www/apps/resources/app/commerce-modules/product/extend/page.mdx create mode 100644 www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx delete mode 100644 www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx create mode 100644 www/apps/resources/app/commerce-modules/promotion/extend/page.mdx rename www/apps/resources/app/commerce-modules/promotion/{relations-to-other-modules => links-to-other-modules}/page.mdx (65%) diff --git a/www/apps/resources/app/commerce-modules/customer/extend/page.mdx b/www/apps/resources/app/commerce-modules/customer/extend/page.mdx index 2a84396158..6bbf7a358b 100644 --- a/www/apps/resources/app/commerce-modules/customer/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/extend/page.mdx @@ -18,7 +18,7 @@ You'll then learn how to: -Similar steps can bee applied to the `CustomerAddress` data model. +Similar steps can be applied to the `CustomerAddress` data model. @@ -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" diff --git a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx index 5fe0b50c82..6b59a52f6e 100644 --- a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx @@ -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). ![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) @@ -16,6 +18,8 @@ A [PriceSet](/references/pricing/models/PriceSet) represents a collection of pri ## Price List -A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` properties, which indicate the date range in which a price list can be applied. +A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. + +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. Its associated prices are represented by the `Price` data model. \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx b/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx index 0e0094122c..2d37576498 100644 --- a/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx @@ -13,16 +13,15 @@ In this document, you’ll find common examples of how you can use the Pricing M - ```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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) @@ -84,16 +83,15 @@ export async function POST(request: Request) { - ```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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) @@ -130,16 +128,15 @@ export async function GET(request: Request) { - ```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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) @@ -176,16 +173,15 @@ export async function GET(request: Request) { - ```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 { - 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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) @@ -330,16 +326,15 @@ export async function POST(request: Request) { - ```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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) diff --git a/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx new file mode 100644 index 0000000000..debc6df062 --- /dev/null +++ b/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx @@ -0,0 +1,29 @@ +export const metadata = { + title: `Links between Pricing Module and Other Modules`, +} + +# {metadata.title} + +This document showcases the module links defined between the Pricing Module and other commerce modules. + +## Fulfillment Module + +The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. + +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +--- + +## Product Module + +The Product Module doesn't store or manage the prices of product variants. + +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. + +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) + +So, when you want to add prices for a product variant, you create a price set and add the prices to it. + +You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. diff --git a/www/apps/resources/app/commerce-modules/pricing/page.mdx b/www/apps/resources/app/commerce-modules/pricing/page.mdx index 6819e56e4b..4f22de8385 100644 --- a/www/apps/resources/app/commerce-modules/pricing/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/page.mdx @@ -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: + + +```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() +}) +``` + + - ```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 { - const pricingModuleService: IPricingModuleService = request.scope.resolve( + const pricingModuleService = request.scope.resolve( Modules.PRICING ) @@ -39,35 +54,17 @@ export async function GET( - ```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() } -``` - - - - - ```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() -}) ``` @@ -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([ diff --git a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx index 406b80235d..a0c2fe96d8 100644 --- a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx @@ -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: + +Learn more about the returned properties in [this guide](../price-calculation/page.mdx#returned-price-object). + + + - `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. diff --git a/www/apps/resources/app/commerce-modules/product/examples/page.mdx b/www/apps/resources/app/commerce-modules/product/examples/page.mdx index e9d63f0dce..e1e514ec6a 100644 --- a/www/apps/resources/app/commerce-modules/product/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/examples/page.mdx @@ -13,13 +13,12 @@ In this guide, you’ll find common examples of how you can use the Product Modu - ```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) { - ```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) { - ```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( - ```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) { - ```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) { - ```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 ) diff --git a/www/apps/resources/app/commerce-modules/product/extend/page.mdx b/www/apps/resources/app/commerce-modules/product/extend/page.mdx new file mode 100644 index 0000000000..9046c43098 --- /dev/null +++ b/www/apps/resources/app/commerce-modules/product/extend/page.mdx @@ -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. + + + +Similar steps can be applied to the `ProductVariant` or `ProductOption` data models. + + + +## Step 1: Define Custom Data Model + +Consider you have a Hello Module defined in the `/src/modules/hello` directory. + + + +If you don't have a module, follow [this guide](!docs!/basics/modules) to create one. + + + +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. + + + +Learn more about data models in [this guide](!docs!/data-models). + + + +--- + +## 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. + + + +Learn more about module links in [this guide](!docs!/module-links). + + + +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 + + + +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) + + + +Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks). + + + +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. + + + +Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data). + + + +### 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. + + + +Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function). + + + +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. + + + +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). + + + +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 +} + +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. diff --git a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx index a1e91676a5..d4903742cc 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx @@ -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: + + +Learn more about retrieving product variants' prices for a context in [this guide](../price/page.mdx). + + + ```ts import { QueryContext } from "@medusajs/framework/utils" @@ -63,12 +69,6 @@ const { data: products } = await query.graph({ }) ``` - - -Learn more about retrieving product variants' prices for a context in [this guide](../price/page.mdx). - - - --- ## Step 2: Get Tax Lines for Products diff --git a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx index 84aa982c36..e69b068af8 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx @@ -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). @@ -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" // ... diff --git a/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx new file mode 100644 index 0000000000..d4c39b7f43 --- /dev/null +++ b/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx @@ -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. + +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) + +So, to add prices for a product variant, create a price set and add the prices to it. + +--- + +## Sales Channel Module + +The Sales Channel Module provides functionalities to manage multiple selling channels in your store. + +Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. + +![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) + +--- + +## Inventory Module + +The Inventory Module provides inventory-management features for any stock-kept item. + +Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. + +![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) + +When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. diff --git a/www/apps/resources/app/commerce-modules/product/page.mdx b/www/apps/resources/app/commerce-modules/product/page.mdx index 5b73a2fcc6..0336b23f5d 100644 --- a/www/apps/resources/app/commerce-modules/product/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/page.mdx @@ -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: + + +```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() +}) +``` + + - ```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) { - ```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() } -``` - - - - - ```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() -}) ``` diff --git a/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx deleted file mode 100644 index bab8fcd996..0000000000 --- a/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx +++ /dev/null @@ -1,53 +0,0 @@ -export const metadata = { - title: `Relations between Product Module and Other Modules`, -} - -# {metadata.title} - -This document showcases the link modules defined between the Product Module and other commerce modules. - -## Cart Module - -A cart's line item is associated with a product and its variant. Medusa defines a link module that builds a relationship between the `Cart`, `Product`, and `ProductVariant` data models. - -![A diagram showcasing an example of how data models from the Cart and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716546229/Medusa%20Resources/cart-product_x82x9j.jpg) - ---- - -## Order Module - -An order's line item is associated with the purchased product and its variant. Medusa defines a link module that builds a relationship between the `LineItem`, `Product`, and `ProductVariant` data models. - -![A diagram showcasing an example of how data models from the Order and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716556100/Medusa%20Resources/order-product_l6ylte.jpg) - ---- - -## Pricing Module - -A product 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. - -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) - -So, to add prices for a product variant, create a price set and add the prices as money amounts to it. - -Learn more about the `PriceSet` data model in the [Pricing Concepts](../../pricing/concepts/page.mdx#price-list) - ---- - -## Sales Channel Module - -A product can have different availability in different sales channels. Medusa defines a link module that builds a relationship between the `Product` and `SalesChannel` data models. - -![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) - ---- - -## Inventory Module - -Each product variant has different inventory details. Medusa defines a link module that builds a relationship between the `ProductVariant` and `InventoryItem` data models. - -![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) - -When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. - -Learn more about the `InventoryItem` data model in the [Inventory Concepts](../../inventory/concepts/page.mdx#inventoryitem) \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx b/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx index ef8578bb95..81887ca586 100644 --- a/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx b/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx index b7706bccac..e13dfd151b 100644 --- a/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx @@ -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: +--- + ## PromotionRule -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](/references/promotion/models/PromotionRule). For example, you can create a promotion that only customers of the `VIP` customer group can use. +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](/references/promotion/models/PromotionRule). + +For example, you can create a promotion that only customers of the `VIP` customer group can use. ![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) -A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. -When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. +For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. + +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. + +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. --- @@ -90,8 +98,8 @@ The `PromotionRule`'s `operator` property adds more flexibility to the 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`. ![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) diff --git a/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx b/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx index 3c408044bc..f25901ff83 100644 --- a/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx @@ -13,16 +13,15 @@ In this document, you’ll find common examples of how you can use the Promotion - ```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 { - const promotionModuleService: IPromotionModuleService = request.scope.resolve( + const promotionModuleService = request.scope.resolve( Modules.PROMOTION ) @@ -78,16 +77,15 @@ export async function POST(request: Request) { - ```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 { - const promotionModuleService: IPromotionModuleService = request.scope.resolve( + const promotionModuleService = request.scope.resolve( Modules.PROMOTION ) @@ -135,16 +133,15 @@ export async function POST(request: Request) { - ```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 { - const promotionModuleService: IPromotionModuleService = request.scope.resolve( + const promotionModuleService = request.scope.resolve( Modules.PROMOTION ) @@ -213,16 +210,15 @@ export async function POST(request: Request) { - ```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 { - const promotionModuleService: IPromotionModuleService = request.scope.resolve( + const promotionModuleService = request.scope.resolve( Modules.PROMOTION ) diff --git a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx new file mode 100644 index 0000000000..1d701d1b5b --- /dev/null +++ b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx @@ -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. + + + +Similar steps can be applied to the `Campaign` data model. + + + +## Step 1: Define Custom Data Model + +Consider you have a Hello Module defined in the `/src/modules/hello` directory. + + + +If you don't have a module, follow [this guide](!docs!/basics/modules) to create one. + + + +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. + + + +Learn more about data models in [this guide](!docs!/data-models). + + + +--- + +## 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. + + + +Learn more about module links in [this guide](!docs!/module-links). + + + +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 + + + +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) + + + +Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks). + + + +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. + + + +Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data). + + + +### 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. + + + +Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function). + + + +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. + + + +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). + + + +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 +} + +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. diff --git a/www/apps/resources/app/commerce-modules/promotion/relations-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx similarity index 65% rename from www/apps/resources/app/commerce-modules/promotion/relations-to-other-modules/page.mdx rename to www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx index 836f1c4986..c81834fbee 100644 --- a/www/apps/resources/app/commerce-modules/promotion/relations-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx @@ -1,14 +1,14 @@ export const metadata = { - title: `Relations between Promotion Module and Other Modules`, + title: `Links between Promotion Module and Other Modules`, } # {metadata.title} -This document showcases the link modules defined between the Promotion Module and other commerce modules. +This document showcases the module links defined between the Promotion Module and other commerce modules. ## Cart Module -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link module that builds a relationship between the `Cart`, `LineItemAdjustment`, and `Promotion` data models. +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart`, `LineItemAdjustment`, and `Promotion` data models. ![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) @@ -16,6 +16,6 @@ A promotion can be applied on line items and shipping methods of a cart. Medusa ## Order Module -An order is associated with the promotion applied on it. Medusa defines a link module that builds a relationship between the `Order` and `Promotion` data models. +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. ![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/promotion/page.mdx b/www/apps/resources/app/commerce-modules/promotion/page.mdx index 607c447897..fdee4fcbfa 100644 --- a/www/apps/resources/app/commerce-modules/promotion/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/page.mdx @@ -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: + + +```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() +}) +``` + + - ```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 { - const promotionModuleService: IPromotionModuleService = request.scope.resolve( + const promotionModuleService = request.scope.resolve( Modules.PROMOTION ) @@ -39,35 +54,17 @@ export async function GET( - ```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() } -``` - - - - - ```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() -}) ``` @@ -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({ diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 4eb5ed9653..2b92c1526d 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -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", diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 526821c7ab..528b834ace 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -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", diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index 8593be8bce..94464b7f76 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -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": [] } ] diff --git a/www/apps/resources/next.config.mjs b/www/apps/resources/next.config.mjs index a01c1b11db..0949f0067a 100644 --- a/www/apps/resources/next.config.mjs +++ b/www/apps/resources/next.config.mjs @@ -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 diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 95b36e3bd4..699051adbc 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -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", }, ], },