docs: added documentation pages for experimental features (#5671)

* docs: added documentation pages for experimental features

* fix content lint issues

* fixed lint errors

* added migration step

* added workflows introduction

* add installation guides

* added installation guides for modules + generated workflows reference

* added missing workflows reference link

* Added warning message for experimental features

* fix note
This commit is contained in:
Shahed Nasser
2023-11-27 16:49:12 +00:00
committed by GitHub
parent cf0939aab2
commit cdc1da5df7
148 changed files with 14225 additions and 435 deletions

View File

@@ -0,0 +1,23 @@
# Experimental Features
This section of the documentation includes features that are currently experimental.
As Medusa moves towards modularization, commerce concepts such as Products or Pricing will be moved to isolated commerce modules shipped as NPM packages. This significantly changes Medusa's architecture, making it more flexible for custom digital commerce applications.
## Enabling Experimental Features
:::danger[Production Warning]
All features guarded by the `medusa_v2` flag are not ready for production and will cause unexpected issues in your production server.
:::
Experimental Features are guarded in the Medusa backend by a feature flag.
To use them, [enable the `medusa_v2` feature flag](../development/feature-flags/toggle.md) in your backend.
Then, run migrations in your backend with the following command:
```bash
npx medusa migrations run
```

View File

@@ -0,0 +1,83 @@
# Pricing Concepts
In this document, youll learn about the main concepts in the Pricing module, and how data is stored and related.
## Money Amount
A `MoneyAmount` represents a price.
Money amounts can be conditioned by the `min_quantity` and `max_quantity` attributes, which are helpful when calculating the price for a specific quantity.
If a money amount has its `min_quantity` or `max_quantity` attributes set, theyre only considered for the price calculation if they have a lower `min_quantity` or a higher `max_quantity` than the quantity specified for calculation.
---
## Price Set
A `PriceSet` represents a collection of money amounts that are linked to a resource (for example, a product or a shipping option). The Price Set and Money Amount relationship is represented by the `PriceSetMoneyAmount` entity.
![A diagram showcasing the relation between the price set and money amount](https://res.cloudinary.com/dza7lstvk/image/upload/v1700573983/Medusa%20Docs/Diagrams/price-set-money-amount_xpwria.jpg)
---
## Prices with Rules
### Rule Type
Each money amount within a price set can be a price thats applied for different conditions. These conditions are represented as rule types.
A `RuleType` defines custom conditions. Each rule type has a unique `rule_attribute`, referenced in rule values, such as when setting a rule of a money amount.
### Price Rule
Each rule of a money amount within a price set is represented by the `PriceRule` entity, which holds the value of a rule type. The `PriceSetMoneyAmount` has a `number_rules` attribute, which indicates how many rules, represented by `PriceRule`, are applied to the money amount.
![A diagram showcasing the relation between the PriceRule, PriceSet, MoneyAmount, RuleType, and PriceSetMoneyAmount](https://res.cloudinary.com/dza7lstvk/image/upload/v1700573982/Medusa%20Docs/Diagrams/price-rule-1_mcnnts.jpg)
For example, you can create a `zip_code` rule type. Then, a money amount within the price set can have the rule value `zip_code: 10557`, indicating that the money amount can only be applied within the `10557` zip code.
Each money amount within the price set can have different values for the same rule type.
For example, this diagram showcases two money amounts having different values for the same rule type:
![A diagram showcasing the relation between the PriceRule, PriceSet, MoneyAmount, RuleType, and PriceSetMoneyAmount with two money amounts.](https://res.cloudinary.com/dza7lstvk/image/upload/v1700574156/Medusa%20Docs/Diagrams/price-rule-2_ejxale.jpg)
Each money amount can have multiple rules applied to it as well.
For example, a money amount can have the rules `zip_code` and `region_id` applied to it. In this case, the value of each rule is represented by a `PriceRule`.
![A diagram showcasing the relation between the PriceRule, PriceSet, MoneyAmount, RuleType, and PriceSetMoneyAmount with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1700574174/Medusa%20Docs/Diagrams/price-rule-3_j4kdfu.jpg)
### PriceSetRuleType
The `PriceSetRuleType` entity indicates what rules the money amounts can have within a price set. It creates a relation between the `PriceSet` and `RuleType` entities.
For example, to use the `zip_code` rule type on a money amount in a price set, the rule type must first be enabled on the price set through the `PriceSetRuleType`.
![A diagram showcasing the relation between the PriceSet, PriceRule, MoneyAmount, PriceSetMoneyAmount, RuleType, and PriceSetRuleType](https://res.cloudinary.com/dza7lstvk/image/upload/v1700573984/Medusa%20Docs/Diagrams/price-set-rule-type_e1l0ef.jpg)
---
## Price List
A `PriceList` is a group of prices only enabled if their rules are satisfied. A price list has optional `start_date` and `end_date` attributes, which indicate the date range in which a price list can be applied.
Its associated prices are represented by the `PriceSetMoneyAmount` entity, which is used to store the money amounts of a price set.
Each rule that can be applied to a price list is represented by the `PriceListRule` entity. The `number_rules` attribute of a `PriceList` indicates how many rules are applied to it.
Each rule of a price list can have more than one value, representing its values by the `PriceListRuleValue` entity.
![A diagram showcasing the relation between the PriceSet, PriceList, MoneyAmount, PriceSetMoneyAmount, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1700574138/Medusa%20Docs/Diagrams/price-list_mtcanv.jpg)
---
## Use Case Example: Pricing and Product Modules
In a real use case, you would use the pricing module with your custom logic or other Medusa Commerce Modules, such as the Product Module.
When used with the Product Module, a product variants prices are stored as money amounts belonging to a price set. A relation is formed between the `ProductVariant` and the `PriceSet` when the modules are linked.
![A diagram showcasing an example of how resources 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/v1700574189/Medusa%20Docs/Diagrams/pricing-product_jcsjt0.jpg)
So, when you want to add prices for a product variant, you create a price set and add the prices as money amounts 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.

View File

@@ -0,0 +1,251 @@
# Examples of Pricing Module
In this document, youll find common examples of how you can use the Pricing module in your application.
:::note
Examples in this section are in the context of a Next.js App Router.
:::
## Create a Price Set
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function POST(request: Request) {
const pricingService = await initializePricingModule()
const body = await request.json()
const priceSet = await pricingService.create([
{
prices: [
{
currency_code: body.currency_code,
amount: body.amount,
},
],
},
])
return NextResponse.json({ price_set: priceSet })
}
```
## List Price Sets
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function GET(request: Request) {
const pricingService = await initializePricingModule()
const priceSets = await pricingService.list()
return NextResponse.json({ price_sets: priceSets })
}
```
## Retrieve a Price Set by its ID
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
type ContextType = {
params: {
id: string
}
}
export async function GET(
request: Request,
{ params }: ContextType
) {
const pricingService = await initializePricingModule()
const priceSet = await pricingService.retrieve(params.id)
return NextResponse.json({ price_set: priceSet })
}
```
## Create a Rule Type
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function POST(request: Request) {
const pricingService = await initializePricingModule()
const body = await request.json()
const ruleTypes = await pricingService.createRuleTypes([
{
name: body.name,
rule_attribute: body.rule_attribute,
},
])
return NextResponse.json({ rule_types: ruleTypes })
}
```
## Add Prices with Rules
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function POST(request: Request) {
const pricingService = await initializePricingModule()
const body = await request.json()
const priceSet = await pricingService.addPrices({
priceSetId: body.price_set_id,
prices: [
{
amount: 500,
currency_code: "USD",
rules: {
region_id: body.region_id,
},
},
],
})
return NextResponse.json({ price_set: priceSet })
}
```
## Create a Currency
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function POST(request: Request) {
const pricingService = await initializePricingModule()
const body = await request.json()
const currencies = await pricingService.createCurrencies([{
code: "EUR",
symbol: "€",
symbol_native: "€",
name: "Euro",
}])
return NextResponse.json({ currencies })
}
```
## List Currencies
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function GET(request: Request) {
const pricingService = await initializePricingModule()
const currencies = await pricingService.listCurrencies()
return NextResponse.json({ currencies })
}
```
## Create Price List
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
export async function POST(request: Request) {
const pricingService = await initializePricingModule()
const priceLists = await pricingService.createPriceLists({
title: "My Sale",
type: "sale",
starts_at: Date.parse("01/10/2023"),
ends_at: Date.parse("31/10/2023"),
rules: {
region_id: ["DE", "DK"],
},
prices: [
{
amount: 400,
currency_code: "EUR",
price_set_id: priceSet.id,
},
],
})
return NextResponse.json({ price_lists: priceLists })
}
```
## Calculate Prices For a Currency
```ts
import { NextResponse } from "next/server"
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
type ContextType = {
params: {
id: string
currency_code: string
}
}
export async function GET(
request: Request,
{ params }: ContextType
) {
const pricingService = await initializePricingModule()
const price = await pricingService.calculatePrices({
id: [params.id],
}, {
context: {
currency_code: params.currency_code,
},
})
return NextResponse.json({ price })
}
```
---
## More Examples
The [module interface reference](../../references/pricing/interfaces/IPricingModuleService.mdx) provides a reference to all the methods available for use with examples for each.

View File

@@ -0,0 +1,164 @@
import DocCard from '@theme/DocCard'
import Icons from '@theme/Icon'
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Install Pricing Module in Medusa
In this document, you'll learn how to install the Pricing module using NPM in the Medusa backend.
## Step 1: Install Module
To install the Pricing module, run the following command in the root directory of the Medusa backend:
```bash npm2yarn
npm install @medusajs/pricing
```
---
## Step 2: Add Module to Configurations
In `medusa-config.js`, add the pricing module to the exported object under the `modules` property:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
pricingService: {
resolve: "@medusajs/pricing",
},
},
}
```
---
## Step 3: Run Migrations
Run the following command to reflect schema changes into your database:
```bash
npx medusa migrations run
```
---
## Use the Module
You can now start using the module's `PricingModuleService` by resolving it through [dependency injection](../../development/fundamentals/dependency-injection.md).
For example:
<Tabs groupId="resource-type" isCodeTabs={true}>
<TabItem value="api-route" label="API Route" attributes={{
title: "src/api/store/custom/route.ts"
}} default>
```ts
import type {
MedusaRequest,
MedusaResponse
} from "@medusajs/medusa";
import {
PricingModuleService
} from "@medusajs/pricing"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const pricingModuleService = req.scope.resolve(
"pricingModuleService"
)
return res.json({
pricings: pricingModuleService.list()
})
}
```
</TabItem>
<TabItem value="subscribers" label="Subscribers" attributes={{
title: "src/subscribers/create-customer.ts"
}}>
```ts
import {
type SubscriberConfig,
type SubscriberArgs,
PricingService,
} from "@medusajs/medusa"
import {
PricingModuleService
} from "@medusajs/pricing"
export default async function handleListPriceSets({
data, eventName, container, pluginOptions
}: SubscriberArgs<Customer>) {
const pricingModuleService = container.resolve(
"pricingModuleService"
)
console.log(await pricingModuleService.list())
}
export const config: SubscriberConfig = {
event: PricingService.Events.CREATED,
context: {
subscriberId: "list-pricings"
}
}
```
</TabItem>
<TabItem value="service" label="Service" attributes={{
title: "src/service/hello.ts"
}}>
```ts
import { TransactionBaseService } from "@medusajs/medusa"
import {
PricingModuleService
} from "@medusajs/pricing"
class HelloService extends TransactionBaseService {
private pricingModuleService: PricingModuleService
constructor(container) {
super(container)
this.pricingModuleService = container.pricingModuleService
}
await listPriceSets() {
return await this.pricingModuleService.list()
}
}
export default HelloService
```
</TabItem>
</Tabs>
:::tip
In the Examples or API Reference guides, you may see an initialization of the pricing module. This is only necessary if you're using the module outside the Medusa backend.
:::
---
## Up Next
<DocCard item={{
type: 'link',
href: '/references/pricing',
label: 'Service Interface Reference',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Find a full reference of the module\' service interface\s methods.'
}
}}
/>

View File

@@ -0,0 +1,188 @@
# Install in Node.js-Based Application
In this document, youll learn how to setup and use the Pricing module in a Node.js based application.
## Prerequisites
Before installing the Pricing module in your application, make sure you have the following prerequisites:
- Node.js v16 or greater
- PostgreSQL database. You can use an existing Medusa database, or set up a new PostgreSQL database.
---
## Install Package
In your Node.js-based applications, such as a Next.js application, you can install the Pricing module with the following command:
```bash npm2yarn
npm install @medusajs/pricing
```
---
## Add Database Configuration
Add the following environment variable to your application:
```bash
POSTGRES_URL=<DATABASE_URL>
```
Where `<DATABASE_URL>` is your database connection URL of the format `postgres://[user][:password]@[host][:port]/[dbname]`. You can learn more about the connection URL format in [this guide](../../development/backend/configurations.md#database_url).
You can also set the following optional environment variables:
- `POSTGRES_SCHEMA`: a string indicating the PostgreSQL schema to use. By default, it's `public`.
- `POSTGRES_DRIVER_OPTIONS`: a stringified JSON object indicating the PostgreSQL options to use. The JSON object is then parsed to be used as a JavaScript object. By default, it's `{"connection":{"ssl":false}}` for local PostgreSQL databases, and `{"connection":{"ssl":{"rejectUnauthorized":false}}}` for remote databases.
:::note
If `POSTGRES_DRIVER_OPTIONS` is not specified, the PostgreSQL database is considered local if `POSTGRES_URL` includes `localhost`. Otherwise, it's considered remote.
:::
### Run Database Migrations
:::note
You can skip this step if you use an existing Medusa database.
:::
Migrations are used to create your database schema. Before you can run migrations, add in your `package.json` the following scripts:
```json
"scripts": {
//...other scripts
"price:migrations:run": "medusa-pricing-migrations-up",
"price:seed": "medusa-pricing-seed ./pricing-seed-data.js"
},
```
The first command runs the migrations, and the second command allows you to seed your database with demo prices optionally.
However, youd need the following seed file added to the root of your project directory:
<Details>
<Summary>Seed file</Summary>
```js
const currenciesData = [
{
code: "USD",
symbol: "$",
symbol_native: "$",
name: "US Dollar",
},
{
code: "CAD",
symbol: "CA$",
symbol_native: "$",
name: "Canadian Dollar",
},
{
code: "EUR",
symbol: "€",
symbol_native: "€",
name: "Euro",
},
]
const moneyAmountsData = [
{
id: "money-amount-USD",
currency_code: "USD",
amount: 500,
min_quantity: 1,
max_quantity: 10,
},
{
id: "money-amount-EUR",
currency_code: "EUR",
amount: 400,
min_quantity: 1,
max_quantity: 5,
},
{
id: "money-amount-CAD",
currency_code: "CAD",
amount: 600,
min_quantity: 1,
max_quantity: 8,
},
]
const priceSetsData = [
{
id: "price-set-USD",
},
{
id: "price-set-EUR",
},
{
id: "price-set-CAD",
},
]
const priceSetMoneyAmountsData = [
{
title: "USD Price Set",
price_set: "price-set-USD",
money_amount: "money-amount-USD",
},
{
title: "EUR Price Set",
price_set: "price-set-EUR",
money_amount: "money-amount-EUR",
},
]
module.exports = {
currenciesData,
moneyAmountsData,
priceSetsData,
priceSetMoneyAmountsData,
}
```
</Details>
Then run the commands you added to migrate the database schema and optionally seed data:
```bash npm2yarn
npm run price:migrations:run
# optionally
npm run price:seed
```
---
## Next.js Application: Adjust Configurations
The Pricing module uses dependencies that arent Webpack optimized. Since Next.js uses Webpack for compilation, you need to add the Pricing module as an external dependency.
To do that, add the `serverComponentsExternalPackages` option in `next.config.js`:
```js title=next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: [
"@medusajs/pricing",
],
},
}
module.exports = nextConfig
```
---
## Start Development
You can refer to the [Example Usages documentation page](./examples.md) for examples of using the Pricing module.
You can also refer to the [Module Interface Reference](../../references/pricing/interfaces/IPricingModuleService.mdx) for a detailed reference on all available methods.

View File

@@ -0,0 +1,122 @@
import DocCard from '@theme/DocCard'
import Icons from '@theme/Icon'
# Pricing Module Overview
The Pricing module is the `@medusajs/pricing` NPM package that provides advanced pricing features in your Medusa and Node.js applications. It can be used to add prices to any resource, such as products or shipping options.
## Features
### Price Management
With the Product module, you can store the prices of a resource and manage them through the main interface method. 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.
```ts
const priceSet = await pricingService.create({
rules: [],
prices: [
{
amount: 500,
currency_code: "USD",
rules: {},
},
{
amount: 400,
currency_code: "EUR",
min_quantity: 0,
max_quantity: 4,
rules: {},
},
],
})
```
### Advanced Rule Engine
You can create custom rules and apply them to prices. This gives you more flexibility in how you condition prices, filter them, and ensure the best prices are retrieved for custom contexts.
```ts
const ruleTypes = await pricingService.createRuleTypes([
{
name: "Region",
rule_attribute: "region_id",
},
])
const priceSet = await pricingService.addPrices({
priceSetId,
prices: [
{
amount: 500,
currency_code: "EUR",
rules: {
region_id: "PL",
},
},
],
})
```
### 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.
```ts
const priceList = await pricingService.createPriceLists({
title: "My Sale",
type: "sale",
starts_at: Date.parse("01/10/2023"),
ends_at: Date.parse("31/10/2023"),
rules: {
region_id: ["DE", "DK"],
},
prices: [
{
amount: 400,
currency_code: "EUR",
price_set_id: priceSet.id,
},
],
})
```
### Price Calculation Strategy
The modules main interface provides a `calculatePrices` method to retrieve the best price for a given context. You can benefit from your custom rules here to find the best price for the specified rule values.
```ts
const price = await pricingService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: "EUR",
region_id: "PL",
},
}
)
```
---
## How to Use the Pricing Module
The Pricing Module can be used in many use cases, including:
- Medusa Backend: The Medusa backend uses the pricing module to implement some features. However, it's guarded by the [experimental feature flag](../index.md#enabling-experimental-features). If you want to use the pricing module in your backend's customizations, follow [this installation guide](./install-medusa.mdx).
- Serverless Application: Use the Pricing Module in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by [installing it in your Node.js project as an NPM package](./install-nodejs.md).
- Node.js Application: Use the Pricing Module in any Node.js application. Follow [this guide](./install-nodejs.md) to learn how to install it.
---
## Up Next
<DocCard item={{
type: 'link',
href: '/experimental/pricing/concepts',
label: 'Pricing Concepts',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn about the main concepts in the Pricing module.'
}
}}
/>

View File

@@ -0,0 +1,470 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Prices Calculation
In this document, you'll learn how prices are calculated under the hood when you use the `calculatePrices` method of the Pricing module interface.
## Overview
The `calculatePrices` method accepts the ID of one or more price sets and a context. For each price set, it selects two types of prices that best match the context; one belongs to a price list, and one doesn't.
Then, it returns an array of price objects, each price for every supplied price set ID.
---
## Calculation Context
The context is an object passed to the method. It must contain at least the `currency_code`. Only money amounts in the price sets with the same currency code are considered for the price selection.
For example:
```ts
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
async function calculatePrice(
priceSetId: string,
currencyCode: string
) {
const pricingService = await initializePricingModule()
const price = await pricingService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
},
}
)
}
```
The context object can also contain any custom rules, with the key being the `rule_attribute` of a rule type and its value being the rule's value.
For example:
```ts
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
async function calculatePrice(
priceSetId: string,
currencyCode: string
) {
const pricingService = await initializePricingModule()
const price = await pricingService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
region_id: "US",
},
}
)
}
```
---
## Prices Selection
For each price set, the method selects two money amounts:
- The calculated price: a money amount that belongs to a price list. If there are no money amounts associated with a price list, itll be the same as the original price.
- The original price: a money amount that doesn't belong to a price list.
### Calculated Price Selection Process
![A diagram showcasing the calculated price selection process.](https://res.cloudinary.com/dza7lstvk/image/upload/v1700574799/Medusa%20Docs/Diagrams/calculated-price_vjnx3j.jpg)
- Find the price sets associated valid price lists. A price list is considered valid if:
- The current date is between its start and end dates.
- The price list's rules satisfy the context's rules.
- If valid price lists are found, the money amounts within them are sorted by their amount in ascending order. The one having the lowest amount is selected as the calculated price.
- If no valid price list is found, the selected calculated price will be the same as the original price.
### Original Price Selection Process
![A diagram showcasing the original price selection process.](https://res.cloudinary.com/dza7lstvk/image/upload/v1700574800/Medusa%20Docs/Diagrams/original-price_i47fso.jpg)
- If the price list associated with the calculated price is of type `override`, the selected original price is set to the calculated price.
- If no rules are provided in the context other than the `currency_code`, the default money amount is selected as the original price. The default money amount is a money amount having no rules applied to it.
- Otherwise, if a money amount exists in any price set with the same rules provided in the context, it's selected as the original price.
- If no money amount exists with the same rules as the context, all money amounts satisfying any combination of the provided rules are retrieved.
- The money amounts are sorted in descending order by the associated `PriceSetMoneyAmount`'s `number_rules`, the `default_priority` of the rule types, and the `priority` of the associated `PriceRule`. The `priority` attribute has a higher precedence than the `default_priority`.
- The highest money amount sorted is selected as the original price since it's considered the best price.
---
## Returned Calculated Price
After the original and calculated prices are selected, the method will use them to create the following price object for each pr
```ts
const price = {
id: priceSetId,
is_calculated_price_price_list:
!!calculatedPrice?.price_list_id,
calculated_amount: parseInt(calculatedPrice?.amount || "") ||
null,
is_original_price_price_list: !!originalPrice?.price_list_id,
original_amount: parseInt(originalPrice?.amount || "") ||
null,
currency_code: calculatedPrice?.currency_code ||
null,
calculated_price: {
money_amount_id: calculatedPrice?.id || null,
price_list_id: calculatedPrice?.price_list_id || null,
price_list_type: calculatedPrice?.price_list_type || null,
min_quantity: parseInt(
calculatedPrice?.min_quantity || ""
) || null,
max_quantity: parseInt(
calculatedPrice?.max_quantity || ""
) || null,
},
original_price: {
money_amount_id: originalPrice?.id || null,
price_list_id: originalPrice?.price_list_id || null,
price_list_type: originalPrice?.price_list_type || null,
min_quantity: parseInt(originalPrice?.min_quantity || "") ||
null,
max_quantity: parseInt(originalPrice?.max_quantity || "") ||
null,
},
}
```
Where:
- `id`: The ID of the price set from which the money amount was selected.
- `is_calculated_price_price_list`: whether the calculated price belongs to a price list. As mentioned earlier, if no valid price list is found, the calculated price is set to the original price, which doesn't belong to a price list.
- `calculated_amount`: The amount of the calculated price, or `null` if there isn't a calculated price.
- `is_original_price_price_list`: whether the original price belongs to a price list. As mentioned earlier, if the price list of the calculated price is of type `override`, the original price will be the same as the calculated price.
- `original_amount`: The amount of the original price, or `null` if there isn't an original price.
- `currency_code`: The currency code of the calculated price, or `null` if there isn't a calculated price.
- `calculated_price`: An object containing the calculated price's money amount details and potentially its associated price list.
- `original_price`: An object containing the original price's money amount details and potentially its associated price list.
The method returns an array of these price objects.
---
## Example
Consider the following rule types and price sets:
```ts
const ruleTypes = await pricingService.createRuleTypes([
{
name: "Region",
rule_attribute: "region_id",
},
{
name: "City",
rule_attribute: "city",
},
])
const priceSet = await service.create({
rules: [
{ rule_attribute: "region_id" },
{ rule_attribute: "city" },
],
prices: [
//default
{
amount: 500,
currency_code: "EUR",
rules: {},
},
// prices with rules
{
amount: 400,
currency_code: "EUR",
rules: {
region_id: "PL",
},
},
{
amount: 450,
currency_code: "EUR",
rules: {
city: "krakow",
},
},
{
amount: 500,
currency_code: "EUR",
rules: {
city: "warsaw",
region_id: "PL",
},
},
],
})
```
### Default Price Selection
<Tabs groupId="pricing-calculation-example">
<TabItem value="code" label="Code" default>
```ts
const price = await pricingService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR"
}
}
)
```
</TabItem>
<TabItem value="result" label="Result">
The returned price is:
```ts
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 500,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<DEFAULT_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<DEFAULT_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
```
- Original price selection: since there are no provided rules in the context, the original price is the default money amount.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
</TabItem>
</Tabs>
### Exact Match Rule in Context
<Tabs>
<TabItem value="code" label="Code" default>
```ts
const price = await pricingService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL"
}
}
)
```
</TabItem>
<TabItem value="result" label="Result">
The returned price is:
```ts
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 400,
is_original_price_price_list: false,
original_amount: 400,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<SECOND_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<SECOND_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
```
- Original price selection: Since the second money amount of the price set has the same context as the provided one, it's selected as the original price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
</TabItem>
</Tabs>
### Context without Exact Matching Rules
<Tabs>
<TabItem value="code" label="Code" default>
```ts
const price = await pricingService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL",
city: "krakow"
}
}
)
```
</TabItem>
<TabItem value="result" label="Result">
The returned price is:
```ts
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 500,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
```
- Original price selection: The fourth money amount in the price list is selected based on the following process:
- There are no money amounts having the same rules as the context.
- Retrieve all money amounts matching some combination of the rules in the context.
- Sort the money amounts in descending order by their number of rules, each rule type's default priority, and the priority of the rule values.
- All rule types, in this case, don't have a priority. So, the sorting depends on the number of rules each money amount has.
- The fourth money amount has two rules, and the second and third have one rule. So, the fourth money amount is selected as the original price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
</TabItem>
</Tabs>
### Price Selection with Price List
<Tabs>
<TabItem value="code" label="Code" default>
```ts
const priceList = pricingModuleService.createPriceList({
name: "Test Price List",
starts_at: Date.parse("01/10/2023"),
ends_at: Date.parse("31/10/2023"),
rules: {
region_id: ['PL']
},
type: "sale"
prices: [
{
amount: 400,
currency_code: "EUR",
price_set_id: priceSet.id,
},
{
amount: 450,
currency_code: "EUR",
price_set_id: priceSet.id,
},
],
});
const price = await pricingService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL",
city: "krakow"
}
}
)
```
</TabItem>
<TabItem value="result" label="Result">
The returned price is:
```ts
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: true,
calculated_amount: 400,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<PL_MONEY_AMOUNT_ID_1>",
price_list_id: "<PRICE_LIST_ID>",
price_list_type: "sale",
min_quantity: null,
max_quantity: null,
},
}
```
- Original price selection: The process is explained in the Context without Exact Matching Rules section.
- Calculated price selection: The first money amount of the price list is selected based on the following process:
- The price set has a price list associated with it. So, retrieve its money amounts and sort them in ascending order by their amount.
- Since the first money amount of the price list has the lowest amount, it's selected as the calculated price.
</TabItem>
</Tabs>

View File

@@ -0,0 +1,160 @@
# Examples of Product Module
In this document, youll find common examples of how you can use the Product module in your application.
:::note
Examples in this section are in the context of a Next.js App Router.
:::
## Create Product
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function POST(request: Request) {
const productService = await initializeProductModule()
const products = await productService.create([
{
title: "Medusa Shirt",
options: [
{
title: "Color",
},
],
variants: [
{
title: "Black Shirt",
options: [
{
value: "Black",
},
],
},
],
},
])
return NextResponse.json({ products })
}
```
## List Products
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function GET(request: Request) {
const productService = await initializeProductModule()
const data = await productService.list()
return NextResponse.json({ products: data })
}
```
## Retrieve a Product by its ID
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function GET(
request: Request,
{ params }: { params: Record<string, any> }) {
const { id } = params
const productService = await initializeProductModule()
const data = await productService.list({
id,
})
return NextResponse.json({ product: data[0] })
}
```
## Retrieve a Product by its Handle
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function GET(
request: Request,
{ params }: { params: Record<string, any> }) {
const { handle } = params
const productService = await initializeProductModule()
const data = await productService.list({
handle,
})
return NextResponse.json({ product: data[0] })
}
```
## Retrieve Categories
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function GET(request: Request) {
const productService = await initializeProductModule()
const data = await productService.listCategories()
return NextResponse.json({ categories: data })
}
```
## Retrieve Category by Handle
```ts
import { NextResponse } from "next/server"
import {
initialize as initializeProductModule,
} from "@medusajs/product"
export async function GET(
request: Request,
{ params }: { params: Record<string, any> }) {
const { handle } = params
const productService = await initializeProductModule()
const data = await productService.listCategories({
handle,
})
return NextResponse.json({ category: data[0] })
}
```
---
## More Examples
The [module interface reference](../../references/product/interfaces/IProductModuleService.mdx) provides a reference to all the methods available for use with examples for each.

View File

@@ -0,0 +1,164 @@
import DocCard from '@theme/DocCard'
import Icons from '@theme/Icon'
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Install Product Module in Medusa
In this document, you'll learn how to install the Product module using NPM in the Medusa backend.
## Step 1: Install Module
To install the Product module, run the following command in the root directory of the Medusa backend:
```bash npm2yarn
npm install @medusajs/product
```
---
## Step 2: Add Module to Configurations
In `medusa-config.js`, add the product module to the exported object under the `modules` property:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
productService: {
resolve: "@medusajs/product",
},
},
}
```
---
## Step 3: Run Migrations
Run the following command to reflect schema changes into your database:
```bash
npx medusa migrations run
```
---
## Use the Module
You can now start using the module's `ProductModuleService` by resolving it through [dependency injection](../../development/fundamentals/dependency-injection.md).
For example:
<Tabs groupId="resource-type" isCodeTabs={true}>
<TabItem value="api-route" label="API Route" attributes={{
title: "src/api/store/custom/route.ts"
}} default>
```ts
import type {
MedusaRequest,
MedusaResponse
} from "@medusajs/medusa";
import {
ProductModuleService
} from "@medusajs/product"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const productModuleService = req.scope.resolve(
"productModuleService"
)
return res.json({
products: productModuleService.list()
})
}
```
</TabItem>
<TabItem value="subscribers" label="Subscribers" attributes={{
title: "src/subscribers/create-customer.ts"
}}>
```ts
import {
type SubscriberConfig,
type SubscriberArgs,
ProductService,
} from "@medusajs/medusa"
import {
ProductModuleService
} from "@medusajs/product"
export default async function handleListProducts({
data, eventName, container, pluginOptions
}: SubscriberArgs<Customer>) {
const productModuleService = container.resolve(
"productModuleService"
)
console.log(await productModuleService.list())
}
export const config: SubscriberConfig = {
event: ProductService.Events.CREATED,
context: {
subscriberId: "list-products"
}
}
```
</TabItem>
<TabItem value="service" label="Service" attributes={{
title: "src/service/hello.ts"
}}>
```ts
import { TransactionBaseService } from "@medusajs/medusa"
import {
ProductModuleService
} from "@medusajs/product"
class HelloService extends TransactionBaseService {
private productModuleService: ProductModuleService
constructor(container) {
super(container)
this.productModuleService = container.productModuleService
}
await listProducts() {
return await this.productModuleService.list()
}
}
export default HelloService
```
</TabItem>
</Tabs>
:::tip
In the Examples or API Reference guides, you may see an initialization of the product module. This is only necessary if you're using the module outside the Medusa backend.
:::
---
## Up Next
<DocCard item={{
type: 'link',
href: '/references/product',
label: 'Service Interface Reference',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Find a full reference of the module\' service interface\s methods.'
}
}}
/>

View File

@@ -0,0 +1,198 @@
# Install in Node.js-Based Application
In this document, youll learn how to setup and use the Product module in a Node.js based application.
## Prerequisites
Before installing the Product module in your application, make sure you have the following prerequisites:
- Node.js v16 or greater
- PostgreSQL database. You can use an existing Medusa database, or set up a new PostgreSQL database.
---
## Install Package
In your Node.js-based applications, such as a Next.js application, you can install the Product module with the following command:
```bash npm2yarn
npm install @medusajs/product
```
---
## Add Database Configuration
Add the following environment variable to your application:
```bash
POSTGRES_URL=<DATABASE_URL>
```
Where `<DATABASE_URL>` is your database connection URL of the format `postgres://[user][:password]@[host][:port]/[dbname]`. You can learn more about the connection URL format in [this guide](https://www.notion.so/development/backend/configurations.md#database_url).
You can also set the following optional environment variables:
- `POSTGRES_SCHEMA`: a string indicating the PostgreSQL schema to use. By default, it's `public`.
- `POSTGRES_DRIVER_OPTIONS`: a stringified JSON object indicating the PostgreSQL options to use. The JSON object is then parsed to be used as a JavaScript object. By default, it's `{"connection":{"ssl":false}}` for local PostgreSQL databases, and `{"connection":{"ssl":{"rejectUnauthorized":false}}}` for remote databases.
:::note
If `POSTGRES_DRIVER_OPTIONS` is not specified, the PostgreSQL database is considered local if `POSTGRES_URL` includes `localhost`. Otherwise, it's considered remote.
:::
### Run Database Migrations
:::note
You can skip this step if you use an existing Medusa database.
:::
Migrations are used to create your database schema. Before you can run migrations, add in your `package.json` the following scripts:
```json
"scripts": {
//...other scripts
"product:migrations:run": "medusa-product-migrations-up",
"product:seed": "medusa-product-seed ./seed-data.js"
}
```
The first command runs the migrations, and the second command allows you to seed your database with demo products optionally.
However, youd need the following seed file added to the root of your project directory:
<Details>
<Summary>Seed File</Summary>
```js
const productCategoriesData = [
{
id: "category-0",
name: "category 0",
parent_category_id: null,
},
{
id: "category-1",
name: "category 1",
parent_category_id: "category-0",
},
{
id: "category-1-a",
name: "category 1 a",
parent_category_id: "category-1",
},
{
id: "category-1-b",
name: "category 1 b",
parent_category_id: "category-1",
is_internal: true,
},
{
id: "category-1-b-1",
name: "category 1 b 1",
parent_category_id: "category-1-b",
},
]
const productsData = [
{
id: "test-1",
title: "product 1",
status: "published",
descriptions: "Lorem ipsum dolor sit amet, consectetur.",
tags: [
{
id: "tag-1",
value: "France",
},
],
categories: [
{
id: "category-0",
},
],
},
{
id: "test-2",
title: "product",
status: "published",
descriptions: "Lorem ipsum dolor sit amet, consectetur.",
tags: [
{
id: "tag-2",
value: "Germany",
},
],
categories: [
{
id: "category-1",
},
],
},
]
const variantsData = [
{
id: "test-1",
title: "variant title",
sku: "sku 1",
product: { id: productsData[0].id },
inventory_quantity: 10,
},
{
id: "test-2",
title: "variant title",
sku: "sku 2",
product: { id: productsData[1].id },
inventory_quantity: 10,
},
]
module.exports = {
productCategoriesData,
productsData,
variantsData,
}
```
</Details>
Then run the commands you added to migrate the database schema and optionally seed data:
```bash npm2yarn
npm run product:migrations:run
# optionally
npm run product:seed
```
---
## Next.js Application: Adjust Configurations
The Product module uses dependencies that arent Webpack optimized. Since Next.js uses Webpack for compilation, you need to add the Product module as an external dependency.
To do that, add the `serverComponentsExternalPackages` option in `next.config.js`:
```js title=next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: [
"@medusajs/product",
],
},
}
module.exports = nextConfig
```
---
## Start Development
You can refer to the [Example Usages documentation page](./examples.md) for examples of using the Product module.
You can also refer to the [Module Interface Reference](../../references/product/interfaces/IProductModuleService.mdx) for a detailed reference on all available methods.

View File

@@ -0,0 +1,81 @@
import DocCard from '@theme/DocCard'
import Icons from '@theme/Icon'
# Product Module Overview
The Product module is the `@medusajs/product` NPM package that provides product-related features in your Medusa and Node.js applications. It can be used to store products with variants, organize them into categories and collections, and more.
## Features
### Products Management
With the Product module, you can store products and manage them through the main interface methods. Products can have custom options, such as color or size, and each variant in the product sets the value for these options.
```ts
const products = await productService.create([
{
title: "Medusa Shirt",
options: [
{
title: "Color",
},
],
variants: [
{
title: "Black Shirt",
options: [
{
value: "Black",
},
],
},
],
},
])
```
### Product Organizations
The Product module provides different entities that can be used to organize products, including categories, collections, tags, and more.
```ts
const category = await productService.createCategory({
name: "Shirts",
})
const products = await productService.update([
{
id: product.id,
categories: [
{
id: category.id,
},
],
},
])
```
---
## How to Use the Product Module
The Product Module can be used in many use cases, including:
- Medusa Backend: The Medusa backend uses the product module to implement some product features. However, it's guarded by the [experimental feature flag](../index.md#enabling-experimental-features). If you want to use the product module in your backend's customizations, follow [this installation guide](./install-medusa.mdx).
- Serverless Application: Use the Product Module in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by [installing it in your Node.js project as an NPM package](./install-nodejs.md).
- Node.js Application: Use the Product Module in any Node.js application. Follow [this guide](./install-nodejs.md) to learn how to install it.
---
## Up Next
<DocCard item={{
type: 'link',
href: '/experimental/product/examples',
label: 'Example Usages',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Find examples of common usages of the module\'s interface.'
}
}}
/>