docs: improve commerce modules [1/n] (#9498)

Improve and add docs for API Key, Auth, and Cart Modules

[1/n]
This commit is contained in:
Shahed Nasser
2024-10-11 15:19:13 +00:00
committed by GitHub
parent dd162d69be
commit 69b9e73be7
29 changed files with 1025 additions and 323 deletions
@@ -16,13 +16,13 @@ A cart has a shipping and billing address. Both of these addresses are represent
## Line Items
A line item, represented by the `LineItem` data model, is a product variant added to the cart. A cart has multiple line items.
A line item, represented by the [LineItem](/references/cart/models/LineItem) data model, is a quantity of a product variant added to the cart. A cart has multiple line items.
A line item stores some of the product variants properties, such as the `product_title` and `product_description`. It also stores data related to the items quantity and price.
<Note>
A product variant can be from the [Product Module](../../product/page.mdx) but can also be a custom item used only in this cart.
In the Medusa application, a product variant is implemented in the [Product Module](../../product/page.mdx).
</Note>
@@ -32,12 +32,12 @@ A product variant can be from the [Product Module](../../product/page.mdx) but c
A shipping method, represented by the [ShippingMethod data model](/references/cart/models/ShippingMethod), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method.
If the shipping method is created from a shipping option, available through the [Fulfillment Module](../../fulfillment/page.mdx), its ID is stored in the `shipping_option_id`.
A shipping method can also be a custom method associated with this cart only.
In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](../../fulfillment/page.mdx). Its ID is stored in the `shipping_option_id` property of the method.
### data Property
After an order is placed, you may use a third-party fulfillment provider to fulfill its shipments. If the fulfillment provider requires additional custom data to be passed along from the checkout process, you can add this data in the `ShippingMethod`'s `data` property.
After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments.
If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property.
The `data` property is an object used to store custom data relevant later for fulfillment.
@@ -13,16 +13,15 @@ In this guide, youll find common examples of how you can use the Cart Module
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -87,16 +86,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -135,16 +133,15 @@ export async function GET(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -195,16 +192,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -253,16 +249,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -311,16 +306,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -371,16 +365,15 @@ export async function POST(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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 DELETE(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -415,16 +408,15 @@ export async function DELETE(request: Request) {
<CodeTabs groupId="app-type">
<CodeTab label="Medusa API Router" value="medusa">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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 DELETE(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -437,16 +429,15 @@ export async function DELETE(
</CodeTab>
<CodeTab label="Next.js App Router" value="nextjs">
```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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 DELETE(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -0,0 +1,666 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `Extend Cart Data Model`,
}
# {metadata.title}
In this documentation, you'll learn how to extend a data model of the Cart 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 [Cart data model](/references/cart/models/Cart) defined in the Cart Module.
You'll then learn how to:
- Link the `Custom` data model to the `Cart` data model.
- Set the `custom_name` property when a cart is created or updated using Medusa's API routes.
- Retrieve the `custom_name` property with the cart's details, in custom or existing API routes.
## Step 1: Define Custom Data Model
Consider you have a Hello Module defined in the `/src/modules/hello` directory.
<Note title="Tip">
If you don't have a module, follow [this guide](!docs!/basics/modules) to create one.
</Note>
To add the `custom_name` property to the `Cart` data model, you'll create in the Hello Module a data model that has the `custom_name` property.
Create the file `src/modules/hello/models/custom.ts` with the following content:
```ts title="src/modules/hello/models/custom.ts"
import { model } from "@medusajs/framework/utils"
export const Custom = model.define("custom", {
id: model.id().primaryKey(),
custom_name: model.text(),
})
```
This creates a `Custom` data model that has the `id` and `custom_name` properties.
<Note title="Tip">
Learn more about data models in [this guide](!docs!/data-models).
</Note>
---
## Step 2: Define Link to Cart Data Model
Next, you'll define a module link between the `Custom` and `Cart` data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.
<Note title="Tip">
Learn more about module links in [this guide](!docs!/module-links).
</Note>
Create the file `src/links/cart-custom.ts` with the following content:
```ts title="src/links/cart-custom.ts"
import { defineLink } from "@medusajs/framework/utils"
import HelloModule from "../modules/hello"
import CartModule from "@medusajs/medusa/cart"
export default defineLink(
CartModule.linkable.cart,
HelloModule.linkable.custom
)
```
This defines a link between the `Cart` and `Custom` data models. Using this link, you'll later query data across the modules, and link records of each data model.
---
## Step 3: Generate and Run Migrations
<Prerequisites
items={[
{
text: "Module must be registered in medusa-config.js",
link: "!docs!/basics/modules#4-add-module-to-configurations"
}
]}
/>
To reflect the `Custom` data model in the database, generate a migration that defines the table to be created for it.
Run the following command in your Medusa project's root:
```bash
npx medusa db:generate helloModuleService
```
Where `helloModuleService` is your module's name.
Then, run the `db:migrate` command to run the migrations and create a table in the database for the link between the `Cart` 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 cartCreated Workflow Hook
When a cart is created, you also want to create a `Custom` record and set the `custom_name` property, then create a link between the `Cart` and `Custom` records.
To do that, you'll consume the [cartCreated](/references/medusa-workflows/createCartWorkflow#cartcreated) hook of the [createCartWorkflow](/references/medusa-workflows/createCartWorkflow). This workflow is executed in the [Create Cart API Route](!api!/store#carts_postcarts).
<Note title="Tip">
Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks).
</Note>
The API route accepts in its request body an `additional_data` parameter. You can pass in it custom data, which is passed to the workflow hook handler.
### Add custom_name to Additional Data Validation
To pass the `custom_name` in the `additional_data` parameter, you must add a validation rule that tells the Medusa application about this custom property.
Create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/medusa"
import { z } from "zod"
export default defineMiddlewares({
routes: [
{
method: "POST",
matcher: "/store/carts",
additionalDataValidator: {
custom_name: z.string().optional(),
},
},
],
})
```
The `additional_data` parameter validation is customized using the `defineMiddlewares` utility function. In the routes middleware configuration object, the `additionalDataValidator` property accepts [Zod](https://zod.dev/) validaiton rules.
In the snippet above, you add a validation rule indicating that `custom_name` is a string that can be passed in the `additional_data` object.
<Note title="Tip">
Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data).
</Note>
### Create Workflow to Create Custom Record
You'll now create a workflow that will be used in the hook handler.
This workflow will create a `Custom` record, then link it to the cart.
Start by creating the step that creates the `Custom` record. Create the file `src/workflows/create-custom-from-cart/steps/create-custom.ts` with the following content:
```ts title="src/workflows/create-custom-from-cart/steps/create-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type CreateCustomStepInput = {
custom_name?: string
}
export const createCustomStep = createStep(
"create-custom",
async (data: CreateCustomStepInput, { container }) => {
if (!data.custom_name) {
return
}
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const custom = await helloModuleService.createCustoms(data)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
}
)
```
In the step, you resolve the Hello Module's main service and create a `Custom` record.
In the compensation function that undoes the step's actions in case of an error, you delete the created record.
<Note title="Tip">
Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function).
</Note>
Then, create the workflow at `src/workflows/create-custom-from-cart/index.ts` with the following content:
```ts title="src/workflows/create-custom-from-cart/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { CartDTO } from "@medusajs/framework/types"
import { createCustomStep } from "./steps/create-custom"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
export type CreateCustomFromCartWorkflowInput = {
cart: CartDTO
additional_data?: {
custom_name?: string
}
}
export const createCustomFromCartWorkflow = createWorkflow(
"create-custom-from-cart",
(input: CreateCustomFromCartWorkflowInput) => {
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.CART]: {
cart_id: input.cart.id,
},
[HELLO_MODULE]: {
custom_id: custom.id,
},
}])
})
return new WorkflowResponse({
custom,
})
}
)
```
The workflow accepts as an input the created cart and the `additional_data` parameter passed in the request. This is the same input that the `cartCreated` 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 cart 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.
### Consume Workflow Hook
You can now consume the `cartCreated` hook, which is executed in the `createCartWorkflow` after the cart is created.
To consume the hook, create the file `src/workflow/hooks/cart-created.ts` with the following content:
```ts title="src/workflow/hooks/cart-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createCartWorkflow } from "@medusajs/medusa/core-flows"
import {
createCustomFromCartWorkflow,
CreateCustomFromCartWorkflowInput,
} from "../create-custom-from-cart"
createCartWorkflow.hooks.cartCreated(
async (hookData, { container }) => {
await createCustomFromCartWorkflow(container)
.run({
input: hookData as CreateCustomFromCartWorkflowInput,
})
}
)
```
The hook handler executes the `createCustomFromCartWorkflow`, passing it its input.
### Test it Out
To test it out, send a `POST` request to `/store/carts` to create a cart, passing `custom_name` in `additional_data`:
```bash
curl -X POST 'localhost:9000/store/carts' \
-H 'x-publishable-api-key: {publishable_api_key}' \
-H 'Content-Type: application/json' \
--data '{
"region_id": "reg_01J9NNNWVV0T71PT44EAMTJCMP",
"additional_data": {
"custom_name": "test"
}
}'
```
Make sure to replace `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin. Also, replace the value of `region_id` with an ID of a region in your application.
The request will return the cart's details. You'll learn how to retrive the `custom_name` property with the cart's details in the next section.
---
## Step 5: Retrieve custom_name with Cart 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 cart through API routes, such as the [Get Cart API Route](!api!/store#carts_getcartsid), pass in the `fields` query parameter `+custom.*`, which retrieves the linked `Custom` record's details.
<Note title="Tip">
The `+` prefix in `+custom.*` indicates that the relation should be retrieved with the default cart fields. Learn more about selecting fields and relations in the [API reference](!api!/store#select-fields-and-relations).
</Note>
For example:
```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \
-H 'x-publishable-api-key: {publishable_api_key}'
```
Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin.
Among the returned `cart` object, you'll find a `custom` property which holds the details of the linked `Custom` record:
```json
{
"cart": {
// ...
"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 cart in your code using [Query](!docs!/advanced-development/module-links/query).
For example:
```ts
const { data: [cart] } = await query.graph({
entity: "cart",
fields: ["*", "custom.*"],
filters: {
id: cart_id,
},
})
```
Learn more about how to use Query in [this guide](!docs!/advanced-development/module-links/query).
---
## Step 6: Consume cartUpdated Workflow Hook
Similar to the `cartCreated` hook, you'll consume the [cartUpdated](/references/medusa-workflows/updateCartWorkflow#cartupdated) hook of the [updateCartWorkflow](/references/medusa-workflows/updateCartWorkflow) to update `custom_name` when the cart is updated.
The `updateCartWorkflow` is executed by the [Update Cart API route](!api!/store#carts_postcartsid), 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 cart 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: "/store/carts/:id",
additionalDataValidator: {
custom_name: z.string().nullish(),
},
},
],
})
```
The validation schema is the similar to that of the Create Cart 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 cart is deleted.
2. If `additional_data.custom_name` is set and the cart doesn't have a linked `Custom` record, a new record is created and linked to the cart.
3. If `additional_data.custom_name` is set and the cart 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-cart/steps/update-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-cart/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-cart/steps/delete-custom.ts` with the following content:
```ts title="src/workflows/update-custom-from-cart/steps/delete-custom.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Custom } from "../../../modules/hello/models/custom"
import { InferTypeOf } from "@medusajs/framework/types"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type DeleteCustomStepInput = {
custom: InferTypeOf<typeof Custom>
}
export const deleteCustomStep = createStep(
"delete-custom",
async ({ custom }: DeleteCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.createCustoms(custom)
}
)
```
In this step, you delete a `Custom` record. In the compensation function, you create it again.
Finally, you'll create the workflow. Create the file `src/workflows/update-custom-from-cart/index.ts` with the following content:
```ts title="src/workflows/update-custom-from-cart/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { CartDTO } 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 UpdateCustomFromCartStepInput = {
cart: CartDTO
additional_data?: {
custom_name?: string | null
}
}
export const updateCustomFromCartWorkflow = createWorkflow(
"update-custom-from-cart",
(input: UpdateCustomFromCartStepInput) => {
const cartData = useRemoteQueryStep({
entry_point: "cart",
fields: ["custom.*"],
variables: {
filters: {
id: input.cart.id,
},
},
list: false,
})
// TODO create, update, or delete Custom record
}
)
```
The workflow accepts the same input as the `cartUpdated` workflow hook handler would.
In the workflow, you retrieve the cart's linked `Custom` record using Query.
Next, replace the `TODO` with the following:
```ts title="src/workflows/update-custom-from-cart/index.ts"
const created = when({
input,
cartData,
}, (data) =>
!data.cartData.custom &&
data.input.additional_data?.custom_name?.length > 0
)
.then(() => {
const custom = createCustomStep({
custom_name: input.additional_data.custom_name,
})
createRemoteLinkStep([{
[Modules.CART]: {
cart_id: input.cart.id,
},
[HELLO_MODULE]: {
custom_id: custom.id,
},
}])
return custom
})
// TODO update, or delete Custom record
```
Using the `when-then` utility, you check if the cart 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 cart.
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-cart/index.ts"
const deleted = when({
input,
cartData,
}, (data) =>
data.cartData.custom && (
data.input.additional_data?.custom_name === null ||
data.input.additional_data?.custom_name.length === 0
)
)
.then(() => {
deleteCustomStep({
custom: cartData.custom,
})
dismissRemoteLinkStep({
[HELLO_MODULE]: {
custom_id: cartData.custom.id,
},
})
return cartData.custom.id
})
// TODO delete Custom record
```
Using the `when-then` utility, you check if the cart 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-cart/index.ts"
const updated = when({
input,
cartData,
}, (data) => data.cartData.custom && data.input.additional_data?.custom_name?.length > 0)
.then(() => {
const custom = updateCustomStep({
id: cartData.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 cart 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 cartUpdated Workflow Hook
You can now consume the `cartUpdated` and execute the workflow you created.
Create the file `src/workflows/hooks/cart-updated.ts` with the following content:
```ts title="src/workflows/hooks/cart-updated.ts"
import { updateCartWorkflow } from "@medusajs/medusa/core-flows"
import {
UpdateCustomFromCartStepInput,
updateCustomFromCartWorkflow,
} from "../update-custom-from-cart"
updateCartWorkflow.hooks.cartUpdated(
async (hookData, { container }) => {
await updateCustomFromCartWorkflow(container)
.run({
input: hookData as UpdateCustomFromCartStepInput,
})
}
)
```
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 `/store/carts/:id` to update a cart, passing `custom_name` in `additional_data`:
```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \
-H 'x-publishable-api-key: {publishable_api_key}' \
-H 'Content-Type: application/json' \
--data '{
"additional_data": {
"custom_name": "test 2"
}
}'
```
Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin.
The request will return the cart's details with the updated `custom` linked record.
@@ -0,0 +1,35 @@
export const metadata = {
title: `Links between Cart Module and Other Modules`,
}
# {metadata.title}
This document showcases the module links defined between the Cart Module and other commerce modules.
## Payment Module
The Payment Module handles payment processing and management.
Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart.
![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)
---
## Promotion Module
The Promotion Module provides discount features.
Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart.
![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)
---
## Order Module
The Order Module provides order-management features.
Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed.
![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg)
@@ -6,7 +6,7 @@ export const metadata = {
# {metadata.title}
The Cart Module is the `@medusajs/medusa/cart` NPM package that provides cart-related features in your Medusa and Node.js applications.
The Cart Module provides cart-related features in your Medusa and Node.js applications.
## How to Use Cart Module's Service
@@ -15,18 +15,33 @@ You can use the Cart Module's main service by resolving from the Medusa containe
For example:
<CodeTabs groupId="resource-type">
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const cartModuleService = container.resolve(
Modules.CART
)
const carts = await cartModuleService.listCarts()
})
```
</CodeTab>
<CodeTab label="API Route" value="api-route">
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ICartModuleService } 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(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const cartModuleService: ICartModuleService = req.scope.resolve(
const cartModuleService = req.scope.resolve(
Modules.CART
)
@@ -39,35 +54,17 @@ export async function GET(
</CodeTab>
<CodeTab label="Subscriber" value="subscribers">
```ts title="src/subscribers/custom-handler.ts"
import { SubscriberArgs } from "@medusajs/framework"
import { ICartModuleService } 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 cartModuleService: ICartModuleService = container.resolve(
const cartModuleService = container.resolve(
Modules.CART
)
const carts = await cartModuleService.listCarts()
}
```
</CodeTab>
<CodeTab label="Workflow Step" value="workflow-step">
```ts title="src/workflows/hello-world/step1.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { ICartModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
const step1 = createStep("step-1", async (_, { container }) => {
const cartModuleService: ICartModuleService = container.resolve(
Modules.CART
)
const carts = await cartModuleService.listCarts()
})
```
</CodeTab>
@@ -121,7 +118,7 @@ const shippingAdjustments =
A cart is scoped to a sales channel, region, and a customer.
When used with their respective modules and other commerce modules, you benefit from features like:
The Medusa application links the Cart Module to each of their respective modules, providing with features like:
- Checking product availability in a sales channel.
- Retrieving pricing per region.
@@ -14,7 +14,7 @@ In this document, youll learn how a promotion is applied to a carts line i
An adjustment line indicates a change to an item or a shipping methods amount. Its used to apply promotions or discounts on a cart.
The `LineItemAdjustment` data model represents changes on a line item, and the `ShippingMethodAdjustment` data model represents changes on a shipping method.
The [LineItemAdjustment](/references/cart/models/LineItemAdjustment) data model represents changes on a line item, and the [ShippingMethodAdjustment](/references/cart/models/ShippingMethodAdjustment) data model represents changes on a shipping method.
![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg)
@@ -24,7 +24,7 @@ The `amount` property of the adjustment line indicates the amount to be discount
## Discountable Option
The `LineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. Its enabled by default.
The [LineItem](/references/cart/models/LineItem) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. Its enabled by default.
When disabled, a promotion cant be applied to a line item. In the context of the Promotion Module, the promotion isnt applied to the line item even if it matches its rules.
@@ -33,7 +33,7 @@ When disabled, a promotion cant be applied to a line item. In the context of
## Promotion Actions
When using the Cart and Promotion modules together, use the [computeActions method of the Promotion Modules main service](/references/promotion/computeActions). It retrieves the actions of line items and shipping methods.
When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Modules main service](/references/promotion/computeActions). It retrieves the actions of line items and shipping methods.
<Note>
@@ -41,6 +41,8 @@ Learn more about actions in the [Promotion Modules documentation](../../promo
</Note>
For example:
```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
ComputeActionAdjustmentLine,
@@ -101,7 +103,7 @@ The `computeActions` method accepts the existing adjustments of line items and s
Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the carts line item and the shipping methods adjustments.
```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
AddItemAdjustmentAction,
AddShippingMethodAdjustment,
@@ -1,53 +0,0 @@
export const metadata = {
title: `Relations between Cart Module and Other Modules`,
}
# {metadata.title}
This document showcases the link modules defined between the Cart Module and other commerce modules.
## Customer Module
A cart is scoped to the customer using it. Medusa defines a link module that builds a relationship between the `Cart` and `Customer` data models.
![A diagram showcasing an example of how data models from the Cart and Customer modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537276/Medusa%20Resources/cart-customer_pnjvuw.jpg)
---
## Payment Module
The Payment Module allows you to associate payments with a cart. Medusa defines a link module that builds a relationship between the `Cart` and `PaymentCollection` data models.
![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)
---
## Product 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)
---
## Promotion 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 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)
---
## Region Module
A cart is scoped to a region. Medusa defines a link module that builds a relationship between the `Cart` and `Region` data models.
![A diagram showcasing an example of how data models from the Cart and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716543714/Medusa%20Resources/customer-region_rtmymb.jpg)
---
## Sales Channel Module
A cart is scoped to a sales channel. Medusa defines a link module that builds a relationship between the `Cart` and `SalesChannel` data models.
![A diagram showcasing an example of how data models from the Cart and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538159/Medusa%20Resources/cart-sales-channel_n2oi0v.jpg)
@@ -16,7 +16,7 @@ A tax line indicates the tax rate of a line item or a shipping method. The [Line
## Tax Inclusivity
By default, the tax amount is calculated by taking the tax rate from the line item or shipping methods amount, and then added to the item/methods subtotal.
By default, the tax amount is calculated by taking the tax rate from the line item or shipping methods amount, and then adding them to the item/methods subtotal.
However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or methods price already includes taxes.
@@ -30,9 +30,11 @@ The following diagram is a simplified showcase of how a subtotal is calculated f
![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg)
For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`.
---
## Retrieving Tax Lines
## Retrieve Tax Lines
When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Modules main service. It retrieves the tax lines for a carts line items and shipping methods.