From a3aa5d7a66867777c19607517c0a49e41f874607 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 18 Apr 2025 11:38:36 +0300 Subject: [PATCH] dos: add docs on overriding allowed fields in routes (#12233) --- www/apps/api-reference/markdown/admin.mdx | 29 + www/apps/api-reference/markdown/store.mdx | 14 + .../specs/admin/openapi.full.yaml | 69 +- .../specs/admin/paths/admin_customers.yaml | 28 +- .../admin/paths/admin_customers_{id}.yaml | 28 +- .../paths/admin_customers_{id}_addresses.yaml | 28 +- ...customers_{id}_addresses_{address_id}.yaml | 42 +- .../admin_customers_{id}_customer-groups.yaml | 18 +- .../admin/paths/admin_product-categories.yaml | 28 +- .../paths/admin_product-categories_{id}.yaml | 28 +- ...dmin_product-categories_{id}_products.yaml | 14 +- .../specs/store/openapi.full.yaml | 34 +- .../specs/store/paths/store_customers.yaml | 6 +- .../specs/store/paths/store_customers_me.yaml | 18 +- .../paths/store_customers_me_addresses.yaml | 24 +- ...e_customers_me_addresses_{address_id}.yaml | 36 +- .../query-linked-records/page.mdx | 2 +- .../api-routes/retrieve-custom-links/page.mdx | 92 + www/apps/book/generated/edit-dates.mjs | 5 +- www/apps/book/generated/sidebar.mjs | 14 +- www/apps/book/public/llms-full.txt | 32914 ++++++++-------- www/apps/book/sidebar.mjs | 7 +- ...n_customers_[id]_addresses_[address_id].ts | 10 +- .../operations/admin/get_admin_customers.ts | 10 +- .../admin/get_admin_customers_[id].ts | 10 +- .../get_admin_customers_[id]_addresses.ts | 10 +- ...n_customers_[id]_addresses_[address_id].ts | 10 +- .../admin/get_admin_product-categories.ts | 10 +- .../get_admin_product-categories_[id].ts | 10 +- .../operations/admin/post_admin_customers.ts | 10 +- .../admin/post_admin_customers_[id].ts | 10 +- .../post_admin_customers_[id]_addresses.ts | 10 +- ...n_customers_[id]_addresses_[address_id].ts | 10 +- ...st_admin_customers_[id]_customer-groups.ts | 10 +- .../admin/post_admin_product-categories.ts | 10 +- .../post_admin_product-categories_[id].ts | 10 +- ..._admin_product-categories_[id]_products.ts | 10 +- ...ore_customers_me_addresses_[address_id].ts | 6 +- .../store/get_store_customers_me.ts | 6 +- .../store/get_store_customers_me_addresses.ts | 6 +- ...ore_customers_me_addresses_[address_id].ts | 6 +- .../operations/store/post_store_customers.ts | 3 +- .../store/post_store_customers_me.ts | 3 +- .../post_store_customers_me_addresses.ts | 6 +- ...ore_customers_me_addresses_[address_id].ts | 6 +- 45 files changed, 17052 insertions(+), 16618 deletions(-) create mode 100644 www/apps/book/app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx diff --git a/www/apps/api-reference/markdown/admin.mdx b/www/apps/api-reference/markdown/admin.mdx index 21ea2ea863..b866601c15 100644 --- a/www/apps/api-reference/markdown/admin.mdx +++ b/www/apps/api-reference/markdown/admin.mdx @@ -639,6 +639,35 @@ This returns the variants of each product, but the variants only have their `id` + + + + +### Select Custom Linked Data Models + +Most of the API routes that accept a `fields` query parameter allow you to specify custom linked data models. For example, if you linked a `Brand` to a `Product`, you can pass `brand` in the `fields` query parameter to retrieve the brand of each product. + +However, some API routes restrict the fields and relations you can retrieve. To learn about those API routes and how to override the allowed fields and relations, refer to the [Retrieve Custom Linked Data Models from Medusa's API routes](!docs!/learn/fundamentals/api-routes/retrieve-custom-links) documentation. + + + + + + + + + + + diff --git a/www/apps/api-reference/markdown/store.mdx b/www/apps/api-reference/markdown/store.mdx index 7cb6056feb..6c5b8f58ab 100644 --- a/www/apps/api-reference/markdown/store.mdx +++ b/www/apps/api-reference/markdown/store.mdx @@ -597,6 +597,20 @@ This returns the variants of each product, but the variants only have their `id` + + +### Select Custom Linked Data Models + +Most of the API routes that accept a `fields` query parameter allow you to specify custom linked data models. For example, if you linked a `Brand` to a `Product`, you can pass `brand` in the `fields` query parameter to retrieve the brand of each product. + +However, some API routes restrict the fields and relations you can retrieve. To learn about those API routes and how to override the allowed fields and relations, refer to the [Retrieve Custom Linked Data Models from Medusa's API routes](!docs!/learn/fundamentals/api-routes/retrieve-custom-links) documentation. + + + + + + + - - "Comma-separated fields that should be included in the returned data. + Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. - without prefix it will replace the entire default fields. NOTE: This - route doesn't allow expanding custom relations." + without prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in + + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -31,8 +36,11 @@ post: Comma-separated fields that should be included in the returned data. If a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. Without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/admin/paths/admin_product-categories.yaml b/www/apps/api-reference/specs/admin/paths/admin_product-categories.yaml index ee22858434..fc45e50ac0 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_product-categories.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_product-categories.yaml @@ -13,8 +13,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. NOTE: This route doesn't allow - expanding custom relations. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -23,8 +26,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' - name: offset @@ -683,8 +689,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. NOTE: This route doesn't allow - expanding custom relations. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -693,8 +702,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}.yaml index a04b54bacd..36a0b82125 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}.yaml @@ -18,8 +18,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. NOTE: This route doesn't allow - expanding custom relations. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -28,8 +31,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' - name: include_ancestors_tree @@ -124,8 +130,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. NOTE: This route doesn't allow - expanding custom relations. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -134,8 +143,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}_products.yaml b/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}_products.yaml index 44db81e032..1a13e4d9de 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}_products.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_product-categories_{id}_products.yaml @@ -17,8 +17,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. NOTE: This route doesn't allow - expanding custom relations. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -27,8 +30,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. NOTE: This route - doesn't allow expanding custom relations. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/store/openapi.full.yaml b/www/apps/api-reference/specs/store/openapi.full.yaml index d072824c71..27511aaae3 100644 --- a/www/apps/api-reference/specs/store/openapi.full.yaml +++ b/www/apps/api-reference/specs/store/openapi.full.yaml @@ -2842,7 +2842,7 @@ paths: schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' requestBody: @@ -2945,12 +2945,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -3029,7 +3029,7 @@ paths: schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -3120,12 +3120,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' - name: offset @@ -3281,12 +3281,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -3445,12 +3445,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -3533,12 +3533,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -3691,12 +3691,12 @@ paths: url: https://docs.medusajs.com/api/store#publishable-api-key - name: fields in: query - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. required: false schema: type: string title: fields - description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. + description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -21335,7 +21335,11 @@ components: continueOnPermanentFailure: type: boolean title: continueOnPermanentFailure - description: Whether the step continues executing even if its status is changed to failed. + description: Whether the workflow should continue executing even if its status is changed to failed. + skipOnPermanentFailure: + type: boolean + title: skipOnPermanentFailure + description: Whether the workflow should skip subsequent steps in case of a permanent failure. maxRetries: type: number title: maxRetries diff --git a/www/apps/api-reference/specs/store/paths/store_customers.yaml b/www/apps/api-reference/specs/store/paths/store_customers.yaml index 2740dcee1c..1588de0dd6 100644 --- a/www/apps/api-reference/specs/store/paths/store_customers.yaml +++ b/www/apps/api-reference/specs/store/paths/store_customers.yaml @@ -34,7 +34,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' requestBody: diff --git a/www/apps/api-reference/specs/store/paths/store_customers_me.yaml b/www/apps/api-reference/specs/store/paths/store_customers_me.yaml index 7c9689fc97..1a1ec443f2 100644 --- a/www/apps/api-reference/specs/store/paths/store_customers_me.yaml +++ b/www/apps/api-reference/specs/store/paths/store_customers_me.yaml @@ -25,7 +25,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -34,7 +38,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -103,7 +111,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/store/paths/store_customers_me_addresses.yaml b/www/apps/api-reference/specs/store/paths/store_customers_me_addresses.yaml index 821d058df9..2c4c6b38b6 100644 --- a/www/apps/api-reference/specs/store/paths/store_customers_me_addresses.yaml +++ b/www/apps/api-reference/specs/store/paths/store_customers_me_addresses.yaml @@ -26,7 +26,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -35,7 +39,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' - name: offset @@ -182,7 +190,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -191,7 +203,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/api-reference/specs/store/paths/store_customers_me_addresses_{address_id}.yaml b/www/apps/api-reference/specs/store/paths/store_customers_me_addresses_{address_id}.yaml index 4ce71a0912..ec31f199c1 100644 --- a/www/apps/api-reference/specs/store/paths/store_customers_me_addresses_{address_id}.yaml +++ b/www/apps/api-reference/specs/store/paths/store_customers_me_addresses_{address_id}.yaml @@ -27,7 +27,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -36,7 +40,11 @@ get: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -104,7 +112,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -113,7 +125,11 @@ post: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: @@ -249,7 +265,11 @@ delete: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without prefix it will - replace the entire default fields. + replace the entire default fields. This API route restricts the fields + that can be selected. Learn how to override the retrievable fields in + the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. required: false schema: type: string @@ -258,7 +278,11 @@ delete: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. without - prefix it will replace the entire default fields. + prefix it will replace the entire default fields. This API route + restricts the fields that can be selected. Learn how to override the + retrievable fields in the [Retrieve Custom + Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) + documentation. externalDocs: url: '#select-fields-and-relations' security: diff --git a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx index c6cfb06337..da7b104ec9 100644 --- a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx @@ -31,7 +31,7 @@ Medusa's existing API routes accept a `fields` query parameter that allows you t -Learn more about selecting fields and relations in the [API Reference](!api!/admin#select-fields-and-relations). +Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](../../../fundamentals/api-routes/retrieve-custom-links/page.mdx) chapter. diff --git a/www/apps/book/app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx new file mode 100644 index 0000000000..45ddb5dc46 --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx @@ -0,0 +1,92 @@ +export const metadata = { + title: `${pageNumber} Retrieve Custom Links from Medusa's API Route`, +} + +# {metadata.title} + +In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. + +## Why Retrieve Custom Linked Data Models? + +Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. + +For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](../../module-links/page.mdx) to the [Product Module](!resources!/commerce-modules/product)'s `Product` data model. + +When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](!api!/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. + +--- + +## How to Retrieve Custom Linked Data Models Using `fields`? + +Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. + +For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](!api!/admin#products_getproductsid): + +```bash +curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ +-H 'Authorization: Bearer {access_token}' +``` + +The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](!api!/store#select-fields-and-relations). + +By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. + +--- + +## API Routes that Restrict Retrievable Fields + +Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. + +The API routes that restrict the fields and relations you can retrieve are: + +- [Customer Store API Routes](!api!/store#customers) +- [Customer Admin API Routes](!api!/admin#customers) +- [Product Category Admin API Routes](!api!/admin#product-categories) + +### How to Override Allowed Fields and Relations + +For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](../middlewares/page.mdx) to those routes. + +For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](!api!/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: + + + +Learn how to create a middleware in the [Middlewares](../middlewares/page.mdx) chapter. + + + +export const highlights = [ + ["10", "req.allowed", "Modify the allowed fields and relations to be retrieved"], +] + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { defineMiddlewares } from "@medusajs/medusa"; + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: "GET", + middlewares: [ + (req, res, next) => { + req.allowed?.push("b2b_company"); + next(); + }, + ], + }, + ], +}); +``` + +In this example, you apply a middleware to the [Get Customer Admin API Route](!api!/admin#customers_getcustomersid). + +The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. + +You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](!api!/admin#customers_getcustomersid): + +```bash +curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ +-H 'Authorization: Bearer {access_token}' +``` + +In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 9484ba02e8..5eb6bf12a3 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -85,7 +85,7 @@ export const generatedEditDates = { "app/learn/customization/customize-admin/widget/page.mdx": "2025-02-05T09:10:18.163Z", "app/learn/customization/extend-features/define-link/page.mdx": "2025-04-17T08:50:17.036Z", "app/learn/customization/extend-features/page.mdx": "2024-12-09T11:02:39.244Z", - "app/learn/customization/extend-features/query-linked-records/page.mdx": "2025-03-25T13:48:49.973Z", + "app/learn/customization/extend-features/query-linked-records/page.mdx": "2025-04-18T07:41:05.912Z", "app/learn/customization/integrate-systems/handle-event/page.mdx": "2024-12-24T15:09:24.653Z", "app/learn/customization/integrate-systems/page.mdx": "2024-12-09T10:40:08.528Z", "app/learn/customization/integrate-systems/schedule-task/page.mdx": "2025-01-28T16:42:42.071Z", @@ -119,5 +119,6 @@ export const generatedEditDates = { "app/learn/production/worker-mode/page.mdx": "2025-03-11T15:21:50.906Z", "app/learn/fundamentals/module-links/read-only/page.mdx": "2025-03-21T09:14:54.944Z", "app/learn/fundamentals/data-models/properties/page.mdx": "2025-03-18T07:57:17.826Z", - "app/learn/fundamentals/framework/page.mdx": "2025-04-17T16:07:19.090Z" + "app/learn/fundamentals/framework/page.mdx": "2025-04-17T16:07:19.090Z", + "app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx": "2025-04-18T07:38:56.729Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index 9616c95d5b..844adca916 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -628,10 +628,20 @@ export const generatedSidebars = [ "isPathHref": true, "type": "link", "path": "/learn/fundamentals/api-routes/additional-data", - "title": "Additional Data", + "title": "Pass Additional Data", "children": [], - "chapterTitle": "3.6.10. Additional Data", + "chapterTitle": "3.6.10. Pass Additional Data", "number": "3.6.10." + }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/api-routes/retrieve-custom-links", + "title": "Retrieve Custom Links", + "children": [], + "chapterTitle": "3.6.11. Retrieve Custom Links", + "number": "3.6.11." } ], "chapterTitle": "3.6. API Routes", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index f92e3de05f..e45ff52d47 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -136,76 +136,6 @@ The next chapter covers how you generally deploy the production build. You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides. -# Medusa Deployment Overview - -In this chapter, you’ll learn the general approach to deploying the Medusa application. - -## Medusa Project Components - -A standard Medusa project is made up of: - -- Medusa application: The Medusa server and the Medusa Admin. -- One or more storefronts - -![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) - -You deploy the Medusa application, with the server and admin, separately from the storefront. - -*** - -## Deploying the Medusa Application - -You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. - -The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… - -![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) - -Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). - -When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. - -### Deploy Server and Worker Instances - -By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. - -Instead, you should deploy two instances: - -- A server instance, which handles incoming requests to the application’s API routes. -- A worker instance, which processes background tasks, including scheduled jobs and subscribers. - -You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. - -Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -### How to Deploy Medusa? - -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) - -To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. - -*** - -## Deploying the Storefront - -The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. - -If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. - -Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. - -Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. - - # Install Medusa In this chapter, you'll learn how to install and run a Medusa application. @@ -330,6 +260,76 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html. In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. +# Medusa Deployment Overview + +In this chapter, you’ll learn the general approach to deploying the Medusa application. + +## Medusa Project Components + +A standard Medusa project is made up of: + +- Medusa application: The Medusa server and the Medusa Admin. +- One or more storefronts + +![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) + +You deploy the Medusa application, with the server and admin, separately from the storefront. + +*** + +## Deploying the Medusa Application + +You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. + +The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… + +![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) + +Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). + +When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. + +### Deploy Server and Worker Instances + +By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. + +Instead, you should deploy two instances: + +- A server instance, which handles incoming requests to the application’s API routes. +- A worker instance, which processes background tasks, including scheduled jobs and subscribers. + +You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. + +Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +### How to Deploy Medusa? + +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) + +To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. + +*** + +## Deploying the Storefront + +The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. + +If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. + +Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. + +Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. + + # Storefront Development The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. @@ -1405,312 +1405,350 @@ import { BrandModuleService } from "@/modules/brand/service" ``` -# General Medusa Application Deployment Guide +# Configure Instrumentation -In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. +In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. -Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). +## Observability with OpenTelemtry -Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: +*** -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. +## How to Configure Instrumentation in Medusa? ### Prerequisites -- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) -## What You'll Deploy +### Install Dependencies -When you deploy the Medusa application, you need to deploy the following resources: +Start by installing the following OpenTelemetry dependencies in your Medusa project: -1. PostgreSQL database: This is the database that will hold your Medusa application's details. -2. Redis database: This is the database that will store the Medusa server's session. -3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: - - The server mode handles incoming API requests and serving the Medusa Admin dashboard. - - The worker mode handles background tasks, such as scheduled jobs and subscribers. +```bash npm2yarn +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg +``` -So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: + +```bash npm2yarn +npm install @opentelemetry/exporter-zipkin +``` + +### Add instrumentation.ts + +Next, create the file `instrumentation.ts` with the following content: + +```ts title="instrumentation.ts" +import { registerOtel } from "@medusajs/medusa" +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" + +// If using an exporter other than Zipkin, initialize it here. +const exporter = new ZipkinExporter({ + serviceName: "my-medusa-project", +}) + +export function register() { + registerOtel({ + serviceName: "medusajs", + // pass exporter + exporter, + instrument: { + http: true, + workflows: true, + query: true, + }, + }) +} +``` + +In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. + +`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: + +The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. + +- serviceName: (\`string\`) The name of the service traced. +- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. +- instrument: (\`object\`) Options specifying what to trace. + + - http: (\`boolean\`) Whether to trace HTTP requests. + + - query: (\`boolean\`) Whether to trace Query usages. + + - workflows: (\`boolean\`) Whether to trace Workflow executions. + + - db: (\`boolean\`) Whether to trace database queries and operations. +- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. *** -## 1. Configure Medusa Application +## Test it Out -### Worker Mode +To test it out, start your exporter, such as Zipkin. -The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. +Then, start your Medusa application: -Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -So, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", - }, -}) +```bash npm2yarn +npm run dev ``` -Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. +Try to open the Medusa Admin or send a request to an API route. -### Configure Medusa Admin +If you check traces in your exporter, you'll find new traces reported. -The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. +### Trace Span Names -To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: +Trace span names start with the following keywords based on what it's reporting: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - disable: process.env.DISABLE_MEDUSA_ADMIN === "true", - }, -}) +- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. +- `route:` when reporting route handlers running on an HTTP request. +- `middleware:` when reporting a middleware running on an HTTP request. +- `workflow:` when reporting a workflow execution. +- `step:` when reporting a step in a workflow execution. +- `query.graph:` when reporting Query usages. +- `pg.query:` when reporting database queries and operations. + + +# Logging + +In this chapter, you’ll learn how to use Medusa’s logging utility. + +## Logger Class + +Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. + +The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. + +*** + +## How to Log a Message + +Resolve the `logger` using the Medusa container to log a message in your resource. + +For example, create the file `src/jobs/log-message.ts` with the following content: + +```ts title="src/jobs/log-message.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("I'm using the logger!") +} + +export const config = { + name: "test-logger", + // execute every minute + schedule: "* * * * *", +} ``` -Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. +This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. -### Configure Redis URL +### Test the Scheduled Job -The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. +To test out the above scheduled job, start the Medusa application: -Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). +```bash npm2yarn +npm run dev +``` -So, add the following configuration in `medusa-config.ts` : +After a minute, you'll see the following message as part of the logged messages: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - redisUrl: process.env.REDIS_URL, - }, -}) +```text +info: I'm using the logger! ``` *** -## 2. Add predeploy Script +## Log Levels -Before you start the Medusa application in production, you should always run migrations and sync links. +The `Logger` class has the following methods: -So, add the following script in `package.json`: +- `info`: The message is logged with level `info`. +- `warn`: The message is logged with level `warn`. +- `error`: The message is logged with level `error`. +- `debug`: The message is logged with level `debug`. -```json +Each of these methods accepts a string parameter to log in the terminal with the associated level. + +*** + +## Logging Configurations + +### Log Level + +The available log levels, from lowest to highest levels, are: + +1. `silly` (default, meaning messages of all levels are logged) +2. `debug` +3. `info` +4. `warn` +5. `error` + +You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. + +For example: + +```bash +LOG_LEVEL=error +``` + +This logs `error` messages only. + +The environment variable must be set as a system environment variable and not in `.env`. + +### Save Logs in a File + +Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. + +For example: + +```bash +LOG_FILE=all.log +``` + +Your logs are now saved in the `all.log` file at the root of your Medusa application. + +The environment variable must be set as a system environment variable and not in `.env`. + +*** + +## Show Log with Progress + +The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. + +For example: + +```ts title="src/jobs/log-message.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + const activityId = logger.activity("First log message") + + logger.progress(activityId, `Second log message`) + + logger.success(activityId, "Last log message") +} +``` + +The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: + +- `progress`: Log a message of level `info` that indicates progress within that same activity. +- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. +- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. + +If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. + + +# Medusa Testing Tools + +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. + +## @medusajs/test-utils Package + +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. + +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: + +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest +``` + +*** + +## Install and Configure Jest + +Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. + +Run the following command to install the required Jest dependencies: + +```bash npm2yarn +npm install --save-dev jest @types/jest @swc/jest +``` + +Then, create the file `jest.config.js` with the following content: + +```js title="jest.config.js" +const { loadEnv } = require("@medusajs/framework/utils") +loadEnv("test", process.cwd()) + +module.exports = { + transform: { + "^.+\\.[jt]s$": [ + "@swc/jest", + { + jsc: { + parser: { syntax: "typescript", decorators: true }, + }, + }, + ], + }, + testEnvironment: "node", + moduleFileExtensions: ["js", "ts", "json"], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["./integration-tests/setup.js"], +} + +if (process.env.TEST_TYPE === "integration:http") { + module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] +} else if (process.env.TEST_TYPE === "integration:modules") { + module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] +} else if (process.env.TEST_TYPE === "unit") { + module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] +} +``` + +Next, create the `integration-tests/setup.js` file with the following content: + +```js title="integration-tests/setup.js" +const { MetadataStorage } = require("@mikro-orm/core") + +MetadataStorage.clear() +``` + +*** + +## Add Test Commands + +Finally, add the following scripts to `package.json`: + +```json title="package.json" "scripts": { // ... - "predeploy": "medusa db:migrate" + "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", + "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" }, ``` -*** +You now have two commands: -## 3. Install Production Modules and Providers +- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. +- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. +- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. -By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. - -It’s highly recommended to instead use modules and providers suitable for production, including: - -- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) -- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) -- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) -- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). -- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). - -Then, add these modules in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/workflow-engine-redis", - options: { - redis: { - url: process.env.REDIS_URL, - }, - }, - }, - ], -}) -``` - -Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. +Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. *** -## 4. Create PostgreSQL and Redis Databases +## Test Tools and Writing Tests -Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. - -If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). - -After hosting both databases, keep their connection URLs for the next steps. - -*** - -## 5. Deploy Medusa Application in Server Mode - -As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. - -The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -STORE_CORS= # STOREFRONT URL -ADMIN_CORS= # ADMIN URL -AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS -DISABLE_MEDUSA_ADMIN=false -MEDUSA_WORKER_MODE=server -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. -- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. -- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm install && npm run predeploy && npm run start -``` - -Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. - -### Set Backend URL in Admin Configuration - -The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. - -After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - // ... - backendUrl: process.env.MEDUSA_BACKEND_URL, - }, -}) -``` - -Then, push the changes to the GitHub repository or deployed application. - -In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: - -```bash -ADMIN_CORS= # MEDUSA APPLICATION URL -AUTH_CORS= # ADD MEDUSA APPLICATION URL -MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION -``` - -Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. - -After setting the environment variables, make sure to restart the deployment for the changes to take effect. - -Remember to separate URLs in `AUTH_CORS` by commas. - -*** - -## 6. Deploy Medusa Application in Worker Mode - -Next, you'll deploy the Medusa application in worker mode. - -As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -DISABLE_MEDUSA_ADMIN=true -MEDUSA_WORKER_MODE=worker -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm run install && npm run start -``` - -*** - -## 7. Test Deployed Application - -Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. - -The Medusa Admin is also available at `/app`. - -*** - -## Create Admin User - -If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: - -```bash -npx medusa user -e admin-medusa@test.com -p supersecret -``` - -Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. - -You can use these credentials to log into the Medusa Admin dashboard. +The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. # Build Custom Features @@ -1804,6 +1842,47 @@ The following guides and references are useful for your development journey: In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. +# Re-Use Customizations with Plugins + +In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. + +You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. + +To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. + +![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) + +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. + +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + +# Admin Development + +In the next chapters, you'll learn more about possible admin customizations. + +You can customize the admin dashboard by: + +- Adding new sections to existing pages using Widgets. +- Adding new pages using UI Routes. + +However, you can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). + +*** + +## Medusa UI Package + +Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design. + +Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components. + +*** + +## Admin Components List + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. + + # Integrate Third-Party Systems Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. @@ -1827,21 +1906,6 @@ In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/ 3. Sync brands from the CMS at a daily schedule. -# Re-Use Customizations with Plugins - -In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. - -You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. - -To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. - -![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) - -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. - -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - # Custom CLI Scripts In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. @@ -1912,32 +1976,6 @@ npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` -# Admin Development - -In the next chapters, you'll learn more about possible admin customizations. - -You can customize the admin dashboard by: - -- Adding new sections to existing pages using Widgets. -- Adding new pages using UI Routes. - -However, you can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). - -*** - -## Medusa UI Package - -Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design. - -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components. - -*** - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - # API Routes In this chapter, you’ll learn what API Routes are and how to create them. @@ -2101,6 +2139,85 @@ For example, the Blog Module's service would have methods like `retrievePost` an Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. +# Environment Variables + +In this chapter, you'll learn how environment variables are loaded in Medusa. + +## System Environment Variables + +The Medusa application loads and uses system environment variables. + +For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. + +In production, you should always use system environment variables that you set through your hosting provider. + +*** + +## Environment Variables in .env Files + +During development, it's easier to set environment variables in a `.env` file in your repository. + +Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: + +As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. + +|\`.env\`| +|---|---| +|\`NODE\_ENV\`|\`.env\`| +|\`NODE\_ENV\`|\`.env.production\`| +|\`NODE\_ENV\`|\`.env.staging\`| +|\`NODE\_ENV\`|\`.env.test\`| + +### Set Environment in `loadEnv` + +In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. + +This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. + +To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. + +*** + +## Environment Variables for Admin Customizations + +Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +*** + +## Predefined Medusa Environment Variables + +The Medusa application uses the following predefined environment variables that you can set: + +You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). + +|Environment Variable|Description|Default| +|---|---|---|---|---| +| +| +| +| +||The URL to connect to the PostgreSQL database. Only used if || +||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || +||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || +||URLs of clients that can access the Medusa backend's authentication routes. Only used if || +||A random string used to create authentication tokens in the http layer. Only used if || +||A random string used to create cookie tokens in the http layer. Only used if || +||The URL to the Medusa backend. Only used if || +| +| +| +| +| +| +| +| +||The allowed levels to log. Learn more in || +||The file to save logs in. By default, logs aren't saved in any file. Learn more in || +||Whether to disable analytics data collection. Learn more in || + + # Framework Overview In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application. @@ -2871,85 +2988,6 @@ To learn more about the different concepts useful for building plugins, check ou - [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) -# Environment Variables - -In this chapter, you'll learn how environment variables are loaded in Medusa. - -## System Environment Variables - -The Medusa application loads and uses system environment variables. - -For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. - -In production, you should always use system environment variables that you set through your hosting provider. - -*** - -## Environment Variables in .env Files - -During development, it's easier to set environment variables in a `.env` file in your repository. - -Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: - -As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. - -|\`.env\`| -|---|---| -|\`NODE\_ENV\`|\`.env\`| -|\`NODE\_ENV\`|\`.env.production\`| -|\`NODE\_ENV\`|\`.env.staging\`| -|\`NODE\_ENV\`|\`.env.test\`| - -### Set Environment in `loadEnv` - -In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. - -This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. - -To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. - -*** - -## Environment Variables for Admin Customizations - -Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -*** - -## Predefined Medusa Environment Variables - -The Medusa application uses the following predefined environment variables that you can set: - -You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). - -|Environment Variable|Description|Default| -|---|---|---|---|---| -| -| -| -| -||The URL to connect to the PostgreSQL database. Only used if || -||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || -||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || -||URLs of clients that can access the Medusa backend's authentication routes. Only used if || -||A random string used to create authentication tokens in the http layer. Only used if || -||A random string used to create cookie tokens in the http layer. Only used if || -||The URL to the Medusa backend. Only used if || -| -| -| -| -| -| -| -| -||The allowed levels to log. Learn more in || -||The file to save logs in. By default, logs aren't saved in any file. Learn more in || -||Whether to disable analytics data collection. Learn more in || - - # Events and Subscribers In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. @@ -3202,44 +3240,6 @@ A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). -# Plugins - -In this chapter, you'll learn what a plugin is in Medusa. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## What is a Plugin? - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). - -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. - -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) - -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). - -*** - -## Plugin vs Module - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. - -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. - -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. - -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. - -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. - -*** - -## How to Create a Plugin? - -The next chapter explains how you can create and publish a plugin. - - # Define Module Link In this chapter, you’ll learn what a module link is and how to define one. @@ -3455,6 +3455,138 @@ npx medusa db:migrate ``` +# Plugins + +In this chapter, you'll learn what a plugin is in Medusa. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). + +*** + +## Plugin vs Module + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + +*** + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + + +# Scheduled Jobs + +In this chapter, you’ll learn about scheduled jobs and how to use them. + +## What is a Scheduled Job? + +When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. + +In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. + +Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. + +- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. +- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. +- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. + +*** + +## How to Create a Scheduled Job? + +You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. + +For example, create the file `src/jobs/hello-world.ts` with the following content: + +![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) + +```ts title="src/jobs/hello-world.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" + +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") + + logger.info("Greeting!") +} + +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} +``` + +You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. + +You also export a `config` object that has the following properties: + +- `name`: A unique name for the job. +- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. + +This scheduled job executes every minute and logs into the terminal `Greeting!`. + +### Test the Scheduled Job + +To test out your scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +After a minute, the following message will be logged to the terminal: + +```bash +info: Greeting! +``` + +*** + +## Example: Sync Products Once a Day + +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. + +When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. + +You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: + +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" + +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} + +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` + +In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. + +The next time you start the Medusa application, it will run this job every day at midnight. + + # Modules In this chapter, you’ll learn about modules and how to create them. @@ -3755,98 +3887,96 @@ This will create a post and return it in the response: You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval. -# Scheduled Jobs +# Worker Mode of Medusa Instance -In this chapter, you’ll learn about scheduled jobs and how to use them. +In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. -## What is a Scheduled Job? +## What is Worker Mode? -When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. +By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. -In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. +In a production environment, you should deploy two separate instances of your Medusa application: -Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. +1. A server instance that handles incoming requests to the application's API routes. +2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers. -- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. -- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. -- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. +You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter. + +This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. + +![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) *** -## How to Create a Scheduled Job? +## How to Set Worker Mode -You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. +You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: -For example, create the file `src/jobs/hello-world.ts` with the following content: +- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. +- `worker`: run a worker process only. +- `server`: run the application server only. -![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) +Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. -```ts title="src/jobs/hello-world.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" +For example, set the worker mode in `medusa-config.ts` to the following: -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") - - logger.info("Greeting!") -} - -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + workerMode: process.env.WORKER_MODE || "shared", + // ... + }, + // ... +}) ``` -You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. +You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. -You also export a `config` object that has the following properties: +Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: -- `name`: A unique name for the job. -- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -This scheduled job executes every minute and logs into the terminal `Greeting!`. - -### Test the Scheduled Job - -To test out your scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, the following message will be logged to the terminal: +### Server Medusa Instance ```bash -info: Greeting! +WORKER_MODE=server ``` -*** +### Worker Medusa Instance -## Example: Sync Products Once a Day - -In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. - -When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. - -You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: - -```ts title="src/jobs/sync-products.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" - -export default async function syncProductsJob(container: MedusaContainer) { - await syncProductToErpWorkflow(container) - .run() -} - -export const config = { - name: "sync-products-job", - schedule: "0 0 * * *", -} +```bash +WORKER_MODE=worker ``` -In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. +### Disable Admin in Worker Mode -The next time you start the Medusa application, it will run this job every day at midnight. +Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. + +To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false, + }, + // ... +}) +``` + +Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. + +Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: + +### Server Medusa Instance + +```bash +ADMIN_DISABLED=false +``` + +### Worker Medusa Instance + +```bash +ADMIN_DISABLED=true +``` # Medusa's Architecture @@ -3922,114 +4052,312 @@ The following diagram illustrates Medusa's architecture including all its layers ![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) -# Configure Instrumentation +# General Medusa Application Deployment Guide -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. +In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. -## Observability with OpenTelemtry +Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: +Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. -*** +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: -## How to Configure Instrumentation in Medusa? +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. ### Prerequisites -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) +- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) -### Install Dependencies +## What You'll Deploy -Start by installing the following OpenTelemetry dependencies in your Medusa project: +When you deploy the Medusa application, you need to deploy the following resources: -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` +1. PostgreSQL database: This is the database that will hold your Medusa application's details. +2. Redis database: This is the database that will store the Medusa server's session. +3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: + - The server mode handles incoming API requests and serving the Medusa Admin dashboard. + - The worker mode handles background tasks, such as scheduled jobs and subscribers. -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: - -```bash npm2yarn -npm install @opentelemetry/exporter-zipkin -``` - -### Add instrumentation.ts - -Next, create the file `instrumentation.ts` with the following content: - -```ts title="instrumentation.ts" -import { registerOtel } from "@medusajs/medusa" -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" - -// If using an exporter other than Zipkin, initialize it here. -const exporter = new ZipkinExporter({ - serviceName: "my-medusa-project", -}) - -export function register() { - registerOtel({ - serviceName: "medusajs", - // pass exporter - exporter, - instrument: { - http: true, - workflows: true, - query: true, - }, - }) -} -``` - -In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. - -`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: - -The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. - -- serviceName: (\`string\`) The name of the service traced. -- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. -- instrument: (\`object\`) Options specifying what to trace. - - - http: (\`boolean\`) Whether to trace HTTP requests. - - - query: (\`boolean\`) Whether to trace Query usages. - - - workflows: (\`boolean\`) Whether to trace Workflow executions. - - - db: (\`boolean\`) Whether to trace database queries and operations. -- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. +So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. *** -## Test it Out +## 1. Configure Medusa Application -To test it out, start your exporter, such as Zipkin. +### Worker Mode -Then, start your Medusa application: +The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. -```bash npm2yarn -npm run dev +Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +So, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", + }, +}) ``` -Try to open the Medusa Admin or send a request to an API route. +Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. -If you check traces in your exporter, you'll find new traces reported. +### Configure Medusa Admin -### Trace Span Names +The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. -Trace span names start with the following keywords based on what it's reporting: +To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: -- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. -- `route:` when reporting route handlers running on an HTTP request. -- `middleware:` when reporting a middleware running on an HTTP request. -- `workflow:` when reporting a workflow execution. -- `step:` when reporting a step in a workflow execution. -- `query.graph:` when reporting Query usages. -- `pg.query:` when reporting database queries and operations. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + disable: process.env.DISABLE_MEDUSA_ADMIN === "true", + }, +}) +``` + +Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. + +### Configure Redis URL + +The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. + +Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). + +So, add the following configuration in `medusa-config.ts` : + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + redisUrl: process.env.REDIS_URL, + }, +}) +``` + +*** + +## 2. Add predeploy Script + +Before you start the Medusa application in production, you should always run migrations and sync links. + +So, add the following script in `package.json`: + +```json +"scripts": { + // ... + "predeploy": "medusa db:migrate" +}, +``` + +*** + +## 3. Install Production Modules and Providers + +By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. + +It’s highly recommended to instead use modules and providers suitable for production, including: + +- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) +- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) +- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) +- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). +- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). + +Then, add these modules in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.REDIS_URL, + }, + }, + }, + ], +}) +``` + +Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. + +*** + +## 4. Create PostgreSQL and Redis Databases + +Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. + +If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). + +After hosting both databases, keep their connection URLs for the next steps. + +*** + +## 5. Deploy Medusa Application in Server Mode + +As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. + +The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +STORE_CORS= # STOREFRONT URL +ADMIN_CORS= # ADMIN URL +AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS +DISABLE_MEDUSA_ADMIN=false +MEDUSA_WORKER_MODE=server +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. +- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. +- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm install && npm run predeploy && npm run start +``` + +Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. + +### Set Backend URL in Admin Configuration + +The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. + +After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // ... + backendUrl: process.env.MEDUSA_BACKEND_URL, + }, +}) +``` + +Then, push the changes to the GitHub repository or deployed application. + +In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: + +```bash +ADMIN_CORS= # MEDUSA APPLICATION URL +AUTH_CORS= # ADD MEDUSA APPLICATION URL +MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION +``` + +Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. + +After setting the environment variables, make sure to restart the deployment for the changes to take effect. + +Remember to separate URLs in `AUTH_CORS` by commas. + +*** + +## 6. Deploy Medusa Application in Worker Mode + +Next, you'll deploy the Medusa application in worker mode. + +As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +DISABLE_MEDUSA_ADMIN=true +MEDUSA_WORKER_MODE=worker +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm run install && npm run start +``` + +*** + +## 7. Test Deployed Application + +Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. + +The Medusa Admin is also available at `/app`. + +*** + +## Create Admin User + +If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: + +```bash +npx medusa user -e admin-medusa@test.com -p supersecret +``` + +Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. + +You can use these credentials to log into the Medusa Admin dashboard. # Workflows @@ -4286,334 +4614,6 @@ You can now execute this workflow in a custom API route, scheduled job, or subsc Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows. -# Logging - -In this chapter, you’ll learn how to use Medusa’s logging utility. - -## Logger Class - -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. - -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. - -*** - -## How to Log a Message - -Resolve the `logger` using the Medusa container to log a message in your resource. - -For example, create the file `src/jobs/log-message.ts` with the following content: - -```ts title="src/jobs/log-message.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - logger.info("I'm using the logger!") -} - -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` - -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. - -### Test the Scheduled Job - -To test out the above scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, you'll see the following message as part of the logged messages: - -```text -info: I'm using the logger! -``` - -*** - -## Log Levels - -The `Logger` class has the following methods: - -- `info`: The message is logged with level `info`. -- `warn`: The message is logged with level `warn`. -- `error`: The message is logged with level `error`. -- `debug`: The message is logged with level `debug`. - -Each of these methods accepts a string parameter to log in the terminal with the associated level. - -*** - -## Logging Configurations - -### Log Level - -The available log levels, from lowest to highest levels, are: - -1. `silly` (default, meaning messages of all levels are logged) -2. `debug` -3. `info` -4. `warn` -5. `error` - -You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. - -For example: - -```bash -LOG_LEVEL=error -``` - -This logs `error` messages only. - -The environment variable must be set as a system environment variable and not in `.env`. - -### Save Logs in a File - -Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. - -For example: - -```bash -LOG_FILE=all.log -``` - -Your logs are now saved in the `all.log` file at the root of your Medusa application. - -The environment variable must be set as a system environment variable and not in `.env`. - -*** - -## Show Log with Progress - -The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. - -For example: - -```ts title="src/jobs/log-message.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - const activityId = logger.activity("First log message") - - logger.progress(activityId, `Second log message`) - - logger.success(activityId, "Last log message") -} -``` - -The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: - -- `progress`: Log a message of level `info` that indicates progress within that same activity. -- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. -- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. - -If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. - - -# Worker Mode of Medusa Instance - -In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. - -## What is Worker Mode? - -By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. - -In a production environment, you should deploy two separate instances of your Medusa application: - -1. A server instance that handles incoming requests to the application's API routes. -2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers. - -You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter. - -This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. - -![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) - -*** - -## How to Set Worker Mode - -You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: - -- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. -- `worker`: run a worker process only. -- `server`: run the application server only. - -Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. - -For example, set the worker mode in `medusa-config.ts` to the following: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", - // ... - }, - // ... -}) -``` - -You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. - -Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: - -### Server Medusa Instance - -```bash -WORKER_MODE=server -``` - -### Worker Medusa Instance - -```bash -WORKER_MODE=worker -``` - -### Disable Admin in Worker Mode - -Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. - -To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false, - }, - // ... -}) -``` - -Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. - -Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: - -### Server Medusa Instance - -```bash -ADMIN_DISABLED=false -``` - -### Worker Medusa Instance - -```bash -ADMIN_DISABLED=true -``` - - -# Medusa Testing Tools - -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. - -## @medusajs/test-utils Package - -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. - -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: - -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` - -*** - -## Install and Configure Jest - -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. - -Run the following command to install the required Jest dependencies: - -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest -``` - -Then, create the file `jest.config.js` with the following content: - -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) - -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], - }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} - -if (process.env.TEST_TYPE === "integration:http") { - module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] -} else if (process.env.TEST_TYPE === "integration:modules") { - module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] -} else if (process.env.TEST_TYPE === "unit") { - module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] -} -``` - -Next, create the `integration-tests/setup.js` file with the following content: - -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") - -MetadataStorage.clear() -``` - -*** - -## Add Test Commands - -Finally, add the following scripts to `package.json`: - -```json title="package.json" -"scripts": { - // ... - "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", - "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", - "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" -}, -``` - -You now have two commands: - -- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. -- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. -- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. - -*** - -## Test Tools and Writing Tests - -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. - - # Usage Information At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa. @@ -4704,6 +4704,429 @@ MEDUSA_FF_ANALYTICS=false ``` +# Next.js Starter Storefront + +The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. + +The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. + +In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). + +## Install Next.js Starter + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) + +If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: + +1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): + +```bash +git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront +``` + +2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: + +```bash npm2yarn +cd my-medusa-storefront +npm install +mv .env.template .env.local +``` + +3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys + +```bash +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... +``` + +4. While the Medusa application is running, start the Next.js Starter storefront: + +```bash npm2yarn +npm run dev +``` + +Your Next.js Starter storefront is now running at `http://localhost:8000`. + +*** + +## Customize Storefront + +To customize the storefront, refer to the following directories: + +- `src/app`: The storefront’s pages. +- `src/modules`: The storefront’s components. +- `src/styles`: The storefront’s styles. + +You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). + +*** + +## Configurations and Integrations + +The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. + +Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. + + +# Write Integration Tests + +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## medusaIntegrationTestRunner Utility + +The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. + +For example: + +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. + +`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: + +- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: + - `get`: Send a `GET` request to an API route. + - `post`: Send a `POST` request to an API route. + - `delete`: Send a `DELETE` request to an API route. +- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). + +### Jest Timeout + +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: + +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` + +*** + +### Run Tests + +Run the following command to run your tests: + +```bash npm2yarn +npm run test:integration +``` + +If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. + +*** + +## Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). + +*** + +## Example Integration Tests + +The next chapters provide examples of writing integration tests for API routes and workflows. + + +# Guide: Implement Brand Module + +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. + +A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. + +In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. + +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +## 1. Create Module Directory + +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. + +![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) + +*** + +## 2. Create Data Model + +A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). + +You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content: + +![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) + +```ts title="src/modules/brand/models/brand.ts" +import { model } from "@medusajs/framework/utils" + +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), +}) +``` + +You create a `Brand` data model which has an `id` primary key property, and a `name` text property. + +You define the data model using the `define` method of the DML. It accepts two parameters: + +1. The first one is the name of the data model's table in the database. Use snake-case names. +2. The second is an object, which is the data model's schema. + +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md). + +*** + +## 3. Create Module Service + +You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. + +In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. + +Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). + +You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: + +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) + +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { + +} + +export default BrandModuleService +``` + +The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. + +The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. + +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). + +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). + +*** + +## 4. Export Module Definition + +A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. + +So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: + +![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) + +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" + +export const BRAND_MODULE = "brand" + +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` + +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name (`brand`). You'll use this name when you use this module in other customizations. +2. An object with a required property `service` indicating the module's main service. + +You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. + +*** + +## 5. Add Module to Medusa's Configurations + +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. + +The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/brand", + }, + ], +}) +``` + +The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). + +*** + +## 6. Generate and Run Migrations + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. + +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). + +[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: + +```bash +npx medusa db:generate brand +npx medusa db:migrate +``` + +The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. + +*** + +## Next Step: Create Brand Workflow + +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. + +In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. + + +# Write Tests for Modules + +In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## moduleIntegrationTestRunner Utility + +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. + +For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: + +```ts title="src/modules/blog/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import Post from "../models/post" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./src/modules/blog", + testSuite: ({ service }) => { + // TODO write tests + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: + +- `moduleName`: The name of the module. +- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. +- `resolve`: The path to the module's directory. +- `testSuite`: A function that defines the tests to run. + +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. + +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). + +*** + +## Run Tests + +Run the following command to run your module integration tests: + +```bash npm2yarn +npm run test:integration:modules +``` + +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. + +*** + +## Pass Module Options + +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" + +moduleIntegrationTestRunner({ + moduleOptions: { + apiKey: "123", + }, + // ... +}) +``` + +*** + +## Write Tests for Modules without Data Models + +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" +import { model } from "@medusajs/framework/utils" + +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) + +moduleIntegrationTestRunner({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` + +*** + +### Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). + + # Guide: Create Brand API Route In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. @@ -4912,226 +5335,212 @@ Now that you have brands in your Medusa application, you want to associate a bra In the next chapters, you'll learn how to build associations between data models defined in different modules. -# Next.js Starter Storefront +# Guide: Create Brand Workflow -The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. +This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. -The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. +After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. -In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). +The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. -## Install Next.js Starter +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). ### Prerequisites -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) - -If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: - -1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): - -```bash -git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront -``` - -2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: - -```bash npm2yarn -cd my-medusa-storefront -npm install -mv .env.template .env.local -``` - -3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys - -```bash -NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... -``` - -4. While the Medusa application is running, start the Next.js Starter storefront: - -```bash npm2yarn -npm run dev -``` - -Your Next.js Starter storefront is now running at `http://localhost:8000`. +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) *** -## Customize Storefront +## 1. Create createBrandStep -To customize the storefront, refer to the following directories: +A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK -- `src/app`: The storefront’s pages. -- `src/modules`: The storefront’s components. -- `src/styles`: The storefront’s styles. +The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: -You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). +![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) -*** - -## Configurations and Integrations - -The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. - -Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. - - -# Guide: Implement Brand Module - -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. - -A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. - -In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. - -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -## 1. Create Module Directory - -Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. - -![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) - -*** - -## 2. Create Data Model - -A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. - -Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). - -You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content: - -![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) - -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" - -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) -``` - -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. - -You define the data model using the `define` method of the DML. It accepts two parameters: - -1. The first one is the name of the data model's table in the database. Use snake-case names. -2. The second is an object, which is the data model's schema. - -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md). - -*** - -## 3. Create Module Service - -You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. - -In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. - -Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). - -You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: - -![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) - -```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" - -class BrandModuleService extends MedusaService({ - Brand, -}) { +```ts title="src/workflows/create-brand.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { BRAND_MODULE } from "../modules/brand" +import BrandModuleService from "../modules/brand/service" +export type CreateBrandStepInput = { + name: string } -export default BrandModuleService +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brand = await brandModuleService.createBrands(input) + + return new StepResponse(brand, brand.id) + } +) ``` -The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. +You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. -The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. +The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. -You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). +So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. -*** +Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). -## 4. Export Module Definition +A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. -A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. +### Add Compensation Function to Step -So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: +You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. -![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) +Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: -export const BRAND_MODULE = "brand" - -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) -``` - -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: - -1. The module's name (`brand`). You'll use this name when you use this module in other customizations. -2. An object with a required property `service` indicating the module's main service. - -You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. - -*** - -## 5. Add Module to Medusa's Configurations - -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. - -The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], -}) + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(id) + } +) ``` -The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. + +In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. + +Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). + +So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. *** -## 6. Generate and Run Migrations +## 2. Create createBrandWorkflow -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. +You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). +Add the following content in the same `src/workflows/create-brand.ts` file: -[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +type CreateBrandWorkflowInput = { + name: string +} + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) + + return new WorkflowResponse(brand) + } +) +``` + +You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. + +The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. + +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. + +*** + +## Next Steps: Expose Create Brand API Route + +You now have a `createBrandWorkflow` that you can execute to create a brand. + +In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. + + +# Guide: Define Module Link Between Brand and Product + +In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. + +Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. + +A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. + +In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. + +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +### Prerequisites + +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +## 1. Define Link + +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. + +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: + +![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) + +```ts title="src/links/product-brand.ts" highlights={highlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` + +You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. + +The `defineLink` function accepts two parameters of the same type, which is either: + +- The data model's link configuration, which you access from the Module's `linkable` property; +- Or an object that has two properties: + - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. + - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. + +So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. + +*** + +## 2. Sync the Link to the Database + +A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: ```bash -npx medusa db:generate brand npx medusa db:migrate ``` -The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. +This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. + +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. *** -## Next Step: Create Brand Workflow +## Next Steps: Extend Create Product Flow -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. - -In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. +In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. # Create Brands UI Route in Admin @@ -5506,214 +5915,6 @@ Your customizations often span across systems, where you need to retrieve data o In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. -# Guide: Create Brand Workflow - -This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. - -After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. - -The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -*** - -## 1. Create createBrandStep - -A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK - -The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: - -![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) - -```ts title="src/workflows/create-brand.ts" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { BRAND_MODULE } from "../modules/brand" -import BrandModuleService from "../modules/brand/service" - -export type CreateBrandStepInput = { - name: string -} - -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const brand = await brandModuleService.createBrands(input) - - return new StepResponse(brand, brand.id) - } -) -``` - -You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. - -The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. - -The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. - -So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. - -Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). - -A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. - -### Add Compensation Function to Step - -You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. - -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: - -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.deleteBrands(id) - } -) -``` - -The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. - -In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. - -Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). - -So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. - -*** - -## 2. Create createBrandWorkflow - -You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. - -Add the following content in the same `src/workflows/create-brand.ts` file: - -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -type CreateBrandWorkflowInput = { - name: string -} - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) - - return new WorkflowResponse(brand) - } -) -``` - -You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. - -The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. - -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. - -*** - -## Next Steps: Expose Create Brand API Route - -You now have a `createBrandWorkflow` that you can execute to create a brand. - -In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. - - -# Guide: Define Module Link Between Brand and Product - -In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. - -Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. - -A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. - -In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. - -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -### Prerequisites - -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -## 1. Define Link - -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. - -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: - -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - -```ts title="src/links/product-brand.ts" highlights={highlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. - -The `defineLink` function accepts two parameters of the same type, which is either: - -- The data model's link configuration, which you access from the Module's `linkable` property; -- Or an object that has two properties: - - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. - - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. - -So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. - -*** - -## 2. Sync the Link to the Database - -A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: - -```bash -npx medusa db:migrate -``` - -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. - -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. - -*** - -## Next Steps: Extend Create Product Flow - -In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. - - # Guide: Add Product's Brand Widget in Admin In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. @@ -5868,154 +6069,6 @@ The [Admin Components guides](https://docs.medusajs.com/resources/admin-componen In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. -# Guide: Query Product's Brands - -In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. - -In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -*** - -## Approach 1: Retrieve Brands in Existing API Routes - -Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. - -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). - -For example, send the following request to retrieve the list of products with their brands: - -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -Any product that is linked to a brand will have a `brand` property in its object: - -```json title="Example Product Object" -{ - "id": "prod_123", - // ... - "brand": { - "id": "01JEB44M61BRM3ARM2RRMK7GJF", - "name": "Acme", - "created_at": "2024-12-05T09:59:08.737Z", - "updated_at": "2024-12-05T09:59:08.737Z", - "deleted_at": null - } -} -``` - -By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. - -### Limitations: Filtering by Brands in Existing API Routes - -While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. - -Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Approach 2: Use Query to Retrieve Linked Records - -You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. - -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: - -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) - - res.json({ brands }) -} -``` - -This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: - -- `entity`: The data model's name as specified in the first parameter of `model.define`. -- `fields`: An array of properties and relations to retrieve. You can pass: - - A property's name, such as `id`, or `*` for all properties. - - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. - -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. - -### Test it Out - -To test the API route out, send a `GET` request to `/admin/brands`: - -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -This returns the brands in your store with their linked products. For example: - -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` - -### Limitations: Filtering by Brand in Query - -While you can use Query to retrieve linked records, you can't filter by linked records. - -For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Summary - -By following the examples of the previous chapters, you: - -- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. -- Extended the create-product workflow and route to allow setting the product's brand while creating the product. -- Queried a product's brand, and vice versa. - -*** - -## Next Steps: Customize Medusa Admin - -Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. - -In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. - - # Guide: Extend Create Product Flow After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. @@ -6228,934 +6281,6 @@ In the Medusa application's logs, you'll find the message `Linked brand to produ Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. -# Guide: Schedule Syncing Brands from Third-Party - -In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. - -However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. - -You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. - -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). - -In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. - -### Prerequisites - -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) - -*** - -## 1. Implement Syncing Workflow - -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -This workflow will have three steps: - -1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. -2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. -3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. - -### retrieveBrandsFromCmsStep - -To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: - -![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) - -```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import CmsModuleService from "../modules/cms/service" -import { CMS_MODULE } from "../modules/cms" - -const retrieveBrandsFromCmsStep = createStep( - "retrieve-brands-from-cms", - async (_, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve( - CMS_MODULE - ) - - const brands = await cmsModuleService.retrieveBrands() - - return new StepResponse(brands) - } -) -``` - -You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. - -### createBrandsStep - -The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: - -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -// other imports... -import BrandModuleService from "../modules/brand/service" -import { BRAND_MODULE } from "../modules/brand" - -// ... - -type CreateBrand = { - name: string -} - -type CreateBrandsInput = { - brands: CreateBrand[] -} - -export const createBrandsStep = createStep( - "create-brands-step", - async (input: CreateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const brands = await brandModuleService.createBrands(input.brands) - - return new StepResponse(brands, brands) - }, - async (brands, { container }) => { - if (!brands) { - return - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) - } -) -``` - -The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. - -The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Update Brands Step - -The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: - -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} -// ... - -type UpdateBrand = { - id: string - name: string -} - -type UpdateBrandsInput = { - brands: UpdateBrand[] -} - -export const updateBrandsStep = createStep( - "update-brands-step", - async ({ brands }: UpdateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const prevUpdatedBrands = await brandModuleService.listBrands({ - id: brands.map((brand) => brand.id), - }) - - const updatedBrands = await brandModuleService.updateBrands(brands) - - return new StepResponse(updatedBrands, prevUpdatedBrands) - }, - async (prevUpdatedBrands, { container }) => { - if (!prevUpdatedBrands) { - return - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.updateBrands(prevUpdatedBrands) - } -) -``` - -The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. - -In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. - -### Create Workflow - -Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -// other imports... -import { - // ... - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -export const syncBrandsFromCmsWorkflow = createWorkflow( - "sync-brands-from-system", - () => { - const brands = retrieveBrandsFromCmsStep() - - // TODO create and update brands - } -) -``` - -In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. - -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. - -Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -So, replace the `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const { toCreate, toUpdate } = transform( - { - brands, - }, - (data) => { - const toCreate: CreateBrand[] = [] - const toUpdate: UpdateBrand[] = [] - - data.brands.forEach((brand) => { - if (brand.external_id) { - toUpdate.push({ - id: brand.external_id as string, - name: brand.name as string, - }) - } else { - toCreate.push({ - name: brand.name as string, - }) - } - }) - - return { toCreate, toUpdate } - } -) - -// TODO create and update the brands -``` - -`transform` accepts two parameters: - -1. The data to be passed to the function in the second parameter. -2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. - -In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. - -You now have the list of brands to create and update. So, replace the new `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const created = createBrandsStep({ brands: toCreate }) -const updated = updateBrandsStep({ brands: toUpdate }) - -return new WorkflowResponse({ - created, - updated, -}) -``` - -You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. - -Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. - -*** - -## 2. Schedule Syncing Task - -You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. - -A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) - -```ts title="src/jobs/sync-brands-from-cms.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" - -export default async function (container: MedusaContainer) { - const logger = container.resolve("logger") - - const { result } = await syncBrandsFromCmsWorkflow(container).run() - - logger.info( - `Synced brands from third-party system: ${ - result.created.length - } brands created and ${result.updated.length} brands updated.`) -} - -export const config = { - name: "sync-brands-from-system", - schedule: "0 0 * * *", // change to * * * * * for debugging -} -``` - -A scheduled job file must export: - -- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. -- An object of scheduled jobs configuration. It has two properties: - - `name`: A unique name for the scheduled job. - - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. - -Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. - -*** - -## Test it Out - -To test out the scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. - -*** - -## Summary - -By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems. - -With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. - - -# Guide: Sync Brands from Medusa to Third-Party - -In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. - -In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. - -Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. - -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. - -### Prerequisites - -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) - -## 1. Emit Event in createBrandWorkflow - -Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. - -Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: - -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -// ... - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... - - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) - - return new WorkflowResponse(brand) - } -) -``` - -The `emitEventStep` accepts an object parameter having two properties: - -- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. -- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. - -You'll learn how to handle this event in a later step. - -*** - -## 2. Create Sync to Third-Party System Workflow - -The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -You'll create a `syncBrandToSystemWorkflow` that has two steps: - -- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. -- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. - -### syncBrandToCmsStep - -To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Brand } from "../modules/brand/models/brand" -import { CMS_MODULE } from "../modules/cms" -import CmsModuleService from "../modules/cms/service" - -type SyncBrandToCmsStepInput = { - brand: InferTypeOf -} - -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.createBrand(brand) - - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } - - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.deleteBrand(id) - } -) -``` - -You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. - -You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Create Workflow - -You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -type SyncBrandToCmsWorkflowInput = { - id: string -} - -export const syncBrandToCmsWorkflow = createWorkflow( - "sync-brand-to-cms", - (input: SyncBrandToCmsWorkflowInput) => { - // @ts-ignore - const { data: brands } = useQueryGraphStep({ - entity: "brand", - fields: ["*"], - filters: { - id: input.id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) -``` - -You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: - -- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. -- `syncBrandToCmsStep`: Create the brand in the third-party CMS. - -You'll execute this workflow in the subscriber next. - -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). - -*** - -## 3. Handle brand.created Event - -You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. - -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: - -![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) - -```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} -import type { - SubscriberConfig, - SubscriberArgs, -} from "@medusajs/framework" -import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" - -export default async function brandCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncBrandToCmsWorkflow(container).run({ - input: data, - }) -} - -export const config: SubscriberConfig = { - event: "brand.created", -} -``` - -A subscriber file must export: - -- The asynchronous function that's executed when the event is emitted. This must be the file's default export. -- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. - -The subscriber function accepts an object parameter that has two properties: - -- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. -- `container`: The Medusa container used to resolve Framework and commerce tools. - -In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. - -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Test it Out - -To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. - -First, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: - -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` - -This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: - -```plain -info: Processing brand.created which has 1 subscribers -http: POST /admin/brands ← - (200) - 16.418 ms -info: Sending a POST request to /brands. -info: Request Data: { - "id": "01JEDWENYD361P664WRQPMC3J8", - "name": "Acme", - "created_at": "2024-12-06T11:42:32.909Z", - "updated_at": "2024-12-06T11:42:32.909Z", - "deleted_at": null -} -info: API Key: "123" -``` - -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa - -You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. - - -# Seed Data with Custom CLI Script - -In this chapter, you'll learn how to seed data using a custom CLI script. - -## How to Seed Data - -To seed dummy data for development or demo purposes, use a custom CLI script. - -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. - -### Example: Seed Dummy Products - -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. - -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: - -```bash npm2yarn -npm install --save-dev @faker-js/faker -``` - -Then, create the file `src/scripts/demo-products.ts` with the following content: - -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) - - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) - - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 - - // TODO seed products -} -``` - -So far, in the script, you: - -- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. -- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. -- Initialize some default data to use when seeding the products next. - -Next, replace the `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const productsData = new Array(productsNum).fill(0).map((_, index) => { - const title = faker.commerce.product() + "_" + index - return { - title, - is_giftcard: true, - description: faker.commerce.productDescription(), - status: ProductStatus.PUBLISHED, - options: [ - { - title: "Size", - values: sizeOptions, - }, - { - title: "Color", - values: colorOptions, - }, - ], - images: [ - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - ], - variants: new Array(10).fill(0).map((_, variantIndex) => ({ - title: `${title} ${variantIndex}`, - sku: `variant-${variantIndex}${index}`, - prices: new Array(10).fill(0).map((_, priceIndex) => ({ - currency_code, - amount: 10 * priceIndex, - })), - options: { - Size: sizeOptions[Math.floor(Math.random() * 3)], - }, - })), - shipping_profile_id: "sp_123", - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } -}) - -// TODO seed products -``` - -You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. - -Then, replace the new `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) - -logger.info(`Seeded ${products.length} products.`) - -// TODO add inventory levels -``` - -You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. - -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") - -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) - -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) - -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) - -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) - -logger.info("Finished seeding inventory levels data.") -``` - -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. - -Then, you generate inventory levels for each inventory item, associating it with the first stock location. - -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. - -### Test Script - -To test out the script, run the following command in your project's directory: - -```bash -npx medusa exec ./src/scripts/demo-products.ts -``` - -This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. - - -# Guide: Integrate Third-Party Brand System - -In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. - -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -## 1. Create Module Directory - -You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. - -![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) - -*** - -## 2. Create Module Service - -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. - -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: - -![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) - -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" - -export type ModuleOptions = { - apiKey: string -} - -type InjectedDependencies = { - logger: Logger - configModule: ConfigModule -} - -class CmsModuleService { - private options_: ModuleOptions - private logger_: Logger - - constructor({ logger }: InjectedDependencies, options: ModuleOptions) { - this.logger_ = logger - this.options_ = options - - // TODO initialize SDK - } -} - -export default CmsModuleService -``` - -You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: - -1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. -2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. - -When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. - -### Integration Methods - -Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. - -Add the following methods in the `CmsModuleService`: - -```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} -export class CmsModuleService { - // ... - - // a dummy method to simulate sending a request, - // in a realistic scenario, you'd use an SDK, fetch, or axios clients - private async sendRequest(url: string, method: string, data?: any) { - this.logger_.info(`Sending a ${method} request to ${url}.`) - this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) - this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) - } - - async createBrand(brand: Record) { - await this.sendRequest("/brands", "POST", brand) - } - - async deleteBrand(id: string) { - await this.sendRequest(`/brands/${id}`, "DELETE") - } - - async retrieveBrands(): Promise[]> { - await this.sendRequest("/brands", "GET") - - return [] - } -} -``` - -The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. - -You also add three methods that use the `sendRequest` method: - -- `createBrand` that creates a brand in the third-party system. -- `deleteBrand` that deletes the brand in the third-party system. -- `retrieveBrands` to retrieve a brand from the third-party system. - -*** - -## 3. Export Module Definition - -After creating the module's service, you'll export the module definition indicating the module's name and service. - -Create the file `src/modules/cms/index.ts` with the following content: - -![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) - -```ts title="src/modules/cms/index.ts" -import { Module } from "@medusajs/framework/utils" -import CmsModuleService from "./service" - -export const CMS_MODULE = "cms" - -export default Module(CMS_MODULE, { - service: CmsModuleService, -}) -``` - -You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. - -*** - -## 4. Add Module to Medusa's Configurations - -Finally, add the module to the Medusa configurations at `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - // ... - { - resolve: "./src/modules/cms", - options: { - apiKey: process.env.CMS_API_KEY, - }, - }, - ], -}) -``` - -The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. - -You can add the `CMS_API_KEY` environment variable to `.env`: - -```bash -CMS_API_KEY=123 -``` - -*** - -## Next Steps: Sync Brand From Medusa to CMS - -You can now use the CMS Module's service to perform actions on the third-party CMS. - -In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. - - # Environment Variables in Admin Customizations In this chapter, you'll learn how to use environment variables in your admin customizations. @@ -7278,6 +6403,154 @@ export const config = defineWidgetConfig({ ``` +# Guide: Query Product's Brands + +In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. + +In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +*** + +## Approach 1: Retrieve Brands in Existing API Routes + +Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. + +Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. + +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +Any product that is linked to a brand will have a `brand` property in its object: + +```json title="Example Product Object" +{ + "id": "prod_123", + // ... + "brand": { + "id": "01JEB44M61BRM3ARM2RRMK7GJF", + "name": "Acme", + "created_at": "2024-12-05T09:59:08.737Z", + "updated_at": "2024-12-05T09:59:08.737Z", + "deleted_at": null + } +} +``` + +By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. + +### Limitations: Filtering by Brands in Existing API Routes + +While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. + +Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Approach 2: Use Query to Retrieve Linked Records + +You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. + +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: + +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) + + res.json({ brands }) +} +``` + +This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: + +- `entity`: The data model's name as specified in the first parameter of `model.define`. +- `fields`: An array of properties and relations to retrieve. You can pass: + - A property's name, such as `id`, or `*` for all properties. + - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. + +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. + +### Test it Out + +To test the API route out, send a `GET` request to `/admin/brands`: + +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +This returns the brands in your store with their linked products. For example: + +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` + +### Limitations: Filtering by Brand in Query + +While you can use Query to retrieve linked records, you can't filter by linked records. + +For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Summary + +By following the examples of the previous chapters, you: + +- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. +- Extended the create-product workflow and route to allow setting the product's brand while creating the product. +- Queried a product's brand, and vice versa. + +*** + +## Next Steps: Customize Medusa Admin + +Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. + +In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. + + # Admin Routing Customizations The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components. @@ -7431,137 +6704,6 @@ export const handle = { Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. -# Admin Development Tips - -In this chapter, you'll find some tips for your admin development. - -## Send Requests to API Routes - -To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: - -```ts -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). - -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. - -For example: - -### Query - -```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" - -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) - -export default ProductWidget -``` - -### Mutation - -```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useMutation } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" - -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) - - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - - - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). - -### Use Route Loaders for Initial Data - -You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). - -*** - -## Global Variables in Admin Customizations - -In your admin customizations, you can use the following global variables: - -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. -- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. -- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. - -*** - -## Admin Translations - -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. - -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). - - # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -7917,6 +7059,1065 @@ Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injec To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. +# Guide: Sync Brands from Medusa to Third-Party + +In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. + +In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. + +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. + +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. + +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +## 1. Emit Event in createBrandWorkflow + +Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. + +Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: + +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +// ... + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... + + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) + + return new WorkflowResponse(brand) + } +) +``` + +The `emitEventStep` accepts an object parameter having two properties: + +- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. +- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. + +You'll learn how to handle this event in a later step. + +*** + +## 2. Create Sync to Third-Party System Workflow + +The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. + +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +You'll create a `syncBrandToSystemWorkflow` that has two steps: + +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. +- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. + +### syncBrandToCmsStep + +To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" +import { CMS_MODULE } from "../modules/cms" +import CmsModuleService from "../modules/cms/service" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf +} + +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.createBrand(brand) + + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.deleteBrand(id) + } +) +``` + +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. + +You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. + +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +### Create Workflow + +You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +type SyncBrandToCmsWorkflowInput = { + id: string +} + +export const syncBrandToCmsWorkflow = createWorkflow( + "sync-brand-to-cms", + (input: SyncBrandToCmsWorkflowInput) => { + // @ts-ignore + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: ["*"], + filters: { + id: input.id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: + +- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. +- `syncBrandToCmsStep`: Create the brand in the third-party CMS. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: + +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) + +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} +import type { + SubscriberConfig, + SubscriberArgs, +} from "@medusajs/framework" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" + +export default async function brandCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await syncBrandToCmsWorkflow(container).run({ + input: data, + }) +} + +export const config: SubscriberConfig = { + event: "brand.created", +} +``` + +A subscriber file must export: + +- The asynchronous function that's executed when the event is emitted. This must be the file's default export. +- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. + +The subscriber function accepts an object parameter that has two properties: + +- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. +- `container`: The Medusa container used to resolve Framework and commerce tools. + +In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. + +First, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: + +```plain +info: Processing brand.created which has 1 subscribers +http: POST /admin/brands ← - (200) - 16.418 ms +info: Sending a POST request to /brands. +info: Request Data: { + "id": "01JEDWENYD361P664WRQPMC3J8", + "name": "Acme", + "created_at": "2024-12-06T11:42:32.909Z", + "updated_at": "2024-12-06T11:42:32.909Z", + "deleted_at": null +} +info: API Key: "123" +``` + +*** + +## Next Chapter: Sync Brand from Third-Party CMS to Medusa + +You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. + + +# Admin Development Tips + +In this chapter, you'll find some tips for your admin development. + +## Send Requests to API Routes + +To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: + +```ts +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). + +Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. + +For example: + +### Query + +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + + {isLoading && Loading...} + {data?.products && ( +
    + {data.products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) + +export default ProductWidget +``` + +### Mutation + +```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) + + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + + + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). + +### Use Route Loaders for Initial Data + +You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). + +*** + +## Global Variables in Admin Customizations + +In your admin customizations, you can use the following global variables: + +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. +- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. +- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. + +*** + +## Admin Translations + +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. + +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). + + +# Guide: Schedule Syncing Brands from Third-Party + +In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. + +However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. + +You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. + +Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + +In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. + +### Prerequisites + +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +*** + +## 1. Implement Syncing Workflow + +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. + +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +This workflow will have three steps: + +1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. +2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. +3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. + +### retrieveBrandsFromCmsStep + +To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: + +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) + +```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import CmsModuleService from "../modules/cms/service" +import { CMS_MODULE } from "../modules/cms" + +const retrieveBrandsFromCmsStep = createStep( + "retrieve-brands-from-cms", + async (_, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve( + CMS_MODULE + ) + + const brands = await cmsModuleService.retrieveBrands() + + return new StepResponse(brands) + } +) +``` + +You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. + +### createBrandsStep + +The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +// other imports... +import BrandModuleService from "../modules/brand/service" +import { BRAND_MODULE } from "../modules/brand" + +// ... + +type CreateBrand = { + name: string +} + +type CreateBrandsInput = { + brands: CreateBrand[] +} + +export const createBrandsStep = createStep( + "create-brands-step", + async (input: CreateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brands = await brandModuleService.createBrands(input.brands) + + return new StepResponse(brands, brands) + }, + async (brands, { container }) => { + if (!brands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + } +) +``` + +The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. + +The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. + +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +### Update Brands Step + +The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} +// ... + +type UpdateBrand = { + id: string + name: string +} + +type UpdateBrandsInput = { + brands: UpdateBrand[] +} + +export const updateBrandsStep = createStep( + "update-brands-step", + async ({ brands }: UpdateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const prevUpdatedBrands = await brandModuleService.listBrands({ + id: brands.map((brand) => brand.id), + }) + + const updatedBrands = await brandModuleService.updateBrands(brands) + + return new StepResponse(updatedBrands, prevUpdatedBrands) + }, + async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.updateBrands(prevUpdatedBrands) + } +) +``` + +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. + +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. + +### Create Workflow + +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... +import { + // ... + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +export const syncBrandsFromCmsWorkflow = createWorkflow( + "sync-brands-from-system", + () => { + const brands = retrieveBrandsFromCmsStep() + + // TODO create and update brands + } +) +``` + +In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. + +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. + +Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +So, replace the `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const { toCreate, toUpdate } = transform( + { + brands, + }, + (data) => { + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] + + data.brands.forEach((brand) => { + if (brand.external_id) { + toUpdate.push({ + id: brand.external_id as string, + name: brand.name as string, + }) + } else { + toCreate.push({ + name: brand.name as string, + }) + } + }) + + return { toCreate, toUpdate } + } +) + +// TODO create and update the brands +``` + +`transform` accepts two parameters: + +1. The data to be passed to the function in the second parameter. +2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. + +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. + +You now have the list of brands to create and update. So, replace the new `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const created = createBrandsStep({ brands: toCreate }) +const updated = updateBrandsStep({ brands: toUpdate }) + +return new WorkflowResponse({ + created, + updated, +}) +``` + +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. + +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. + +*** + +## 2. Schedule Syncing Task + +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. + +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) + +```ts title="src/jobs/sync-brands-from-cms.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" + +export default async function (container: MedusaContainer) { + const logger = container.resolve("logger") + + const { result } = await syncBrandsFromCmsWorkflow(container).run() + + logger.info( + `Synced brands from third-party system: ${ + result.created.length + } brands created and ${result.updated.length} brands updated.`) +} + +export const config = { + name: "sync-brands-from-system", + schedule: "0 0 * * *", // change to * * * * * for debugging +} +``` + +A scheduled job file must export: + +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. + +The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. + +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. + +*** + +## Test it Out + +To test out the scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. + +*** + +## Summary + +By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems. + +With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. + + +# Seed Data with Custom CLI Script + +In this chapter, you'll learn how to seed data using a custom CLI script. + +## How to Seed Data + +To seed dummy data for development or demo purposes, use a custom CLI script. + +In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. + +### Example: Seed Dummy Products + +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. + +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: + +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` + +Then, create the file `src/scripts/demo-products.ts` with the following content: + +```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { ExecArgs } from "@medusajs/framework/types" +import { faker } from "@faker-js/faker" +import { + ContainerRegistrationKeys, + Modules, + ProductStatus, +} from "@medusajs/framework/utils" +import { + createInventoryLevelsWorkflow, + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export default async function seedDummyProducts({ + container, +}: ExecArgs) { + const salesChannelModuleService = container.resolve( + Modules.SALES_CHANNEL + ) + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + const query = container.resolve( + ContainerRegistrationKeys.QUERY + ) + + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) + + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 + + // TODO seed products +} +``` + +So far, in the script, you: + +- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. +- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. +- Initialize some default data to use when seeding the products next. + +Next, replace the `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const productsData = new Array(productsNum).fill(0).map((_, index) => { + const title = faker.commerce.product() + "_" + index + return { + title, + is_giftcard: true, + description: faker.commerce.productDescription(), + status: ProductStatus.PUBLISHED, + options: [ + { + title: "Size", + values: sizeOptions, + }, + { + title: "Color", + values: colorOptions, + }, + ], + images: [ + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + ], + variants: new Array(10).fill(0).map((_, variantIndex) => ({ + title: `${title} ${variantIndex}`, + sku: `variant-${variantIndex}${index}`, + prices: new Array(10).fill(0).map((_, priceIndex) => ({ + currency_code, + amount: 10 * priceIndex, + })), + options: { + Size: sizeOptions[Math.floor(Math.random() * 3)], + }, + })), + shipping_profile_id: "sp_123", + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } +}) + +// TODO seed products +``` + +You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. + +Then, replace the new `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) + +logger.info(`Seeded ${products.length} products.`) + +// TODO add inventory levels +``` + +You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. + +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") + +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) + +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: ["id"], +}) + +const inventoryLevels = inventoryItems.map((inventoryItem) => ({ + location_id: stockLocations[0].id, + stocked_quantity: 1000000, + inventory_item_id: inventoryItem.id, +})) + +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, +}) + +logger.info("Finished seeding inventory levels data.") +``` + +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. + +Then, you generate inventory levels for each inventory item, associating it with the first stock location. + +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. + +### Test Script + +To test out the script, run the following command in your project's directory: + +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` + +This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. + + +# Guide: Integrate Third-Party Brand System + +In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. + +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +## 1. Create Module Directory + +You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. + +![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) + +*** + +## 2. Create Module Service + +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. + +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: + +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) + +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" + +export type ModuleOptions = { + apiKey: string +} + +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule +} + +class CmsModuleService { + private options_: ModuleOptions + private logger_: Logger + + constructor({ logger }: InjectedDependencies, options: ModuleOptions) { + this.logger_ = logger + this.options_ = options + + // TODO initialize SDK + } +} + +export default CmsModuleService +``` + +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: + +1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. +2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. + +When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. + +### Integration Methods + +Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. + +Add the following methods in the `CmsModuleService`: + +```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} +export class CmsModuleService { + // ... + + // a dummy method to simulate sending a request, + // in a realistic scenario, you'd use an SDK, fetch, or axios clients + private async sendRequest(url: string, method: string, data?: any) { + this.logger_.info(`Sending a ${method} request to ${url}.`) + this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) + } + + async createBrand(brand: Record) { + await this.sendRequest("/brands", "POST", brand) + } + + async deleteBrand(id: string) { + await this.sendRequest(`/brands/${id}`, "DELETE") + } + + async retrieveBrands(): Promise[]> { + await this.sendRequest("/brands", "GET") + + return [] + } +} +``` + +The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. + +You also add three methods that use the `sendRequest` method: + +- `createBrand` that creates a brand in the third-party system. +- `deleteBrand` that deletes the brand in the third-party system. +- `retrieveBrands` to retrieve a brand from the third-party system. + +*** + +## 3. Export Module Definition + +After creating the module's service, you'll export the module definition indicating the module's name and service. + +Create the file `src/modules/cms/index.ts` with the following content: + +![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) + +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" + +export const CMS_MODULE = "cms" + +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) +``` + +You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. + +*** + +## 4. Add Module to Medusa's Configurations + +Finally, add the module to the Medusa configurations at `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... + { + resolve: "./src/modules/cms", + options: { + apiKey: process.env.CMS_API_KEY, + }, + }, + ], +}) +``` + +The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. + +You can add the `CMS_API_KEY` environment variable to `.env`: + +```bash +CMS_API_KEY=123 +``` + +*** + +## Next Steps: Sync Brand From Medusa to CMS + +You can now use the CMS Module's service to perform actions on the third-party CMS. + +In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. + + # Pass Additional Data to Medusa's API Route In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. @@ -8228,6 +8429,49 @@ export default defineMiddlewares({ This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. +# HTTP Methods + +In this chapter, you'll learn about how to add new API routes for each HTTP method. + +## HTTP Method Handler + +An API route is created for every HTTP method you export a handler function for in a route file. + +Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. + +For example, create the file `src/api/hello-world/route.ts` with the following content: + +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[POST] Hello world!", + }) +} +``` + +This adds two API Routes: + +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. + + # Throwing and Handling Errors In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. @@ -8335,194 +8579,6 @@ The `errorHandler` property's value is a function that accepts four parameters: This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. -# HTTP Methods - -In this chapter, you'll learn about how to add new API routes for each HTTP method. - -## HTTP Method Handler - -An API route is created for every HTTP method you export a handler function for in a route file. - -Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. - -For example, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[POST] Hello world!", - }) -} -``` - -This adds two API Routes: - -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. - - -# API Route Parameters - -In this chapter, you’ll learn about path, query, and request body parameters. - -## Path Parameters - -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. - -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` - -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. - -### Multiple Path Parameters - -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. - -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` - -You access the `id` and `name` path parameters using the `req.params` property. - -*** - -## Query Parameters - -You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, - }) -} -``` - -The value of `req.query.name` is the value passed in `?name=John`, for example. - -### Validate Query Parameters - -You can apply validation rules on received query parameters to ensure they match specified rules and types. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). - -*** - -## Request Body Parameters - -The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. - -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -type HelloWorldReq = { - name: string -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, - }) -} -``` - -In this example, you use the `name` request body parameter to create the message in the returned response. - -The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. - -To test it out, send the following request to your Medusa application: - -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` - -This returns the following JSON object: - -```json -{ - "message": "[POST] Hello John!" -} -``` - -### Validate Body Parameters - -You can apply validation rules on received body parameters to ensure they match specified rules and types. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). - - # Middlewares In this chapter, you’ll learn about middlewares and how to create them. @@ -9244,6 +9300,151 @@ export const GET = async ( In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. +# API Route Parameters + +In this chapter, you’ll learn about path, query, and request body parameters. + +## Path Parameters + +To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. + +For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: + +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) +} +``` + +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. + +### Multiple Path Parameters + +To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. + +For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: + +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} +``` + +You access the `id` and `name` path parameters using the `req.params` property. + +*** + +## Query Parameters + +You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. + +For example: + +```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `Hello ${req.query.name}`, + }) +} +``` + +The value of `req.query.name` is the value passed in `?name=John`, for example. + +### Validate Query Parameters + +You can apply validation rules on received query parameters to ensure they match specified rules and types. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). + +*** + +## Request Body Parameters + +The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. + +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). + +For example: + +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +type HelloWorldReq = { + name: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} +``` + +In this example, you use the `name` request body parameter to create the message in the returned response. + +The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. + +To test it out, send the following request to your Medusa application: + +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' +``` + +This returns the following JSON object: + +```json +{ + "message": "[POST] Hello John!" +} +``` + +### Validate Body Parameters + +You can apply validation rules on received body parameters to ensure they match specified rules and types. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). + + # API Route Response In this chapter, you'll learn how to send a response in your API route. @@ -9346,6 +9547,160 @@ This API route opens a stream by setting the `Content-Type` in the header to `te The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. +# Add Data Model Check Constraints + +In this chapter, you'll learn how to add check constraints to your data model. + +## What is a Check Constraint? + +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. + +For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. + +*** + +## How to Set a Check Constraint? + +To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. + +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: + +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + (columns) => `${columns.price} >= 0`, +]) +``` + +The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. + +The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. + +You can also pass an object to the `checks` method: + +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) +``` + +The object accepts the following properties: + +- `name`: The check constraint's name. +- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). + +*** + +## Apply in Migrations + +After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. + +To generate a migration for the data model's module then reflect it on the database, run the following command: + +```bash +npx medusa db:generate custom_module +npx medusa db:migrate +``` + +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. + + +# Retrieve Custom Links from Medusa's API Route + +In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. + +## Why Retrieve Custom Linked Data Models? + +Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. + +For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model. + +When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. + +*** + +## How to Retrieve Custom Linked Data Models Using `fields`? + +Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. + +For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid): + +```bash +curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ +-H 'Authorization: Bearer {access_token}' +``` + +The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations). + +By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. + +*** + +## API Routes that Restrict Retrievable Fields + +Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. + +The API routes that restrict the fields and relations you can retrieve are: + +- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers) +- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers) +- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories) + +### How to Override Allowed Fields and Relations + +For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes. + +For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: + +Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { defineMiddlewares } from "@medusajs/medusa"; + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: "GET", + middlewares: [ + (req, res, next) => { + req.allowed?.push("b2b_company"); + next(); + }, + ], + }, + ], +}); +``` + +In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid). + +The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. + +You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid): + +```bash +curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ +-H 'Authorization: Bearer {access_token}' +``` + +In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. + + # Request Body and Query Parameter Validation In this chapter, you'll learn how to validate request body and query parameters in your custom API route. @@ -9595,46 +9950,6 @@ For example, if you omit the `a` parameter, you'll receive a `400` response code To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). -# Infer Type of Data Model - -In this chapter, you'll learn how to infer the type of a data model. - -## How to Infer Type of Data Model? - -Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. - -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. - -For example: - -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../modules/blog/models/post" // relative path to the model - -export type Post = InferTypeOf -``` - -`InferTypeOf` accepts as a type argument the type of the data model. - -Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. - -You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: - -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../models/post" - -type Post = InferTypeOf - -class BlogModuleService extends MedusaService({ Post }) { - async doSomething(): Promise { - // ... - } -} -``` - - # Data Model Database Index In this chapter, you’ll learn how to define a database index on a data model. @@ -9747,295 +10062,45 @@ export default MyCustom This creates a unique composite index on the `name` and `age` properties. -# Manage Relationships +# Infer Type of Data Model -In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. +In this chapter, you'll learn how to infer the type of a data model. -## Manage One-to-One Relationship +## How to Infer Type of Data Model? -### BelongsTo Side of One-to-One +Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. -When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. - -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: - -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) - -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", -}) -``` - -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. - -### HasOne Side - -When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. - -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: - -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) - -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` - -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. - -*** - -## Manage One-to-Many Relationship - -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. - -When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. - -For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: - -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) - -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", -}) -``` - -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. - -*** - -## Manage Many-to-Many Relationship - -If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. - -### Create Associations - -When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: - -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) - -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], -}) -``` - -In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. - -### Update Associations - -When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. - -However, this removes any existing associations to records whose IDs aren't included in the array. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: - -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` - -If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. - -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: - -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) - -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", - ], -}) -``` - -This keeps existing associations between the product and orders, and adds a new one. - -*** - -## Manage Many-to-Many Relationship with pivotEntity - -If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. - -If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. - -For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: - -```ts highlights={["4"]} -class BlogModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` - -This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. For example: ```ts -// create order-product association -const orderProduct = await blogModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../modules/blog/models/post" // relative path to the model -// update order-product association -const orderProduct = await blogModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) - -// delete order-product association -await blogModuleService.deleteOrderProducts("123") +export type Post = InferTypeOf ``` -Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. +`InferTypeOf` accepts as a type argument the type of the data model. -Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. +Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. -*** +You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: -## Retrieve Records of Relation +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../models/post" -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. +type Post = InferTypeOf -To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: - -```ts highlights={retrieveHighlights} -const product = await blogModuleService.retrieveProducts( - "123", - { - relations: ["orders"], +class BlogModuleService extends MedusaService({ Post }) { + async doSomething(): Promise { + // ... } -) +} ``` -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. - - -# Add Data Model Check Constraints - -In this chapter, you'll learn how to add check constraints to your data model. - -## What is a Check Constraint? - -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. - -For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. - -*** - -## How to Set a Check Constraint? - -To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. - -For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: - -```ts highlights={checks1Highlights} -import { model } from "@medusajs/framework/utils" - -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` - -The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. - -The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. - -You can also pass an object to the `checks` method: - -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" - -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) -``` - -The object accepts the following properties: - -- `name`: The check constraint's name. -- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). - -*** - -## Apply in Migrations - -After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. - -To generate a migration for the data model's module then reflect it on the database, run the following command: - -```bash -npx medusa db:generate custom_module -npx medusa db:migrate -``` - -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. - # Data Model Properties @@ -10389,581 +10454,222 @@ const posts = await blogModuleService.listPosts({ This retrieves records that include `New Products` in their `title` property. -# Migrations +# Manage Relationships -In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. +In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. -## What is a Migration? +## Manage One-to-One Relationship -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. +### BelongsTo Side of One-to-One -The migration's file has a class with two methods: +When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: -*** +```ts highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user_id: "123", +}) -## Generate Migration - -Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. - -For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: - -```bash -npx medusa db:generate blog -``` - -This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). - -*** - -## Write a Migration Manually - -You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. - -For example: - -```ts title="src/modules/blog/migrations/Migration202507021059.ts" -import { Migration } from "@mikro-orm/migrations" - -export class Migration202507021059 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } - -} -``` - -The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. - -In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. - -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. - -*** - -## Run the Migration - -To run your migration, run the following command: - -This command also syncs module links. If you don't want that, use the `--skip-links` option. - -```bash -npx medusa db:migrate -``` - -This reflects the changes in the database as implemented in the migration's `up` method. - -*** - -## Rollback the Migration - -To rollback or revert the last migration you ran for a module, run the following command: - -```bash -npx medusa db:rollback blog -``` - -This rolls back the last ran migration on the Blog Module. - -### Caution: Rollback Migration before Deleting - -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. - -So, always rollback the migration before deleting it. - -*** - -## More Database Commands - -To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). - - -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload - -When events are emitted, they’re emitted with a data payload. - -The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. - -For example: - -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" - -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} - -export const config: SubscriberConfig = { - event: "product.created", -} -``` - -The `event` object has the following properties: - -- data: (\`object\`) The data payload of the event. Its properties are different for each event. -- name: (string) The name of the triggered event. -- metadata: (\`object\`) Additional data and context of the emitted event. - -This logs the product ID received in the `product.created` event’s data payload to the console. - -{/* --- - -## List of Events with Data Payload - -Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} - - -# Create a Plugin - -In this chapter, you'll learn how to create a Medusa plugin and publish it. - -A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## 1. Create a Plugin Project - -Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. - -Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: - -```bash -npx create-medusa-app my-plugin --plugin -``` - -This will create a new Medusa plugin project in the `my-plugin` directory. - -### Plugin Directory Structure - -After the installation is done, the plugin structure will look like this: - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -- `src/`: Contains the Medusa customizations. -- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. -- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -- `src/provider`: Contains [module providers](#create-module-providers). -- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. -- `package.json`: Contains the plugin's package information, including general information and dependencies. -- `tsconfig.json`: Contains the TypeScript configuration for the plugin. - -*** - -## 2. Prepare Plugin - -### Package Name - -Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. - -For example: - -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` - -### Package Keywords - -Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. - -Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. - -```json title="package.json" -{ - "keywords": [ - "medusa-plugin-integration", - "medusa-v2" - ], - // ... -} -``` - -In addition, make sure to use one of the following keywords based on your integration type: - -|Keyword|Description|Example| -|---|---|---| -|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| -|\`medusa-plugin-auth\`|Authentication service integration|Auth0| -|\`medusa-plugin-cms\`|CMS service integration|Contentful| -|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| -|\`medusa-plugin-payment\`|Payment service integration|PayPal| -|\`medusa-plugin-search\`|Search service integration|MeiliSearch| -|\`medusa-plugin-shipping\`|Shipping service integration|DHL| -|\`medusa-plugin-other\`|Other third-party integrations|Sentry| - -### Package Dependencies - -Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. - -In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. - -For example, assuming `2.5.0` is the latest Medusa version: - -```json title="package.json" -{ - "devDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/ui": "4.0.4", - "@medusajs/icons": "2.5.0", - "@swc/core": "1.5.7", - }, - "peerDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/ui": "4.0.3", - "@medusajs/icons": "2.5.0", - } -} -``` - -### Package Exports - -Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. - -In the `package.json` file, make sure your plugin has the following exports: - -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./admin": { - "import": "./.medusa/server/src/admin/index.mjs", - "require": "./.medusa/server/src/admin/index.js", - "default": "./.medusa/server/src/admin/index.js" - }, - "./*": "./.medusa/server/src/*.js" - } -} -``` - -Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. - -The plugin exports the following files and directories: - -- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. -- `./workflows`: The workflows exported in `./src/workflows/index.ts`. -- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. -- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. -- `./admin`: The admin extensions exported in `./src/admin/index.ts`. -- `./*`: Any other files in the plugin's `src` directory. - -*** - -## 3. Publish Plugin Locally for Development and Testing - -Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. - -### Publish and Install Local Package - -### Prerequisites - -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) - -The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. - -To publish the plugin to the local registry, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, navigate to your Medusa application: - -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` - -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. - -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: - -```bash npm2yarn title="Medusa application" -npm install --save-dev yalc -``` - -After that, run the following Medusa CLI command to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` - -Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. - -### Register Plugin in Medusa Application - -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. - -Add the plugin to the `plugins` array in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user_id: "123", }) ``` -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. -#### Pass Module Options through Plugin +### HasOne Side -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. +When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. -For example: +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) + +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", }) ``` -The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - -### Watch Plugin Changes During Development - -While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. - -To do that, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -This command will: - -- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. -- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. - -### Start Medusa Application - -You can start your Medusa application's development server to test out your plugin: - -```bash npm2yarn title="Medusa application" -npm run dev -``` - -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. *** -## 4. Create Customizations in the Plugin +## Manage One-to-Many Relationship -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) -- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) -- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) +When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. -While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). +For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: -### Generating Migrations for Modules +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) -During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: - -```plain title="Plugin project" -DB_USERNAME=postgres -DB_PASSWORD=123... -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=db_name +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) ``` -You can add these environment variables in a `.env` file in your plugin project. The variables are: +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. -- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. -- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. -- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. -- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. -- `DB_NAME`: The name of the PostgreSQL database to connect to. +*** -Then, run the following command in your plugin project to generate migrations for the modules in your plugin: +## Manage Many-to-Many Relationship -```bash title="Plugin project" -npx medusa plugin:db:generate +If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. + +### Create Associations + +When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. + +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: + +```ts highlights={manyHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + orders: ["123", "321"], +}) + +// when creating an order +const order = await helloModuleService.createOrders({ + id: "321", + // other properties... + products: ["123", "321"], +}) ``` -This command generates migrations for all modules in the plugin. +In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. -Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: +### Update Associations -```bash title="Medusa application" -npx medusa db:migrate -``` +When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. -The migrations in your application, including your plugin, will run and update the database. +However, this removes any existing associations to records whose IDs aren't included in the array. -### Importing Module Resources - -In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. - -These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. - -For example, to import the plugin's workflow in your Medusa application: - -`@myorg/plugin-name` is the plugin package's name. +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: ```ts -import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" -import BlogModule from "@myorg/plugin-name/modules/blog" -// import other files created in plugin like ./src/types/blog.ts -import BlogType from "@myorg/plugin-name/types/blog" +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) ``` -### Create Module Providers +If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. -The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: -For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } +) -`@myorg/plugin-name` is the plugin package's name. - -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@myorg/plugin-name/providers/my-notification", - id: "my-notification", - options: { - channels: ["email"], - // provider options... - }, - }, - ], - }, - }, +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", ], }) ``` -You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. - -To learn how to create module providers, refer to the following guides: - -- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) -- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) -- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) -- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) -- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) -- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) +This keeps existing associations between the product and orders, and adds a new one. *** -## 5. Publish Plugin to NPM +## Manage Many-to-Many Relationship with pivotEntity -Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. +If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. -Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: +If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. -```bash -npx medusa plugin:build +For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: + +```ts highlights={["4"]} +class BlogModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} ``` -The command will compile an output in the `.medusa/server` directory. +This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. -You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: +For example: -```bash -npm publish +```ts +// create order-product association +const orderProduct = await blogModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) + +// update order-product association +const orderProduct = await blogModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) + +// delete order-product association +await blogModuleService.deleteOrderProducts("123") ``` -If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. +Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. -### Install Public Plugin in Medusa Application - -You install a plugin that's published publicly using your package manager. For example: - -```bash npm2yarn -npm install @myorg/plugin-name -``` - -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. - -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). +Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. *** -## Update a Published Plugin +## Retrieve Records of Relation -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. +To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. -First, run the following command to change the version of the plugin: +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: -```bash -npm version +```ts highlights={retrieveHighlights} +const product = await blogModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) ``` -Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. - -Then, re-run the same commands for publishing a plugin: - -```bash -npx medusa plugin:build -npm publish -``` - -This will publish an updated version of your plugin under a new version. +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. # Data Model Relationships @@ -11261,6 +10967,104 @@ The `cascades` method accepts an object. Its key is the operation’s name, such In the example above, when a store is deleted, its associated products are also deleted. +# Migrations + +In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. + +## What is a Migration? + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +The migration's file has a class with two methods: + +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. + +*** + +## Generate Migration + +Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. + +For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: + +```bash +npx medusa db:generate blog +``` + +This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). + +*** + +## Write a Migration Manually + +You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. + +For example: + +```ts title="src/modules/blog/migrations/Migration202507021059.ts" +import { Migration } from "@mikro-orm/migrations" + +export class Migration202507021059 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } + +} +``` + +The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. + +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. + +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. + +*** + +## Run the Migration + +To run your migration, run the following command: + +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate +``` + +This reflects the changes in the database as implemented in the migration's `up` method. + +*** + +## Rollback the Migration + +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback blog +``` + +This rolls back the last ran migration on the Blog Module. + +### Caution: Rollback Migration before Deleting + +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. + +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. + +*** + +## More Database Commands + +To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). + + # Emit Workflow and Service Events In this chapter, you'll learn about event types and how to emit an event in a service or workflow. @@ -11429,6 +11233,51 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. +# Event Data Payload + +In this chapter, you'll learn how subscribers receive an event's data payload. + +## Access Event's Data Payload + +When events are emitted, they’re emitted with a data payload. + +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. + +For example: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" + +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `event` object has the following properties: + +- data: (\`object\`) The data payload of the event. Its properties are different for each event. +- name: (string) The name of the triggered event. +- metadata: (\`object\`) Additional data and context of the emitted event. + +This logs the product ID received in the `product.created` event’s data payload to the console. + +{/* --- + +## List of Events with Data Payload + +Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} + + # Add Columns to a Link Table In this chapter, you'll learn how to add custom columns to a link definition's table and manage them. @@ -13100,71 +12949,469 @@ If multiple posts have their `product_id` set to a product's ID, an array of pos [Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md). -# Module Container +# Create a Plugin -In this chapter, you'll learn about the module's container and how to resolve resources in that container. +In this chapter, you'll learn how to create a Medusa plugin and publish it. -Since modules are isolated, each module has a local container only used by the resources of that module. +A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -### List of Registered Resources +## 1. Create a Plugin Project -Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. + +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- `src/provider`: Contains [module providers](#create-module-providers). +- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. *** -## Resolve Resources +## 2. Prepare Plugin -### Services +### Package Name -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. For example: -```ts highlights={[["4"], ["10"]]} -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export default class BlogModuleService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - - this.logger_.info("[BlogModuleService]: Hello World!") - } - +```json title="package.json" +{ + "name": "@myorg/plugin-name", // ... } ``` -### Loader +### Package Keywords -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. + +Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. + +```json title="package.json" +{ + "keywords": [ + "medusa-plugin-integration", + "medusa-v2" + ], + // ... +} +``` + +In addition, make sure to use one of the following keywords based on your integration type: + +|Keyword|Description|Example| +|---|---|---| +|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| +|\`medusa-plugin-auth\`|Authentication service integration|Auth0| +|\`medusa-plugin-cms\`|CMS service integration|Contentful| +|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| +|\`medusa-plugin-payment\`|Payment service integration|PayPal| +|\`medusa-plugin-search\`|Search service integration|MeiliSearch| +|\`medusa-plugin-shipping\`|Shipping service integration|DHL| +|\`medusa-plugin-other\`|Other third-party integrations|Sentry| + +### Package Dependencies + +Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. + +In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. + +For example, assuming `2.5.0` is the latest Medusa version: + +```json title="package.json" +{ + "devDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/ui": "4.0.4", + "@medusajs/icons": "2.5.0", + "@swc/core": "1.5.7", + }, + "peerDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/ui": "4.0.3", + "@medusajs/icons": "2.5.0", + } +} +``` + +### Package Exports + +Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. + +In the `package.json` file, make sure your plugin has the following exports: + +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./admin": { + "import": "./.medusa/server/src/admin/index.mjs", + "require": "./.medusa/server/src/admin/index.js", + "default": "./.medusa/server/src/admin/index.js" + }, + "./*": "./.medusa/server/src/*.js" + } +} +``` + +Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. + +The plugin exports the following files and directories: + +- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. +- `./workflows`: The workflows exported in `./src/workflows/index.ts`. +- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. +- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. +- `./admin`: The admin extensions exported in `./src/admin/index.ts`. +- `./*`: Any other files in the plugin's `src` directory. + +*** + +## 3. Publish Plugin Locally for Development and Testing + +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. + +### Publish and Install Local Package + +### Prerequisites + +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, navigate to your Medusa application: + +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` + +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. + +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: + +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` + +After that, run the following Medusa CLI command to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` + +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. + +### Register Plugin in Medusa Application + +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. + +Add the plugin to the `plugins` array in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` + +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. + +#### Pass Module Options through Plugin + +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. For example: -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - logger.info("[helloWorldLoader]: Hello, World!") +### Watch Plugin Changes During Development + +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. + +To do that, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +This command will: + +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. + +### Start Medusa Application + +You can start your Medusa application's development server to test out your plugin: + +```bash npm2yarn title="Medusa application" +npm run dev +``` + +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. + +*** + +## 4. Create Customizations in the Plugin + +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. + +- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) +- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) +- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) + +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). + +### Generating Migrations for Modules + +During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: + +```plain title="Plugin project" +DB_USERNAME=postgres +DB_PASSWORD=123... +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=db_name +``` + +You can add these environment variables in a `.env` file in your plugin project. The variables are: + +- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. +- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. +- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. +- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. +- `DB_NAME`: The name of the PostgreSQL database to connect to. + +Then, run the following command in your plugin project to generate migrations for the modules in your plugin: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +This command generates migrations for all modules in the plugin. + +Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: + +```bash title="Medusa application" +npx medusa db:migrate +``` + +The migrations in your application, including your plugin, will run and update the database. + +### Importing Module Resources + +In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. + +These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. + +For example, to import the plugin's workflow in your Medusa application: + +`@myorg/plugin-name` is the plugin package's name. + +```ts +import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" +import BlogModule from "@myorg/plugin-name/modules/blog" +// import other files created in plugin like ./src/types/blog.ts +import BlogType from "@myorg/plugin-name/types/blog" +``` + +### Create Module Providers + +The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. + +For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: + +`@myorg/plugin-name` is the plugin package's name. + +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. + +To learn how to create module providers, refer to the following guides: + +- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) +- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) +- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) +- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) + +*** + +## 5. Publish Plugin to NPM + +Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. + +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: + +```bash +npx medusa plugin:build +``` + +The command will compile an output in the `.medusa/server` directory. + +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: + +```bash +npm publish +``` + +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. + +### Install Public Plugin in Medusa Application + +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name +``` + +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). + +*** + +## Update a Published Plugin + +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). + +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. + +First, run the following command to change the version of the plugin: + +```bash +npm version +``` + +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. + +Then, re-run the same commands for publishing a plugin: + +```bash +npx medusa plugin:build +npm publish +``` + +This will publish an updated version of your plugin under a new version. + + +# Scheduled Jobs Number of Executions + +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. + +## numberOfExecutions Option + +The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. + +For example: + +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} + +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, } ``` +The above scheduled job has the `numberOfExecutions` configuration set to `3`. + +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. + +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. + # Commerce Modules @@ -13210,6 +13457,38 @@ export const countProductsStep = createStep( Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features. +# Infrastructure Modules + +In this chapter, you’ll learn about Infrastructure Modules. + +## What is an Infrastructure Module? + +An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + +Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. + +*** + +## Infrastructure Module Types + +There are different Infrastructure Module types including: + +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/infrastructure-modules_bj9bb9.jpg) + +- Cache Module: Defines the caching mechanism or logic to cache computational results. +- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. +- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. +- File Module: Integrates a storage service to handle uploading and managing files. +- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. +- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. + +*** + +## Infrastructure Modules List + +Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. + + # Perform Database Operations in a Service In this chapter, you'll learn how to perform database operations in a module's service. @@ -13670,36 +13949,326 @@ class BlogModuleService { ``` -# Infrastructure Modules +# Module Container -In this chapter, you’ll learn about Infrastructure Modules. +In this chapter, you'll learn about the module's container and how to resolve resources in that container. -## What is an Infrastructure Module? +Since modules are isolated, each module has a local container only used by the resources of that module. -An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. -Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. +### List of Registered Resources + +Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). *** -## Infrastructure Module Types +## Resolve Resources -There are different Infrastructure Module types including: +### Services -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/infrastructure-modules_bj9bb9.jpg) +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. -- Cache Module: Defines the caching mechanism or logic to cache computational results. -- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. -- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. -- File Module: Integrates a storage service to handle uploading and managing files. -- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. -- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. +For example: + +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export default class BlogModuleService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + + this.logger_.info("[BlogModuleService]: Hello World!") + } + + // ... +} +``` + +### Loader + +A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. + +For example: + +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("[helloWorldLoader]: Hello, World!") +} +``` + + +# Module Isolation + +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. + +- Modules can't access resources, such as services or data models, from other modules. +- Use Medusa's linking concepts, as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. + +## How are Modules Isolated? + +A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. + +For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. *** -## Infrastructure Modules List +## Why are Modules Isolated -Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. +Some of the module isolation's benefits include: + +- Integrate your module into any Medusa application without side-effects to your setup. +- Replace existing modules with your custom implementation, if your use case is drastically different. +- Use modules in other environments, such as Edge functions and Next.js apps. + +*** + +## How to Extend Data Model of Another Module? + +To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +*** + +## How to Use Services of Other Modules? + +If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. + +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. + +### Example + +For example, consider you have two modules: + +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). + +To sync brands from your application to the third-party system, create the following steps: + +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) + + const brands = await brandModuleService.listBrands() + + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` + +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. + +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) + } +) +``` + +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. + + +# Multiple Services in a Module + +In this chapter, you'll learn how to use multiple services in a module. + +## Module's Main and Internal Services + +A module has one main service only, which is the service exported in the module's definition. + +However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. + +*** + +## How to Add an Internal Service + +### 1. Create Service + +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/blog/services/client.ts` with the following content: + +```ts title="src/modules/blog/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } +} +``` + +### 2. Export Service in Index + +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + +For example, create the file `src/modules/blog/services/index.ts` with the following content: + +```ts title="src/modules/blog/services/index.ts" +export * from "./client" +``` + +This exports the `ClientService`. + +### 3. Resolve Internal Service + +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" + +type InjectedDependencies = { + clientService: ClientService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` + +You can now use your internal service in your main service. + +*** + +## Resolve Resources in Internal Service + +Resolve dependencies from your module's container in the constructor of your internal service. + +For example: + +```ts +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` + +*** + +## Access Module Options + +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. + +For example: + +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { BLOG_MODULE } from ".." + +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[BLOG_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } +} +``` + +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. + +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. + + +# Modules Directory Structure + +In this document, you'll learn about the expected files and directories in your module. + +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) + +## index.ts + +The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## service.ts + +A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## Other Directories + +The following directories are optional and their content are explained more in the following chapters: + +- `models`: Holds the data models representing tables in the database. +- `migrations`: Holds the migration files used to reflect changes on the database. +- `loaders`: Holds the scripts to run on the Medusa application's start-up. # Loaders @@ -13945,300 +14514,6 @@ info: Connected to MongoDB You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. - -- Modules can't access resources, such as services or data models, from other modules. -- Use Medusa's linking concepts, as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. - -## How are Modules Isolated? - -A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. - -For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. - -*** - -## Why are Modules Isolated - -Some of the module isolation's benefits include: - -- Integrate your module into any Medusa application without side-effects to your setup. -- Replace existing modules with your custom implementation, if your use case is drastically different. -- Use modules in other environments, such as Edge functions and Next.js apps. - -*** - -## How to Extend Data Model of Another Module? - -To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -*** - -## How to Use Services of Other Modules? - -If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. - -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. - -### Example - -For example, consider you have two modules: - -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). - -To sync brands from your application to the third-party system, create the following steps: - -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) - - const brands = await brandModuleService.listBrands() - - return new StepResponse(brands) - } -) - -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - const cmsBrands = await cmsModuleService.createBrands(brands) - - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) -``` - -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. - -Then, create the following workflow that uses these steps: - -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() - - createBrandsInCmsStep({ brands }) - } -) -``` - -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. - - -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - -# Service Constraints - -This chapter lists constraints to keep in mind when creating a service. - -## Use Async Methods - -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. - -For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: - -```ts -await blogModuleService.getMessage() -``` - -So, make sure your service's methods are always async to avoid unexpected errors or behavior. - -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } - - // Do - async getMessage(): Promise { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - - -# Multiple Services in a Module - -In this chapter, you'll learn how to use multiple services in a module. - -## Module's Main and Internal Services - -A module has one main service only, which is the service exported in the module's definition. - -However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. - -*** - -## How to Add an Internal Service - -### 1. Create Service - -To add an internal service, create it in the `services` directory of your module. - -For example, create the file `src/modules/blog/services/client.ts` with the following content: - -```ts title="src/modules/blog/services/client.ts" -export class ClientService { - async getMessage(): Promise { - return "Hello, World!" - } -} -``` - -### 2. Export Service in Index - -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - -For example, create the file `src/modules/blog/services/index.ts` with the following content: - -```ts title="src/modules/blog/services/index.ts" -export * from "./client" -``` - -This exports the `ClientService`. - -### 3. Resolve Internal Service - -Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. - -For example, in your main service: - -```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" - -type InjectedDependencies = { - clientService: ClientService -} - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected clientService_: ClientService - - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } -} -``` - -You can now use your internal service in your main service. - -*** - -## Resolve Resources in Internal Service - -Resolve dependencies from your module's container in the constructor of your internal service. - -For example: - -```ts -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export class ClientService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - } -} -``` - -*** - -## Access Module Options - -Your internal service can't access the module's options. - -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. - -For example: - -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { BLOG_MODULE } from ".." - -export type InjectedDependencies = { - configModule: ConfigModule -} - -export class ClientService { - protected options: Record - - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[BLOG_MODULE] - - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } - } -} -``` - -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. - -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. - - # Module Options In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources. @@ -14404,6 +14679,89 @@ export default Module(BLOG_MODULE, { Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. +# Service Constraints + +This chapter lists constraints to keep in mind when creating a service. + +## Use Async Methods + +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. + +For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: + +```ts +await blogModuleService.getMessage() +``` + +So, make sure your service's methods are always async to avoid unexpected errors or behavior. + +```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } + + // Do + async getMessage(): Promise { + return "Hello, World!" + } +} + +export default BlogModuleService +``` + + +# Access Workflow Errors + +In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. + +## How to Access Workflow Errors? + +By default, when an error occurs in a workflow, it throws that error, and the execution stops. + +You can configure the workflow to return the errors instead so that you can access and handle them differently. + +For example: + +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) + + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) +} + +``` + +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. + +The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. + + # Service Factory In this chapter, you’ll learn about what the service factory is and how to use it. @@ -14648,79 +15006,163 @@ The hook is available on the workflow's `hooks` property using its name `product You invoke the hook, passing a step function (the hook handler) as a parameter. -# Access Workflow Errors +# Conditions in Workflows with When-Then -In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. +In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. -## How to Access Workflow Errors? +## Why If-Conditions Aren't Allowed in Workflows? -By default, when an error occurs in a workflow, it throws that error, and the execution stops. +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. -You can configure the workflow to return the errors instead so that you can access and handle them differently. +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. -For example: +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. -```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) +*** - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } +## How to use When-Then? - res.send(result) -} - -``` - -The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. - -The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. - - -# Scheduled Jobs Number of Executions - -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. - -## numberOfExecutions Option - -The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. +The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. For example: ```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) + + // executed without condition + const anotherStepResult = anotherStep(result) + + return new WorkflowResponse( + anotherStepResult + ) + } +) ``` -The above scheduled job has the `numberOfExecutions` configuration set to `3`. +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. +### When Parameters -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +`when` accepts the following parameters: + +1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. + +### Then Parameters + +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. + +The callback function is only executed if `when`'s second parameter function returns a `true` value. + +*** + +## Implementing If-Else with When-Then + +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. + +For example: + +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) + + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) + + // ... + } +) +``` + +In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. + +*** + +## Specify Name for When-Then + +Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: + +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` + +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. + +However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. + +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: + +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active + } +).then(() => { + const isActive = isActiveStep() + + return { + isActive, + } +}) +``` + +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: + +1. A unique name to be assigned to the `when-then` block. +2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +3. A function that returns a boolean indicating whether to execute the action in `then`. + +The second and third parameters are the same as the parameters you previously passed to `when`. # Compensation Function @@ -14977,295 +15419,6 @@ The `StepResponse.permanentFailure` fails the step and its workflow, triggering So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. -# Conditions in Workflows with When-Then - -In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. - -## Why If-Conditions Aren't Allowed in Workflows? - -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - -So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. - -Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. - -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. - -*** - -## How to use When-Then? - -The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... - -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) - - // executed without condition - const anotherStepResult = anotherStep(result) - - return new WorkflowResponse( - anotherStepResult - ) - } -) -``` - -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters - -`when` accepts the following parameters: - -1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. - -### Then Parameters - -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. - -The callback function is only executed if `when`'s second parameter function returns a `true` value. - -*** - -## Implementing If-Else with When-Then - -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. - -For example: - -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) - - // ... - } -) -``` - -In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. - -*** - -## Specify Name for When-Then - -Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: - -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` - -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. - -However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. - -You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: - -```ts highlights={nameHighlights} -const { isActive } = when( - "check-is-active", - input, - (input) => { - return input.is_active - } -).then(() => { - const isActive = isActiveStep() - - return { - isActive, - } -}) -``` - -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: - -1. A unique name to be assigned to the `when-then` block. -2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -3. A function that returns a boolean indicating whether to execute the action in `then`. - -The second and third parameters are the same as the parameters you previously passed to `when`. - - -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. - -## Execute in a Workflow - -To execute a workflow in another, use the `runAsStep` method that every workflow has. - -For example: - -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) - - // ... - } -) -``` - -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. - -The object has an `input` property to pass input to the workflow. - -*** - -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. - -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -For example: - -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -type WorkflowInput = { - title: string -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - - // ... - } -) -``` - -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. - -*** - -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. - -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -For example: - -```ts highlights={whenHighlights} collapsibleLines="1-16" -import { - createWorkflow, - when, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" -import { - CreateProductWorkflowInputDTO, -} from "@medusajs/framework/types" - -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) -``` - -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. - - # Workflow Constraints This chapter lists constraints of defining a workflow or its steps. @@ -15614,6 +15767,189 @@ const step1 = createStep( ``` +# Execute Another Workflow + +In this chapter, you'll learn how to execute a workflow in another. + +## Execute in a Workflow + +To execute a workflow in another, use the `runAsStep` method that every workflow has. + +For example: + +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) + + // ... + } +) +``` + +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. + +*** + +## Preparing Input Data + +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. + +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +For example: + +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + title: string +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) + + // ... + } +) +``` + +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. + +*** + +## Run Workflow Conditionally + +To run a workflow in another based on a condition, use when-then from the Workflows SDK. + +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). + +For example: + +```ts highlights={whenHighlights} collapsibleLines="1-16" +import { + createWorkflow, + when, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" +import { + CreateProductWorkflowInputDTO, +} from "@medusajs/framework/types" + +type WorkflowInput = { + product?: CreateProductWorkflowInputDTO + should_create?: boolean +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const product = when(input, ({ should_create }) => should_create) + .then(() => { + return createProductsWorkflow.runAsStep({ + input: { + products: [input.product], + }, + }) + }) + } +) +``` + +In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + + +# Run Workflow Steps in Parallel + +In this chapter, you’ll learn how to run workflow steps in parallel. + +## parallelize Utility Function + +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. + +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. + +For example: + +```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + parallelize, +} from "@medusajs/framework/workflows-sdk" +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" + +interface WorkflowInput { + title: string +} + +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) + + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) + + const refetchedProduct = getProductStep(product.id) + + return new WorkflowResponse(refetchedProduct) + } +) +``` + +The `parallelize` function accepts the steps to run in parallel as a parameter. + +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. + +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. + + # Long-Running Workflows In this chapter, you’ll learn what a long-running workflow is and how to configure it. @@ -15801,7 +16137,7 @@ type SetStepFailureStepInput = { }; export const setStepFailureStep = createStep( - "set-step-success-step", + "set-step-failure-step", async function ( { transactionId }: SetStepFailureStepInput, { container } @@ -16106,59 +16442,6 @@ However, since the long-running workflow runs in the background, you won't recei Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). -# Run Workflow Steps in Parallel - -In this chapter, you’ll learn how to run workflow steps in parallel. - -## parallelize Utility Function - -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. - -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. - -For example: - -```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - parallelize, -} from "@medusajs/framework/workflows-sdk" -import { - createProductStep, - getProductStep, - createPricesStep, - attachProductToSalesChannelStep, -} from "./steps" - -interface WorkflowInput { - title: string -} - -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) - - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) - - const refetchedProduct = getProductStep(product.id) - - return new WorkflowResponse(refetchedProduct) - } -) -``` - -The `parallelize` function accepts the steps to run in parallel as a parameter. - -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. - -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. - - # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -16509,92 +16792,6 @@ const myWorkflow = createWorkflow( ``` -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - -*** - -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. - -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - // ... - } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. - -*** - -## Configure Step Timeout - -Alternatively, you can configure the timeout for a step rather than the entire workflow. - -As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -This step's executions fail if they run longer than two seconds. - -A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. - - # Workflow Hooks In this chapter, you'll learn what a workflow hook is and how to consume them. @@ -16719,563 +16916,90 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) { Your hook handler then receives that passed data in the `additional_data` object. -# Write Integration Tests +# Workflow Timeout -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. +In this chapter, you’ll learn how to set a timeout for workflows and steps. -### Prerequisites +## What is a Workflow Timeout? -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -## medusaIntegrationTestRunner Utility +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. -The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. +### Timeout Doesn't Stop Step Execution + +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. + +*** + +## Configure Workflow Timeout + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. + +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. For example: -```ts title="integration-tests/http/test.spec.ts" highlights={highlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - // TODO write tests... - }, +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) }) -jest.setTimeout(60 * 1000) +export default myWorkflow + ``` -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. +This workflow's executions fail if they run longer than two seconds. -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: - -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -### Jest Timeout - -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: - -```ts title="integration-tests/http/test.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. *** -### Run Tests +## Configure Step Timeout -Run the following command to run your tests: +Alternatively, you can configure the timeout for a step rather than the entire workflow. -```bash npm2yarn -npm run test:integration -``` +As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. -If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. -This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. +For example: -*** - -## Other Options and Inputs - -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - -*** - -## Database Used in Tests - -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). - -*** - -## Example Integration Tests - -The next chapters provide examples of writing integration tests for API routes and workflows. - - -# Translate Medusa Admin - -The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. - -{/* vale docs.We = NO */} - -You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. - -{/* vale docs.We = YES */} - -Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). - -*** - -## How to Contribute Translation - -1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: - -```bash -git clone https://github.com/medusajs/medusa.git -``` - -If you already have it cloned, make sure to pull the latest changes from the `develop` branch. - -2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: - -```bash -yarn install -``` - -3. Create a branch that you'll use to open the pull request later: - -```bash -git checkout -b feat/translate- -``` - -Where `` is your language name. For example, `feat/translate-da`. - -4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. - - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. - - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. - -5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. - - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: - -```bash title="packages/admin/dashboard" -yarn i18n:validate da.json -``` - -6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: - -```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} -// other imports... -import da from "./da.json" - -export default { - // other languages... - da: { - translation: da, - }, -} -``` - -The language's key in the object is the ISO-2 name of the language. - -7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: - -```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} -import { da } from "date-fns/locale" -// other imports... - -export const languages: Language[] = [ - // other languages... +```tsx +const step1 = createStep( { - code: "da", - display_name: "Danish", - ltr: true, - date_locale: da, + name: "step-1", + timeout: 2, // 2 seconds }, -] + async () => { + // ... + } +) ``` -`languages` is an array having the following properties: +This step's executions fail if they run longer than two seconds. -- `code`: The ISO-2 name of the language. For example, `da` for Danish. -- `display_name`: The language's name to be displayed in the admin. -- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. -- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. - -8. Once you're done, push the changes into your branch and open a pull request on GitHub. - -Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. - - -# Docs Contribution Guidelines - -Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. - -This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). - -## What Can You Contribute? - -You can contribute to the Medusa documentation in the following ways: - -- Fixes to existing content. This includes small fixes like typos, or adding missing information. -- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. - - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. -- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. - -*** - -## Documentation Workspace - -Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. - -The workspace has the following two directories: - -- `apps`: this directory holds the different documentation websites and projects. - - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). - - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). - - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). - - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). -- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. - - `docs-ui` includes the shared React components between the different apps. - - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. - -*** - -## Documentation Content - -All documentation projects are built with Next.js. The content is writtin in MDX files. - -### Medusa Main Docs Content - -The content of the Medusa main docs are under the `www/apps/book/app` directory. - -### Medusa Resources Content - -The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. - -Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. - -### API Reference - -The API reference's content is split into two types: - -1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. -2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. - -### Medusa UI Documentation - -The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. - -The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. - -The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. - -*** - -## Style Guide - -When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). - -*** - -## How to Contribute - -If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). - -If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. - -### Base Branch - -When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. - -Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. - -### Branch Name - -Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. - -### Pull Request Conventions - -When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. - -In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. - -*** - -## Images - -If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. - -*** - -## NPM and Yarn Code Blocks - -If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. - -For example: - -````md -```bash npm2yarn -npm run start -``` -```` - -The code snippet must be written using NPM. - -### Global Option - -When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example: - -```bash npm2yarn -npm install @medusajs/cli -g -``` - -*** - -## Linting with Vale - -Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. - -### Result of Vale PR Checks - -You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix. - -### Run Vale Locally - -If you want to check your work locally, you can do that by: - -1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. -2. Changing to the `www/vale` directory: - -```bash -cd www/vale -``` - -3\. Running the `run-vale` script: - -```bash -# to lint content for the main documentation -./run-vale.sh book/app/learn error resources -# to lint content for the resources documentation -./run-vale.sh resources/app error -# to lint content for the API reference -./run-vale.sh api-reference/markdown error -# to lint content for the Medusa UI documentation -./run-vale.sh ui/src/content/docs error -# to lint content for the user guide -./run-vale.sh user-guide/app error -``` - -{/* TODO need to enable MDX v1 comments first. */} - -{/* ### Linter Exceptions - -If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files: - -```md - - -content that shouldn't be scanned for errors here... - - -``` - -You can also disable specific rules. For example: - -```md - - -Medusa supports Node versions 14 and 16. - - -``` - -If you use this in your PR, you must justify its usage. */} - -*** - -## Linting with ESLint - -Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. - -### Linting Content with ESLint - -Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. - -If you want to check content ESLint errors locally and fix them, you can do that by: - -1\. Install the dependencies in the `www` directory: - -```bash -yarn install -``` - -2\. Run the turbo command in the `www` directory: - -```bash -turbo run lint:content -``` - -This will fix any fixable errors, and show errors that require your action. - -### Linting Code with ESLint - -Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. - -If you want to check code ESLint errors locally and fix them, you can do that by: - -1\. Install the dependencies in the `www` directory: - -```bash -yarn install -``` - -2\. Run the turbo command in the `www` directory: - -```bash -yarn lint -``` - -This will fix any fixable errors, and show errors that require your action. - -{/* TODO need to enable MDX v1 comments first. */} - -{/* ### ESLint Exceptions - -If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: - -~~~md - - -```js -console.log("This block isn't linted") -``` - -```js -console.log("This block is linted") -``` -~~~ - -You can also disable specific rules. For example: - -~~~md - - -```js -console.log("This block can use semicolons"); -``` - -```js -console.log("This block can't use semi colons") -``` -~~~ */} - - -# Write Tests for Modules - -In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## moduleIntegrationTestRunner Utility - -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. - -For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: - -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import Post from "../models/post" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [Post], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - // TODO write tests - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: - -- `moduleName`: The name of the module. -- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. -- `resolve`: The path to the module's directory. -- `testSuite`: A function that defines the tests to run. - -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. - -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -*** - -## Run Tests - -Run the following command to run your module integration tests: - -```bash npm2yarn -npm run test:integration:modules -``` - -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. - -*** - -## Pass Module Options - -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. - -For example: - -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" - -moduleIntegrationTestRunner({ - moduleOptions: { - apiKey: "123", - }, - // ... -}) -``` - -*** - -## Write Tests for Modules without Data Models - -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. - -For example: - -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" -import { model } from "@medusajs/framework/utils" - -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) - -moduleIntegrationTestRunner({ - moduleModels: [DummyModel], - // ... -}) - -jest.setTimeout(60 * 1000) -``` - -*** - -### Other Options and Inputs - -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - -*** - -## Database Used in Tests - -The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. # Example: Write Integration Tests for API Routes @@ -17847,6 +17571,364 @@ const response = await api.post(`/custom`, form, { ``` +# Docs Contribution Guidelines + +Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. + +This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). + +## What Can You Contribute? + +You can contribute to the Medusa documentation in the following ways: + +- Fixes to existing content. This includes small fixes like typos, or adding missing information. +- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. + - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. +- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. + +*** + +## Documentation Workspace + +Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. + +The workspace has the following two directories: + +- `apps`: this directory holds the different documentation websites and projects. + - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). + - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). + - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). + - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). +- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. + - `docs-ui` includes the shared React components between the different apps. + - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. + +*** + +## Documentation Content + +All documentation projects are built with Next.js. The content is writtin in MDX files. + +### Medusa Main Docs Content + +The content of the Medusa main docs are under the `www/apps/book/app` directory. + +### Medusa Resources Content + +The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. + +Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. + +### API Reference + +The API reference's content is split into two types: + +1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. +2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. + +### Medusa UI Documentation + +The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. + +The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. + +The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. + +*** + +## Style Guide + +When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). + +*** + +## How to Contribute + +If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). + +If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. + +### Base Branch + +When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. + +Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. + +### Branch Name + +Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. + +### Pull Request Conventions + +When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. + +In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. + +*** + +## Images + +If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. + +*** + +## NPM and Yarn Code Blocks + +If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. + +For example: + +````md +```bash npm2yarn +npm run start +``` +```` + +The code snippet must be written using NPM. + +### Global Option + +When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example: + +```bash npm2yarn +npm install @medusajs/cli -g +``` + +*** + +## Linting with Vale + +Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. + +### Result of Vale PR Checks + +You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix. + +### Run Vale Locally + +If you want to check your work locally, you can do that by: + +1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. +2. Changing to the `www/vale` directory: + +```bash +cd www/vale +``` + +3\. Running the `run-vale` script: + +```bash +# to lint content for the main documentation +./run-vale.sh book/app/learn error resources +# to lint content for the resources documentation +./run-vale.sh resources/app error +# to lint content for the API reference +./run-vale.sh api-reference/markdown error +# to lint content for the Medusa UI documentation +./run-vale.sh ui/src/content/docs error +# to lint content for the user guide +./run-vale.sh user-guide/app error +``` + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### Linter Exceptions + +If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files: + +```md + + +content that shouldn't be scanned for errors here... + + +``` + +You can also disable specific rules. For example: + +```md + + +Medusa supports Node versions 14 and 16. + + +``` + +If you use this in your PR, you must justify its usage. */} + +*** + +## Linting with ESLint + +Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. + +### Linting Content with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. + +If you want to check content ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +turbo run lint:content +``` + +This will fix any fixable errors, and show errors that require your action. + +### Linting Code with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. + +If you want to check code ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +yarn lint +``` + +This will fix any fixable errors, and show errors that require your action. + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### ESLint Exceptions + +If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: + +~~~md + + +```js +console.log("This block isn't linted") +``` + +```js +console.log("This block is linted") +``` +~~~ + +You can also disable specific rules. For example: + +~~~md + + +```js +console.log("This block can use semicolons"); +``` + +```js +console.log("This block can't use semi colons") +``` +~~~ */} + + +# Translate Medusa Admin + +The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. + +{/* vale docs.We = NO */} + +You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. + +{/* vale docs.We = YES */} + +Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). + +*** + +## How to Contribute Translation + +1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: + +```bash +git clone https://github.com/medusajs/medusa.git +``` + +If you already have it cloned, make sure to pull the latest changes from the `develop` branch. + +2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: + +```bash +yarn install +``` + +3. Create a branch that you'll use to open the pull request later: + +```bash +git checkout -b feat/translate- +``` + +Where `` is your language name. For example, `feat/translate-da`. + +4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. + - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. + - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. + +5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. + - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: + +```bash title="packages/admin/dashboard" +yarn i18n:validate da.json +``` + +6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: + +```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} +// other imports... +import da from "./da.json" + +export default { + // other languages... + da: { + translation: da, + }, +} +``` + +The language's key in the object is the ISO-2 name of the language. + +7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: + +```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} +import { da } from "date-fns/locale" +// other imports... + +export const languages: Language[] = [ + // other languages... + { + code: "da", + display_name: "Danish", + ltr: true, + date_locale: da, + }, +] +``` + +`languages` is an array having the following properties: + +- `code`: The ISO-2 name of the language. For example, `da` for Danish. +- `display_name`: The language's name to be displayed in the admin. +- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. +- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. + +8. Once you're done, push the changes into your branch and open a pull request on GitHub. + +Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. + + # Example: Write Integration Tests for Workflows In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. @@ -18228,297 +18310,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Customer Module - -In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. - -Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Customer Features - -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). - -*** - -## How to Use the Customer Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-customer.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) - - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) - - return new StepResponse({ customer }, customer.id) - }, - async (customerId, { container }) => { - if (!customerId) { - return - } - const customerModuleService = container.resolve(Modules.CUSTOMER) - - await customerModuleService.deleteCustomers([customerId]) - } -) - -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() - - return new WorkflowResponse({ - customer, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCustomerWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Cart Module - -In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. - -Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Cart Features - -- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. -- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. - -*** - -## How to Use the Cart Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-cart.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCartStep = createStep( - "create-cart", - async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) - - const cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - address_1: "1512 Barataria Blvd", - country_code: "us", - }, - items: [ - { - title: "Shirt", - unit_price: 1000, - quantity: 1, - }, - ], - }) - - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) - - await cartModuleService.deleteCarts([cartId]) - } -) - -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() - - return new WorkflowResponse({ - cart, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createCartWorkflow } from "../../workflows/create-cart" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Auth Module In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. @@ -18797,6 +18588,297 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Cart Module + +In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. + +Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Cart Features + +- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. +- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. + +*** + +## How to Use the Cart Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-cart.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createCartStep = createStep( + "create-cart", + async ({}, { container }) => { + const cartModuleService = container.resolve(Modules.CART) + + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + shipping_address: { + address_1: "1512 Barataria Blvd", + country_code: "us", + }, + items: [ + { + title: "Shirt", + unit_price: 1000, + quantity: 1, + }, + ], + }) + + return new StepResponse({ cart }, cart.id) + }, + async (cartId, { container }) => { + if (!cartId) { + return + } + const cartModuleService = container.resolve(Modules.CART) + + await cartModuleService.deleteCarts([cartId]) + } +) + +export const createCartWorkflow = createWorkflow( + "create-cart", + () => { + const { cart } = createCartStep() + + return new WorkflowResponse({ + cart, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createCartWorkflow } from "../../workflows/create-cart" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCartWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCartWorkflow } from "../workflows/create-cart" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCartWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + +# Customer Module + +In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. + +Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Customer Features + +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). + +*** + +## How to Use the Customer Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-customer.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createCustomerStep = createStep( + "create-customer", + async ({}, { container }) => { + const customerModuleService = container.resolve(Modules.CUSTOMER) + + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", + }) + + return new StepResponse({ customer }, customer.id) + }, + async (customerId, { container }) => { + if (!customerId) { + return + } + const customerModuleService = container.resolve(Modules.CUSTOMER) + + await customerModuleService.deleteCustomers([customerId]) + } +) + +export const createCustomerWorkflow = createWorkflow( + "create-customer", + () => { + const { customer } = createCustomerStep() + + return new WorkflowResponse({ + customer, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createCustomerWorkflow } from "../../workflows/create-customer" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCustomerWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCustomerWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCustomerWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Fulfillment Module In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application. @@ -18963,6 +19045,306 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** +# Inventory Module + +In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. + +Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Inventory Features + +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. + +*** + +## How to Use the Inventory Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createInventoryItemStep = createStep( + "create-inventory-item", + async ({}, { container }) => { + const inventoryModuleService = container.resolve(Modules.INVENTORY) + + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, + }) + + return new StepResponse({ inventoryItem }, inventoryItem.id) + }, + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { + return + } + const inventoryModuleService = container.resolve(Modules.INVENTORY) + + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + } +) + +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", + () => { + const { inventoryItem } = createInventoryItemStep() + + return new WorkflowResponse({ + inventoryItem, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createInventoryItemWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createInventoryItemWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createInventoryItemWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + +# Order Module + +In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. + +Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Order Features + +- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. +- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. +- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. + +*** + +## How to Use the Order Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createDraftOrderStep = createStep( + "create-order", + async ({}, { container }) => { + const orderModuleService = container.resolve(Modules.ORDER) + + const draftOrder = await orderModuleService.createOrders({ + currency_code: "usd", + items: [ + { + title: "Shirt", + quantity: 1, + unit_price: 3000, + }, + ], + shipping_methods: [ + { + name: "Express shipping", + amount: 3000, + }, + ], + status: "draft", + }) + + return new StepResponse({ draftOrder }, draftOrder.id) + }, + async (draftOrderId, { container }) => { + if (!draftOrderId) { + return + } + const orderModuleService = container.resolve(Modules.ORDER) + + await orderModuleService.deleteOrders([draftOrderId]) + } +) + +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", + () => { + const { draftOrder } = createDraftOrderStep() + + return new WorkflowResponse({ + draftOrder, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createDraftOrderWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Pricing Module In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. @@ -19117,6 +19499,161 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Payment Module + +In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. + +Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Payment Features + +- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. +- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. +- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. +- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers. +- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. + +*** + +## How to Use the Payment Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createPaymentCollectionStep = createStep( + "create-payment-collection", + async ({}, { container }) => { + const paymentModuleService = container.resolve(Modules.PAYMENT) + + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) + + return new StepResponse({ paymentCollection }, paymentCollection.id) + }, + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { + return + } + const paymentModuleService = container.resolve(Modules.PAYMENT) + + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + } +) + +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", + () => { + const { paymentCollection } = createPaymentCollectionStep() + + return new WorkflowResponse({ + paymentCollection, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPaymentCollectionWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPaymentCollectionWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPaymentCollectionWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure Payment Module + +The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. + +*** + +## Providers + +Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. + +*** + + # Product Module In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application. @@ -19271,154 +19808,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Promotion Module - -In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. - -Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Promotion Features - -- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. -- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. -- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. -- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. - -*** - -## How to Use the Promotion Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-promotion.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createPromotionStep = createStep( - "create-promotion", - async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) - - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, - currency_code: "usd", - }, - }) - - return new StepResponse({ promotion }, promotion.id) - }, - async (promotionId, { container }) => { - if (!promotionId) { - return - } - const promotionModuleService = container.resolve(Modules.PROMOTION) - - await promotionModuleService.deletePromotions(promotionId) - } -) - -export const createPromotionWorkflow = createWorkflow( - "create-promotion", - () => { - const { promotion } = createPromotionStep() - - return new WorkflowResponse({ - promotion, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createPromotionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createPromotionWorkflow } from "../workflows/create-cart" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createPromotionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Region Module In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. @@ -19562,6 +19951,143 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Stock Location Module + +In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. + +Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Stock Location Features + +- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). +- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. + +*** + +## How to Use Stock Location Module's Service + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-stock-location.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createStockLocationStep = createStep( + "create-stock-location", + async ({}, { container }) => { + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", + }) + + return new StepResponse({ stockLocation }, stockLocation.id) + }, + async (stockLocationId, { container }) => { + if (!stockLocationId) { + return + } + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + + await stockLocationModuleService.deleteStockLocations([stockLocationId]) + } +) + +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", + () => { + const { stockLocation } = createStockLocationStep() + + return new WorkflowResponse({ stockLocation }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createStockLocationWorkflow } from "../../workflows/create-stock-location" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStockLocationWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStockLocationWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStockLocationWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Sales Channel Module In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. @@ -19722,24 +20248,26 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module +# Promotion Module -In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module. +Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Stock Location Features +## Promotion Features -- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). -- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. +- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. +- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. +- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. +- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. *** -## How to Use Stock Location Module's Service +## How to Use the Promotion Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -19747,7 +20275,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-stock-location.ts" highlights={highlights} +```ts title="src/workflows/create-promotion.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -19756,33 +20284,42 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createPromotionStep = createStep( + "create-promotion", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const promotionModuleService = container.resolve(Modules.PROMOTION) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, + currency_code: "usd", + }, }) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ promotion }, promotion.id) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (promotionId, { container }) => { + if (!promotionId) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const promotionModuleService = container.resolve(Modules.PROMOTION) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await promotionModuleService.deletePromotions(promotionId) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createPromotionWorkflow = createWorkflow( + "create-promotion", () => { - const { stockLocation } = createStockLocationStep() + const { promotion } = createPromotionStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ + promotion, + }) } ) ``` @@ -19796,13 +20333,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createPromotionWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createPromotionWorkflow(req.scope) .run() res.send(result) @@ -19816,13 +20353,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -19837,12 +20374,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -20000,162 +20537,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Order Module - -In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. - -Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Order Features - -- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. -- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. -- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. - -*** - -## How to Use the Order Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-draft-order.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createDraftOrderStep = createStep( - "create-order", - async ({}, { container }) => { - const orderModuleService = container.resolve(Modules.ORDER) - - const draftOrder = await orderModuleService.createOrders({ - currency_code: "usd", - items: [ - { - title: "Shirt", - quantity: 1, - unit_price: 3000, - }, - ], - shipping_methods: [ - { - name: "Express shipping", - amount: 3000, - }, - ], - status: "draft", - }) - - return new StepResponse({ draftOrder }, draftOrder.id) - }, - async (draftOrderId, { container }) => { - if (!draftOrderId) { - return - } - const orderModuleService = container.resolve(Modules.ORDER) - - await orderModuleService.deleteOrders([draftOrderId]) - } -) - -export const createDraftOrderWorkflow = createWorkflow( - "create-draft-order", - () => { - const { draftOrder } = createDraftOrderStep() - - return new WorkflowResponse({ - draftOrder, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createDraftOrderWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createDraftOrderWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createDraftOrderWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Tax Module In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. @@ -20300,305 +20681,6 @@ The Tax Module accepts options for further configurations. Refer to [this docume *** -# Inventory Module - -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. - -Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Inventory Features - -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. - -*** - -## How to Use the Inventory Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createInventoryItemStep = createStep( - "create-inventory-item", - async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, - }) - - return new StepResponse({ inventoryItem }, inventoryItem.id) - }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { - return - } - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) - } -) - -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", - () => { - const { inventoryItem } = createInventoryItemStep() - - return new WorkflowResponse({ - inventoryItem, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createInventoryItemWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createInventoryItemWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Payment Module - -In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. - -Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Payment Features - -- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. -- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. -- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. -- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers. -- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. - -*** - -## How to Use the Payment Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createPaymentCollectionStep = createStep( - "create-payment-collection", - async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) - - const paymentCollection = await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) - - return new StepResponse({ paymentCollection }, paymentCollection.id) - }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { - return - } - const paymentModuleService = container.resolve(Modules.PAYMENT) - - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) - } -) - -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", - () => { - const { paymentCollection } = createPaymentCollectionStep() - - return new WorkflowResponse({ - paymentCollection, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createPaymentCollectionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createPaymentCollectionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Payment Module - -The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. - -*** - -## Providers - -Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. - -*** - - # User Module In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. @@ -20746,29 +20828,6 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. - -## `has_account` Property - -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. - -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. - -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. - -*** - -## Email Uniqueness - -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - - # API Key Concepts In this document, you’ll learn about the different types of API keys, their expiration and verification. @@ -20895,6 +20954,1299 @@ createRemoteLinkStep({ ``` +# Authentication Flows with the Auth Main Service + +In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. + +## Authentication Methods + +### Register + +The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. + +For example: + +```ts +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `register` method of the provider specified in the first parameter and returns its data. + +### Authenticate + +To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: + +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. + +*** + +## Auth Flow 1: Basic Authentication + +The basic authentication flow requires first using the `register` method, then the `authenticate` method: + +```ts +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (error) { + // registration failed + // TODO return an error + return +} + +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (success && !location) { + // user is authenticated +} +``` + +If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. + +The next section explains the flow if `location` is set. + +Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. + +![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) + +### Auth Identity with Same Identifier + +If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. + +There are two ways to handle this: + +- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. +- Return an error message to the customer, informing them that the email is already in use. + +*** + +## Auth Flow 2: Third-Party Service Authentication + +The third-party service authentication method requires using the `authenticate` method first: + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) + +if (location) { + // return the location for the front-end to redirect to +} + +if (!success) { + // authentication failed +} + +// authentication successful +``` + +If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. + +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. + +![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) + +### Overriding Callback URL + +The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + callback_url: "example.com", + } +) +``` + +### validateCallback + +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. + +So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). + +The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: + +```ts +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) + +if (success) { + // authentication succeeded +} +``` + +For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. + +If the returned `success` property is `true`, the authentication with the third-party provider was successful. + +![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) + +*** + +## Reset Password + +To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. + +For example: + +```ts +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) + +if (success) { + // password reset successfully +} +``` + +The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. + +In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. + +If the returned `success` property is `true`, the password has reset successfully. + + +# Auth Identity and Actor Types + +In this document, you’ll learn about concepts related to identity and actors in the Auth Module. + +## What is an Auth Identity? + +The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. + +Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. + +*** + +## Actor Types + +An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). + +Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. + +For example, an auth identity of a customer has the following `app_metadata` property: + +```json +{ + "app_metadata": { + "customer_id": "cus_123" + } +} +``` + +The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. + +*** + +## Protect Routes by Actor Type + +When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [ + authenticate("user", ["session", "bearer", "api-key"]), + ], + }, + ], +}) +``` + +By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. + +*** + +## Custom Actor Types + +You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. + +For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. + +Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). + + +# Auth Providers + +In this document, you’ll learn how the Auth Module handles authentication using providers. + +## What's an Auth Module Provider? + +An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. + +For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. + +### Auth Providers List + +- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) +- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) +- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) + +*** + +## Configure Allowed Auth Providers of Actor Types + +By default, users of all actor types can authenticate with all installed auth module providers. + +To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, +}) +``` + +When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. + +*** + +## How to Create an Auth Module Provider + +Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. + + +# How to Use Authentication Routes + +In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. + +These routes are added by Medusa's HTTP layer, not the Auth Module. + +## Types of Authentication Flows + +### 1. Basic Authentication Flow + +This authentication flow doesn't require validation with third-party services. + +[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). + +The steps are: + +![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) + +1. Register the user with the [Register Route](#register-route). +2. Use the authentication token to create the user with their respective API route. + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) +3. Authenticate the user with the [Auth Route](#login-route). + +After registration, you only use the [Auth Route](#login-route) for subsequent authentication. + +To handle errors related to existing identities, refer to [this section](#handling-existing-identities). + +### 2. Third-Party Service Authenticate Flow + +This authentication flow authenticates the user with a third-party service, such as Google. + +[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). + +It requires the following steps: + +![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) + +1. Authenticate the user with the [Auth Route](#login-route). +2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. +3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. +4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. +5. If the callback validation is successful, the frontend receives the authentication token. +6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). + - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests. + - If not, follow the rest of the steps. +7. The frontend uses the authentication token to create the user with their respective API route. + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) +8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated. + +*** + +## Register Route + +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` + +This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. + +For example, if you're registering a customer, you: + +1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token. +2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +### Request Body Parameters + +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. + +For example, the EmailPass provider requires an `email` and `password` fields in the request body. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +### Handling Existing Identities + +An auth identity with the same email may already exist in Medusa. This can happen if: + +- Another actor type is using that email. For example, an admin user is trying to register as a customer. +- The same email belongs to a record of the same actor type. For example, another customer has the same email. + +In these scenarios, the Register Route will return an error instead of a token: + +```json +{ + "type": "unauthorized", + "message": "Identity with email already exists" +} +``` + +To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. + +Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: + +```json +{ + "type": "unauthorized", + "message": "Invalid email or password" +} +``` + +You can show that error message to the customer. + +*** + +## Login Route + +The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` + +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +### Request Body Parameters + +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. + +For example, the EmailPass provider requires an `email` and `password` fields in the request body. + +#### Overriding Callback URL + +For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. + +This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +If the authentication requires more action with a third-party service, you'll receive a `location` property: + +```json +{ + "location": "https://..." +} +``` + +Redirect to that URL in the frontend to continue the authentication process with the third-party service. + +[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). + +*** + +## Validate Callback Route + +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +``` + +Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `google`. + +### Query Parameters + +This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): + +- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests. +- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + +*** + +## Refresh Token Route + +The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. + +It requires the user's JWT token that they received from the authentication or callback routes. + +```bash +curl -X POST http://localhost:9000/auth/token/refresh \ +-H 'Authorization: Bearer {token}' +``` + +### Response Fields + +If the token was refreshed successfully, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +*** + +## Reset Password Routes + +To reset a user's password: + +1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). + - The API route emits the `auth.password_reset` event, passing the token in the payload. + - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. +2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. + - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. + +[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) + +### Generate Reset Password Token Route + +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password +-H 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "Whitney_Schultz@gmail.com" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. + +#### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +#### Request Body Parameters + +This route accepts in the request body an object having the following property: + +- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. + +#### Response Fields + +If the authentication is successful, the request returns a `201` response code. + +### Reset Password Route + +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com", + "password": "supersecret" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. + +#### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +#### Pass Token in Authorization Header + +Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. + +In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. + +### Request Body Parameters + +This route accepts in the request body an object that has the data necessary for the provider to update the user's password. + +For the `emailpass` provider, you must pass the following properties: + +- `email`: The user's email. +- `password`: The new password. + +### Response Fields + +If the authentication is successful, the request returns an object with a `success` property set to `true`: + +```json +{ + "success": "true" +} +``` + + +# How to Create an Actor Type + +In this document, learn how to create an actor type and authenticate its associated data model. + +## 0. Create Module with Data Model + +Before creating an actor type, you must have a module with a data model representing the actor type. + +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +The rest of this guide uses this `Manager` data model as an example: + +```ts title="src/modules/manager/models/manager.ts" +import { model } from "@medusajs/framework/utils" + +const Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), +}) + +export default Manager +``` + +*** + +## 1. Create Workflow + +Start by creating a workflow that does two things: + +- Creates a record of the `Manager` data model. +- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. + +For example, create the file `src/workflows/create-manager.ts`. with the following content: + +```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, +} from "@medusajs/medusa/core-flows" +import ManagerModuleService from "../modules/manager/service" + +type CreateManagerWorkflowInput = { + manager: { + first_name: string + last_name: string + email: string + } + authIdentityId: string +} + +const createManagerStep = createStep( + "create-manager-step", + async ({ + manager: managerData, + }: Pick, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") + + const manager = await managerModuleService.createManager( + managerData + ) + + return new StepResponse(manager) + } +) + +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, + }) + + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, + }) + + return new WorkflowResponse(manager) + } +) + +export default createManagerWorkflow +``` + +This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. + +The workflow has two steps: + +1. Create the manager using the `createManagerStep`. +2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. + +*** + +## 2. Define the Create API Route + +Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. + +So, create the file `src/api/manager/route.ts` with the following content: + +```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import createManagerWorkflow from "../../workflows/create-manager" + +type RequestBody = { + first_name: string + last_name: string + email: string +} + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + // If `actor_id` is present, the request carries + // authentication for an existing manager + if (req.auth_context.actor_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Request already authenticated as a manager." + ) + } + + const { result } = await createManagerWorkflow(req.scope) + .run({ + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, + }) + + res.status(200).json({ manager: result }) +} +``` + +Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: + +1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +2. Passing the token in the bearer header of the request to this route. + +In the API route, you create the manager using the workflow from the previous section and return it in the response. + +*** + +## 3. Apply the `authenticate` Middleware + +The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. + +To do that, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], + }, + { + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], + }, + ], +}) +``` + +This applies middlewares on two route patterns: + +1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. +2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. + +### Retrieve Manager API Route + +For example, create the file `src/api/manager/me/route.ts` with the following content: + +```ts title="src/api/manager/me/route.ts" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import ManagerModuleService from "../../../modules/manager/service" + +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +): Promise { + const managerModuleService: ManagerModuleService = + req.scope.resolve("managerModuleService") + + const manager = await managerModuleService.retrieveManager( + req.auth_context.actor_id + ) + + res.json({ manager }) +} +``` + +This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. + +*** + +## Test Custom Actor Type Authentication Flow + +To authenticate managers: + +1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` + +Copy the returned token to use it in the next request. + +2. Send a `POST` request to `/manager` to create a manager: + +```bash +curl -X POST 'http://localhost:9000/manager' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "first_name": "John", + "last_name": "Doe", + "email": "manager@gmail.com" +}' +``` + +Replace `{token}` with the token returned in the previous step. + +3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` + +4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: + +```bash +curl 'http://localhost:9000/manager/me' \ +-H 'Authorization: Bearer {token}' +``` + +Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. + +*** + +## Delete User of Actor Type + +When you delete a user of the actor type, you must update its auth identity to remove the association to the user. + +For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: + +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import ManagerModuleService from "../modules/manager/service" + +export type DeleteManagerWorkflow = { + id: string +} + +const deleteManagerStep = createStep( + "delete-manager-step", + async ( + { id }: DeleteManagerWorkflow, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") + + const manager = await managerModuleService.retrieve(id) + + await managerModuleService.deleteManagers(id) + + return new StepResponse(undefined, { manager }) + }, + async ({ manager }, { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") + + await managerModuleService.createManagers(manager) + } + ) +``` + +You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. + +Next, in the same file, add the workflow that deletes a manager: + +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} +// other imports +import { MedusaError } from "@medusajs/framework/utils" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +// ... + +export const deleteManagerWorkflow = createWorkflow( + "delete-manager", + ( + input: WorkflowData + ): WorkflowResponse => { + deleteManagerStep(input) + + const { data: authIdentities } = useQueryGraphStep({ + entity: "auth_identity", + fields: ["id"], + filters: { + app_metadata: { + // the ID is of the format `{actor_type}_id`. + manager_id: input.id, + }, + }, + }) + + const authIdentity = transform( + { authIdentities }, + ({ authIdentities }) => { + const authIdentity = authIdentities[0] + + if (!authIdentity) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Auth identity not found" + ) + } + + return authIdentity + } + ) + + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "manager", + value: null, + }) + + return new WorkflowResponse(input.id) + } +) +``` + +In the workflow, you: + +1. Use the `deleteManagerStep` defined earlier to delete the manager. +2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. +3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. + +You can use this workflow when deleting a manager, such as in an API route. + + +# How to Handle Password Reset Token Event + +In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md). + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. + +You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. + +### Prerequisites + +- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) + +## 1. Create Subscriber + +The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. + +Create the file `src/subscribers/handle-reset.ts` with the following content: + +```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { Modules } from "@medusajs/framework/utils" + +export default async function resetPasswordTokenHandler({ + event: { data: { + entity_id: email, + token, + actor_type, + } }, + container, +}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const urlPrefix = actor_type === "customer" ? + "https://storefront.com" : + "https://admin.com/app" + + await notificationModuleService.createNotifications({ + to: email, + channel: "email", + template: "reset-password-template", + data: { + // a URL to a frontend application + url: `${urlPrefix}/reset-password?token=${token}&email=${email}`, + }, + }) +} + +export const config: SubscriberConfig = { + event: "auth.password_reset", +} +``` + +You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: + +- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. +- `token`: The token to reset the user's password. +- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. + +This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). + +In the subscriber, you: + +- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. +- Resolve the Notification Module and use its `createNotifications` method to send the notification. +- You pass to the `createNotifications` method an object having the following properties: + - `to`: The identifier to send the notification to, which in this case is the email. + - `channel`: The channel to send the notification through, which in this case is email. + - `template`: The template ID in the third-party service. + - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. + +*** + +## 2. Test it Out: Generate Reset Password Token + +To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. + +For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: + +```bash +curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "admin-test@gmail.com" +}' +``` + +In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. + +If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: + +```plain +info: Processing auth.password_reset which has 1 subscribers +``` + +The notification is sent to the user with the frontend URL to enter a new password. + +*** + +## Next Steps: Implementing Frontend + +In your frontend, you must have a page that accepts `token` and `email` query parameters. + +The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). + +### Examples + +- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) + + +# Auth Module Options + +In this document, you'll learn about the options of the Auth Module. + +## providers + +The `providers` option is an array of auth module providers. + +When the Medusa application starts, these providers are registered and can be used to handle authentication. + +By default, the `emailpass` provider is registered to authenticate customers and admin users. + +For example: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + +*** + +## Auth CORS + +The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. + +By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. + +Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. + +*** + +## authMethodsPerActor Configuration + +The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. + +Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md). + + +# Links between Currency Module and Other Modules + +This document showcases the module links defined between the Currency Module and other Commerce Modules. + +## Summary + +The Currency Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only - has one|| + +*** + +## Store Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. + +### Retrieve with Query + +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: + +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + + # Cart Concepts In this document, you’ll get an overview of the main concepts of a cart. @@ -20932,183 +22284,6 @@ If the fulfillment provider requires additional custom data to be passed along f The `data` property is an object used to store custom data relevant later for fulfillment. -# Links between Customer Module and Other Modules - -This document showcases the module links defined between the Customer Module and other Commerce Modules. - -## Summary - -The Customer Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Read-only - has one|| - -*** - -## Payment Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. - -This link is available starting from Medusa `v2.5.0`. - -### Retrieve with Query - -To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) - -// customers[0].account_holder_link?.[0]?.account_holder -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) - -// customers[0].account_holder_link?.[0]?.account_holder -``` - -### Manage with Link - -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - - # Links between Cart Module and Other Modules This document showcases the module links defined between the Cart Module and other Commerce Modules. @@ -21548,81 +22723,6 @@ const { data: carts } = useQueryGraphStep({ ``` -# Tax Lines in Cart Module - -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. - -![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`. - -*** - -## Retrieve Tax Lines - -When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. - -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) - -// retrieve the tax lines -const taxLines = await taxModuleService.getTaxLines( - [ - ...(cart.items as TaxableItemDTO[]), - ...(cart.shipping_methods as TaxableShippingDTO[]), - ], - { - address: { - ...cart.shipping_address, - country_code: - cart.shipping_address.country_code || "us", - }, - } -) -``` - -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: - -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) - -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` - - # Promotions Adjustments in Carts In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. @@ -21741,695 +22841,280 @@ await cartModuleService.setShippingMethodAdjustments( ``` -# Auth Identity and Actor Types +# Tax Lines in Cart Module -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -## What is an Auth Identity? +## What are Tax Lines? -The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. +A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) *** -## Actor Types +## Tax Inclusivity -An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. -Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. -For example, an auth identity of a customer has the following `app_metadata` property: +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. +![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`. *** -## Protect Routes by Actor Type +## Retrieve Tax Lines -When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. +When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. -For example: - -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], - }, +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", ], }) + +// retrieve the tax lines +const taxLines = await taxModuleService.getTaxLines( + [ + ...(cart.items as TaxableItemDTO[]), + ...(cart.shipping_methods as TaxableShippingDTO[]), + ], + { + address: { + ...cart.shipping_address, + country_code: + cart.shipping_address.country_code || "us", + }, + } +) ``` -By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: + +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) + +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) +``` + + +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. + +## `has_account` Property + +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. *** -## Custom Actor Types +## Email Uniqueness -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. - -Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. -# Auth Providers +# Links between Customer Module and Other Modules -In this document, you’ll learn how the Auth Module handles authentication using providers. +This document showcases the module links defined between the Customer Module and other Commerce Modules. -## What's an Auth Module Provider? +## Summary -An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +The Customer Module has the following links to other modules: -For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -### Auth Providers List - -- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) -- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) -- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Stored - many-to-many|| +| in ||Read-only - has one|| +| in ||Read-only - has one|| *** -## Configure Allowed Auth Providers of Actor Types +## Payment Module -By default, users of all actor types can authenticate with all installed auth module providers. +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. -To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations: +This link is available starting from Medusa `v2.5.0`. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], - }, - // ... - }, - // ... +### Retrieve with Query + +To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "account_holder_link.account_holder.*", + ], +}) + +// customers[0].account_holder_link?.[0]?.account_holder +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "account_holder_link.account_holder.*", + ], +}) + +// customers[0].account_holder_link?.[0]?.account_holder +``` + +### Manage with Link + +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` -When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. +### createRemoteLinkStep -*** - -## How to Create an Auth Module Provider - -Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. - - -# How to Create an Actor Type - -In this document, learn how to create an actor type and authenticate its associated data model. - -## 0. Create Module with Data Model - -Before creating an actor type, you must have a module with a data model representing the actor type. - -Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - -The rest of this guide uses this `Manager` data model as an example: - -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" - -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), -}) - -export default Manager -``` - -*** - -## 1. Create Workflow - -Start by creating a workflow that does two things: - -- Creates a record of the `Manager` data model. -- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. - -For example, create the file `src/workflows/create-manager.ts`. with the following content: - -```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, -} from "@medusajs/medusa/core-flows" -import ManagerModuleService from "../modules/manager/service" - -type CreateManagerWorkflowInput = { - manager: { - first_name: string - last_name: string - email: string - } - authIdentityId: string -} - -const createManagerStep = createStep( - "create-manager-step", - async ({ - manager: managerData, - }: Pick, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") - - const manager = await managerModuleService.createManager( - managerData - ) - - return new StepResponse(manager) - } -) - -const createManagerWorkflow = createWorkflow( - "create-manager", - function (input: CreateManagerWorkflowInput) { - const manager = createManagerStep({ - manager: input.manager, - }) - - setAuthAppMetadataStep({ - authIdentityId: input.authIdentityId, - actorType: "manager", - value: manager.id, - }) - - return new WorkflowResponse(manager) - } -) - -export default createManagerWorkflow -``` - -This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. - -The workflow has two steps: - -1. Create the manager using the `createManagerStep`. -2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. - -*** - -## 2. Define the Create API Route - -Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. - -So, create the file `src/api/manager/route.ts` with the following content: - -```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import createManagerWorkflow from "../../workflows/create-manager" - -type RequestBody = { - first_name: string - last_name: string - email: string -} - -export async function POST( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) { - // If `actor_id` is present, the request carries - // authentication for an existing manager - if (req.auth_context.actor_id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Request already authenticated as a manager." - ) - } - - const { result } = await createManagerWorkflow(req.scope) - .run({ - input: { - manager: req.body, - authIdentityId: req.auth_context.auth_identity_id, - }, - }) - - res.status(200).json({ manager: result }) -} -``` - -Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: - -1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). -2. Passing the token in the bearer header of the request to this route. - -In the API route, you create the manager using the workflow from the previous section and return it in the response. - -*** - -## 3. Apply the `authenticate` Middleware - -The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. - -To do that, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager", - method: "POST", - middlewares: [ - authenticate("manager", ["session", "bearer"], { - allowUnregistered: true, - }), - ], - }, - { - matcher: "/manager/me*", - middlewares: [ - authenticate("manager", ["session", "bearer"]), - ], - }, - ], -}) -``` - -This applies middlewares on two route patterns: - -1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. -2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. - -### Retrieve Manager API Route - -For example, create the file `src/api/manager/me/route.ts` with the following content: - -```ts title="src/api/manager/me/route.ts" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import ManagerModuleService from "../../../modules/manager/service" - -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -): Promise { - const managerModuleService: ManagerModuleService = - req.scope.resolve("managerModuleService") - - const manager = await managerModuleService.retrieveManager( - req.auth_context.actor_id - ) - - res.json({ manager }) -} -``` - -This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. - -*** - -## Test Custom Actor Type Authentication Flow - -To authenticate managers: - -1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: - -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` - -Copy the returned token to use it in the next request. - -2. Send a `POST` request to `/manager` to create a manager: - -```bash -curl -X POST 'http://localhost:9000/manager' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "first_name": "John", - "last_name": "Doe", - "email": "manager@gmail.com" -}' -``` - -Replace `{token}` with the token returned in the previous step. - -3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: - -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` - -4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: - -```bash -curl 'http://localhost:9000/manager/me' \ --H 'Authorization: Bearer {token}' -``` - -Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. - -*** - -## Delete User of Actor Type - -When you delete a user of the actor type, you must update its auth identity to remove the association to the user. - -For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: - -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import ManagerModuleService from "../modules/manager/service" - -export type DeleteManagerWorkflow = { - id: string -} - -const deleteManagerStep = createStep( - "delete-manager-step", - async ( - { id }: DeleteManagerWorkflow, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") - - const manager = await managerModuleService.retrieve(id) - - await managerModuleService.deleteManagers(id) - - return new StepResponse(undefined, { manager }) - }, - async ({ manager }, { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") - - await managerModuleService.createManagers(manager) - } - ) -``` - -You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. - -Next, in the same file, add the workflow that deletes a manager: - -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} -// other imports -import { MedusaError } from "@medusajs/framework/utils" -import { - WorkflowData, - WorkflowResponse, - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -export const deleteManagerWorkflow = createWorkflow( - "delete-manager", - ( - input: WorkflowData - ): WorkflowResponse => { - deleteManagerStep(input) - - const { data: authIdentities } = useQueryGraphStep({ - entity: "auth_identity", - fields: ["id"], - filters: { - app_metadata: { - // the ID is of the format `{actor_type}_id`. - manager_id: input.id, - }, - }, - }) - - const authIdentity = transform( - { authIdentities }, - ({ authIdentities }) => { - const authIdentity = authIdentities[0] - - if (!authIdentity) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Auth identity not found" - ) - } - - return authIdentity - } - ) - - setAuthAppMetadataStep({ - authIdentityId: authIdentity.id, - actorType: "manager", - value: null, - }) - - return new WorkflowResponse(input.id) - } -) -``` - -In the workflow, you: - -1. Use the `deleteManagerStep` defined earlier to delete the manager. -2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. -3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. - -You can use this workflow when deleting a manager, such as in an API route. - - -# Auth Module Options - -In this document, you'll learn about the options of the Auth Module. - -## providers - -The `providers` option is an array of auth module providers. - -When the Medusa application starts, these providers are registered and can be used to handle authentication. - -By default, the `emailpass` provider is registered to authenticate customers and admin users. - -For example: - -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // provider options... - }, - }, - ], - }, - }, - ], +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, }) ``` -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - *** -## Auth CORS +## Cart Module -The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. -By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. +### Retrieve with Query -Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -*** +### query.graph -## authMethodsPerActor Configuration +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. - -Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md). - - -# How to Handle Password Reset Token Event - -In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md). - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. - -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. - -### Prerequisites - -- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) - -## 1. Create Subscriber - -The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. - -Create the file `src/subscribers/handle-reset.ts` with the following content: - -```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" -import { - SubscriberArgs, - type SubscriberConfig, -} from "@medusajs/medusa" -import { Modules } from "@medusajs/framework/utils" - -export default async function resetPasswordTokenHandler({ - event: { data: { - entity_id: email, - token, - actor_type, - } }, - container, -}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) - - const urlPrefix = actor_type === "customer" ? - "https://storefront.com" : - "https://admin.com/app" - - await notificationModuleService.createNotifications({ - to: email, - channel: "email", - template: "reset-password-template", - data: { - // a URL to a frontend application - url: `${urlPrefix}/reset-password?token=${token}&email=${email}`, - }, - }) -} - -export const config: SubscriberConfig = { - event: "auth.password_reset", -} +// carts.customer ``` -You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: +### useQueryGraphStep -- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. -- `token`: The token to reset the user's password. -- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). +// ... -In the subscriber, you: +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) -- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. -- Resolve the Notification Module and use its `createNotifications` method to send the notification. -- You pass to the `createNotifications` method an object having the following properties: - - `to`: The identifier to send the notification to, which in this case is the email. - - `channel`: The channel to send the notification through, which in this case is email. - - `template`: The template ID in the third-party service. - - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. +// carts.customer +``` *** -## 2. Test it Out: Generate Reset Password Token +## Order Module -To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. -For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: +### Retrieve with Query -```bash -curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "admin-test@gmail.com" -}' +To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer ``` -In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. +### useQueryGraphStep -If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -```plain -info: Processing auth.password_reset which has 1 subscribers +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer ``` -The notification is sent to the user with the frontend URL to enter a new password. - -*** - -## Next Steps: Implementing Frontend - -In your frontend, you must have a page that accepts `token` and `email` query parameters. - -The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). - -### Examples - -- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) - # Fulfillment Concepts @@ -22477,63 +23162,6 @@ A shipping profile defines a type of items that are shipped in a similar manner. A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. -# Links between Currency Module and Other Modules - -This document showcases the module links defined between the Currency Module and other Commerce Modules. - -## Summary - -The Currency Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Read-only - has one|| - -*** - -## Store Module - -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. - -Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. - -### Retrieve with Query - -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: - -### query.graph - -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies[0].currency -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies[0].currency -``` - - # Fulfillment Module Provider In this document, you’ll learn what a fulfillment module provider is. @@ -23085,1283 +23713,540 @@ When fulfilling an item, you might use a third-party fulfillment provider that r The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -# Authentication Flows with the Auth Main Service +# Inventory Concepts -In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. -## Authentication Methods +## InventoryItem -### Register +An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. -The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. +The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. -For example: +![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) -```ts -const data = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` +### Inventory Shipping Requirement -This method calls the `register` method of the provider specified in the first parameter and returns its data. +An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. -### Authenticate - -To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: - -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` - -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. +When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). *** -## Auth Flow 1: Basic Authentication +## InventoryLevel -The basic authentication flow requires first using the `register` method, then the `authenticate` method: +An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) +It has three quantity-related properties: -if (error) { - // registration failed - // TODO return an error - return -} +- `stocked_quantity`: The available stock quantity of an item in the associated location. +- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +### Associated Location -if (success && !location) { - // user is authenticated -} -``` - -If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. - -The next section explains the flow if `location` is set. - -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. - -![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) - -### Auth Identity with Same Identifier - -If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. - -There are two ways to handle this: - -- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. -- Return an error message to the customer, informing them that the email is already in use. +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. *** -## Auth Flow 2: Third-Party Service Authentication +## ReservationItem -The third-party service authentication method requires using the `authenticate` method first: +A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. -if (location) { - // return the location for the front-end to redirect to -} -if (!success) { - // authentication failed -} +# Inventory Kits -// authentication successful -``` +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. -If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. +- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). +- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). -![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) +## What is an Inventory Kit? -### Overriding Callback URL +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. -The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. +The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - callback_url: "example.com", - } -) -``` +Using inventory kits, you can implement use cases like: -### validateCallback - -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. - -So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). - -The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: - -```ts -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) - -if (success) { - // authentication succeeded -} -``` - -For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. - -If the returned `success` property is `true`, the authentication with the third-party provider was successful. - -![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. *** -## Reset Password +## Multi-Part Products -To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. -For example: +To implement this in Medusa, you can: -```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. -if (success) { - // password reset successfully -} -``` +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. -The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. +![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) -In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. +### Create Multi-Part Product -If the returned `success` property is `true`, the password has reset successfully. +Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: -# How to Use Authentication Routes +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" -In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. - -These routes are added by Medusa's HTTP layer, not the Auth Module. - -## Types of Authentication Flows - -### 1. Basic Authentication Flow - -This authentication flow doesn't require validation with third-party services. - -[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). - -The steps are: - -![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) - -1. Register the user with the [Register Route](#register-route). -2. Use the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) -3. Authenticate the user with the [Auth Route](#login-route). - -After registration, you only use the [Auth Route](#login-route) for subsequent authentication. - -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). - -### 2. Third-Party Service Authenticate Flow - -This authentication flow authenticates the user with a third-party service, such as Google. - -[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). - -It requires the following steps: - -![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) - -1. Authenticate the user with the [Auth Route](#login-route). -2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. -3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. -4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. -5. If the callback validation is successful, the frontend receives the authentication token. -6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). - - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests. - - If not, follow the rest of the steps. -7. The frontend uses the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) -8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated. - -*** - -## Register Route - -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. - -For example, if you're registering a customer, you: - -1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token. -2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -### Request Body Parameters - -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - -For example, the EmailPass provider requires an `email` and `password` fields in the request body. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -### Handling Existing Identities - -An auth identity with the same email may already exist in Medusa. This can happen if: - -- Another actor type is using that email. For example, an admin user is trying to register as a customer. -- The same email belongs to a record of the same actor type. For example, another customer has the same email. - -In these scenarios, the Register Route will return an error instead of a token: - -```json -{ - "type": "unauthorized", - "message": "Identity with email already exists" -} -``` - -To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. - -Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: - -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` - -You can show that error message to the customer. - -*** - -## Login Route - -The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -### Request Body Parameters - -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - -For example, the EmailPass provider requires an `email` and `password` fields in the request body. - -#### Overriding Callback URL - -For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. - -This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -If the authentication requires more action with a third-party service, you'll receive a `location` property: - -```json -{ - "location": "https://..." -} -``` - -Redirect to that URL in the frontend to continue the authentication process with the third-party service. - -[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). - -*** - -## Validate Callback Route - -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 -``` - -Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `google`. - -### Query Parameters - -This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): - -- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests. -- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - -*** - -## Refresh Token Route - -The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. - -It requires the user's JWT token that they received from the authentication or callback routes. - -```bash -curl -X POST http://localhost:9000/auth/token/refresh \ --H 'Authorization: Bearer {token}' -``` - -### Response Fields - -If the token was refreshed successfully, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -*** - -## Reset Password Routes - -To reset a user's password: - -1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). - - The API route emits the `auth.password_reset` event, passing the token in the payload. - - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. -2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. - - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. - -[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) - -### Generate Reset Password Token Route - -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password --H 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "Whitney_Schultz@gmail.com" -}' -``` - -This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. - -#### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -#### Request Body Parameters - -This route accepts in the request body an object having the following property: - -- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. - -#### Response Fields - -If the authentication is successful, the request returns a `201` response code. - -### Reset Password Route - -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com", - "password": "supersecret" -}' -``` - -This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. - -#### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -#### Pass Token in Authorization Header - -Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. - -In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. - -### Request Body Parameters - -This route accepts in the request body an object that has the data necessary for the provider to update the user's password. - -For the `emailpass` provider, you must pass the following properties: - -- `email`: The user's email. -- `password`: The new password. - -### Response Fields - -If the authentication is successful, the request returns an object with a `success` property set to `true`: - -```json -{ - "success": "true" -} -``` - - -# Pricing Concepts - -In this document, you’ll learn about the main concepts in the Pricing Module. - -## Price Set - -A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) 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](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -![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) - -*** - -## Price List - -A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) 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. - - -# Prices Calculation - -In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. - -## calculatePrices Method - -The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. - -It returns a price object with the best matching price for each price set. - -### 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. - -For example: - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` - -In this example, you retrieve the prices in a price set for the specified currency code and region ID. - -### Returned Price Object - -For each price set, the `calculatePrices` method selects two prices: - -- 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 that has the following properties: - -- id: (\`string\`) The ID of the price set from which the price was selected. -- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. -- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. -- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. -- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. -- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. -- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- calculated\_price: (\`object\`) The calculated price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. - -*** - -## Examples - -Consider the following price set: - -```ts -const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - // default price - { - amount: 500, - currency_code: "EUR", - rules: {}, - }, - // prices with rules - { - amount: 400, - currency_code: "EUR", - rules: { - region_id: "reg_123", +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse", }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, - ], -}) -``` + }) -### Default Price Selection - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` - -### Result - -### Calculate Prices with Rules - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) -``` - -### Result - -### Price Selection with Price List - -### Code - -```ts -const priceList = pricingModuleService.createPriceLists([{ - title: "Summer Price List", - description: "Price list for summer sale", - starts_at: Date.parse("01/10/2023").toString(), - ends_at: Date.parse("31/10/2023").toString(), - 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 pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) -``` - -### Result - - -# Price Rules - -In this Pricing Module guide, you'll learn about price rules for price sets and price lists, and how to add rules to a price. - -## Price Rule - -You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). - -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. - -For exmaple, you create a price restricted to `10557` zip codes. - -![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) - -A price can have multiple price rules. - -For example, a price can be restricted by a region and a zip code. - -![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) - -*** - -## Price List Rules - -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). - -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. - -![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) - -*** - -## How to Set Rules on a Price? - -### Using Workflows - -Medusa uses the Pricing Module to store prices of different resources, such as product variants and shipping options. - -When you manage one of these resources using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or using the API routes that use them, you can set rules on a price using the `rules` property of the price object. - -For example, when creating a shipping option using the [createShippingOptionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) to create a shipping option, you can make the shipping price free based on the cart total: - -```ts highlights={workflowHighlights} -const { result } = await createShippingOptionsWorkflow(container) - .run({ - input: [{ - name: "Standard Shipping", - service_zone_id: "serzo_123", - shipping_profile_id: "sp_123", - provider_id: "prov_123", - type: { - label: "Standard", - description: "Standard shipping", - code: "standard", - }, - price_type: "flat", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], }, - }, - ], - }], - }) -``` - -In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. - -### Using Pricing Module's Service - -For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. - -When adding a price using the [addPrices](https://docs.medusajs.com/resources/references/pricing/addPrices/index.html.md) method of the Pricing Module's service, pass the `rules` property to a price object. - -For example: - -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + ], }, - }, - ], -}) -``` + }) -In this example, you set the default price of a resource (for example, a shipping option), to `$10`. You also add a conditioned price that sets the price to `0` when the cart or order's total is greater than or equal to `$100`. - -### How is the Price Rule Applied? - -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. - -For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: - -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "item_total": { - "operator": "gte", - "value": 100, - } + // TODO create the product } -} +) ``` -The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). -You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: +Then, you create the inventory items that the product variant consists of. -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "customer.group.id": { - "operator": "eq", - "value": "cusgrp_123" - } +Next, create the product and pass the inventory item's IDs to the product's variant: + +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... + + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + shipping_profile_id: "sp_123", + }, + ], + }, + }) } -} +) ``` -In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. +You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. -These same rules apply to product variant prices as well, or any other resource that has a price. - - -# Links between Pricing Module and Other Modules - -This document showcases the module links defined between the Pricing Module and other Commerce Modules. - -## Summary - -The Pricing Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - one-to-one|| -| in ||Stored - one-to-one|| +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). *** -## Fulfillment Module +## Bundled Products -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. +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) -![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) +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. -### Retrieve with Query +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. -To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: +![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) -### query.graph +### Create Bundled Product -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) +You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. -// priceSets[0].shipping_option +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: + +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Pants", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Shoes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) + + // TODO re-retrieve with inventory + } +) ``` -### useQueryGraphStep +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +Next, retrieve the products again but with variant information: -// ... +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) -// priceSets[0].shipping_option + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) + + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) + + // create bundled product + } +) ``` -### Manage with Link +Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Finally, create the bundled product: -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }).config({ name: "create-bundled-product" }) + } +) ``` -### createRemoteLinkStep +The bundled product has the same inventory items as those of the products part of the bundle. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). -// ... -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) *** -## Product Module +## Add to Cart -The Product Module doesn't store or manage the prices of product variants. +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. -Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -![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) +![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.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. +## Order Placed -### Retrieve with Query +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. -To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -### query.graph +![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) +*** -// priceSets[0].variant -``` +## Order Fulfillment -### useQueryGraphStep +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. -// ... +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) +![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) -// priceSets[0].variant -``` +*** -### Manage with Link +## Order Return -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. -### link.create +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) -```ts -import { Modules } from "@medusajs/framework/utils" +![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) -// ... +### Dismissed Returned Items -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. -# Links between Product Module and Other Modules +# Links between Inventory Module and Other Modules -This document showcases the module links defined between the Product Module and other Commerce Modules. +This document showcases the module links defined between the Inventory Module and other Commerce Modules. ## Summary -The Product Module has the following links to other modules: +The Inventory Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Read-only - has one|| -|| in |Stored - many-to-one|| -|| in |Stored - many-to-many|| -| in ||Read-only - has one|| -|| in |Stored - one-to-one|| -|| in |Stored - many-to-many|| +| in ||Stored - many-to-many|| +|| in |Read-only - has many|| *** -## Cart Module +## Product Module -Medusa defines read-only links between: +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. -- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. +![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems[0].variant -``` - -*** - -## Fulfillment Module - -Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile. - -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). - -### Retrieve with Query - -To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`: - -### query.graph - -```ts -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "shipping_profile.*", - ], -}) - -// products[0].shipping_profile -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "shipping_profile.*", - ], -}) - -// products[0].shipping_profile -``` - -### Manage with Link - -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -*** - -## 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. +A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). ### Retrieve with Query -To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: +To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: ### query.graph ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", fields: [ - "inventory_items.*", + "variants.*", ], }) -// variants[0].inventory_items +// inventoryItems[0].variants ``` ### useQueryGraphStep @@ -24371,19 +24256,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", fields: [ - "inventory_items.*", + "variants.*", ], }) -// variants[0].inventory_items +// inventoryItems[0].variants ``` ### Manage with Link -To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24422,1602 +24307,9 @@ createRemoteLinkStep({ *** -## Order Module - -Medusa defines read-only links between: - -- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. -- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems[0].variant -``` - -*** - -## 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. - -### Retrieve with Query - -To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: - -### query.graph - -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) - -// variants[0].price_set -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) - -// variants[0].price_set -``` - -### Manage with Link - -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## 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) - -### Retrieve with Query - -To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: - -### query.graph - -```ts -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "sales_channels.*", - ], -}) - -// products[0].sales_channels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "sales_channels.*", - ], -}) - -// products[0].sales_channels -``` - -### Manage with Link - -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - - -# Product Variant Inventory - -# Product Variant Inventory - -In this guide, you'll learn about the inventory management features related to product variants. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants. - -## Configure Inventory Management of Product Variants - -A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP. - -The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. - -When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. - -*** - -## How the Medusa Application Manages Inventory - -When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. - -![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) - -The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. - -![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) - -Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). - -The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. - -![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) - -Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). - -### Product Inventory in Storefronts - -When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. - -The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. - -For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. - -![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) - -*** - -## Variant Back Orders - -Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. - -You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). - -*** - -## Additional Resources - -The following guides provide more details on inventory management in the Medusa application: - -- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. -- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products. -- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. -- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). - - -# Tax-Inclusive Pricing - -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. - -## What is Tax-Inclusive Pricing? - -A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. - -For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. - -*** - -## How is Tax-Inclusive Pricing Set? - -The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: - -- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. -- `value`: The attribute’s value. For example, `reg_123` or `usd`. - -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. - -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. - -For example: - -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` - -In this example, tax-inclusivity is enabled for the `USD` currency code. - -*** - -## Tax-Inclusive Pricing in Price Calculation - -### Tax Context - -As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. - -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. - -### Returned Tax Properties - -The `calculatePrices` method returns two properties related to tax-inclusivity: - -Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - -- `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. - -A price is considered tax-inclusive if: - -1. It belongs to the region or currency code specified in the calculation context; -2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. - -### Tax Context Precedence - -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 - - -# Promotion Actions - -In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). - -## computeActions Method - -The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. - -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. - -*** - -## Action Types - -### `addItemAdjustment` Action - -The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. - -This action has the following format: - -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string -} -``` - -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. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `removeItemAdjustment` Action - -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. - -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` - -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `addShippingMethodAdjustment` Action - -The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. - -This action has the following format: - -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} -``` - -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. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `removeShippingMethodAdjustment` Action - -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. - -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} -``` - -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `campaignBudgetExceeded` Action - -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. - -This action has the following format: - -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. - - -# Application Method - -In this document, you'll learn what an application method is. - -## What is an Application Method? - -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: - -|Property|Purpose| -|---|---| -|\`type\`|Does the promotion discount a fixed amount or a percentage?| -|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| -|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| - -## Target Promotion Rules - -When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. - -The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. - -![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) - -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. - -*** - -## Buy Promotion Rules - -When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. - -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. - -![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) - -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. - - -# Configure Selling Products - -In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. - -The concepts in this guide are applicable starting from Medusa v2.5.1. - -## Scenario - -Businesses can have different selling requirements: - -1. They may sell physical or digital items. -2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. -3. They may sell items whose inventory is managed by an external system, such as an ERP. - -Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. - -*** - -## Configuring Shipping Requirements - -The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. - -When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. - -If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. - -### Overriding Shipping Requirements for Variants - -A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. - -Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: - -1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. -2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. - -*** - -## Use Case Examples - -By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: - -|Use Case|Configurations|Example| -|---|---|---|---|---| -|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| -|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| -|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| -|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| - - -# Campaign - -In this document, you'll learn about campaigns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. - -## What is a Campaign? - -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. - -![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) - -*** - -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. - -There are two types of budgets: - -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. - -![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) - - -# Promotion Concepts - -In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. - -## What is a Promotion? - -A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. - -A promotion has two types: - -- `standard`: A standard promotion with rules. -- `buyget`: “A buy X get Y” promotion with rules. - -|\`standard\`|\`buyget\`| -|---|---| -|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| -|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| -|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| - -The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. - -*** - -## Promotion Rules - -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). - -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`. - -The expected value for the attribute 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. - -### Flexible Rules - -The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). - -For example, to restrict the promotion to only `VIP` and `B2B` customer groups: - -- 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) - -In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. - -*** - -## How to Apply Rules on a Promotion? - -### Using Workflows - -If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md). - -For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createPromotionsWorkflow/index.html.md): - -```ts -const { result } = await createPromotionsWorkflow(container) - .run({ - input: { - promotionsData: [{ - code: "10OFF", - type: "standard", - status: "active", - application_method: { - type: "percentage", - target_type: "items", - allocation: "across", - value: 10, - currency_code: "usd", - }, - rules: [ - { - attribute: "customer.group.id", - operator: "eq", - values: [ - "cusgrp_123" - ] - } - ] - }], - } - }) -``` - -In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. - -### Using Promotion Module's Service - -For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. - -If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods. - -For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/resources/references/promotion/createPromotions/index.html.md) method: - -```ts -const promotions = await promotionModuleService.createPromotions([ - { - code: "50OFF", - type: "standard", - status: "active", - application_method: { - type: "percentage", - target_type: "items", - value: 50, - }, - rules: [ - { - attribute: "customer.group.id", - operator: "eq", - values: [ - "cusgrp_123" - ] - } - ] - }, -]) -``` - -In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. - -### How is the Promotion Rule Applied? - -A promotion is applied on a resource if its attributes match the promotion's rules. - -For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer: - -```json -{ - "code": "10OFF", - "type": "standard", - "status": "active", - "application_method": { - "type": "percentage", - "target_type": "items", - "allocation": "across", - "value": 10, - "currency_code": "usd" - }, - "rules": [ - { - "attribute": "customer_id", - "operator": "eq", - "values": [ - "cus_123" - ] - } - ] -} -``` - -When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`. - - -# Links between Promotion Module and Other Modules - -This document showcases the module links defined between the Promotion Module and other Commerce Modules. - -## Summary - -The Promotion Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Stored - many-to-many|| - -*** - -## Cart Module - -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` 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) - -Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. - -### Retrieve with Query - -To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions[0].carts -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions[0].carts -``` - -### Manage with Link - -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Order Module - -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) - -### Retrieve with Query - -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions[0].orders -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions[0].orders -``` - -### Manage with Link - -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - - -# Links between Region Module and Other Modules - -This document showcases the module links defined between the Region Module and other Commerce Modules. - -## Summary - -The Region Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Read-only - has one|| -| in ||Read-only - has one|| -|| in |Stored - many-to-many|| - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -*** - -## Payment Module - -You can specify for each region which payment providers are available for use. - -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. - -![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) - -### Retrieve with Query - -To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: - -### query.graph - -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions[0].payment_providers -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions[0].payment_providers -``` - -### Manage with Link - -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - - -# Links between Sales Channel Module and Other Modules - -This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. - -## Summary - -The Sales Channel Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Read-only - has one|| -| in ||Stored - many-to-many|| -|| in |Stored - many-to-many|| - -*** - -## API Key Module - -A publishable API key allows you to easily specify the sales channel scope in a client request. - -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) - -### Retrieve with Query - -To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels[0].publishable_api_keys -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels[0].publishable_api_keys -``` - -### Manage with Link - -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts[0].sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts[0].sales_channel -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders.sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders.sales_channel -``` - -*** - -## Product Module - -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) - -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. - -### Retrieve with Query - -To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) - -// salesChannels[0].products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) - -// salesChannels[0].products -``` - -### Manage with Link - -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -*** - ## Stock Location Module -A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. - -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) - -### Retrieve with Query - -To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) - -// salesChannels[0].stock_locations -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) - -// salesChannels[0].stock_locations -``` - -### Manage with Link - -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# Publishable API Keys with Sales Channels - -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. - -## Publishable API Keys with Sales Channels - -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. - -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: - -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` - -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. - -*** - -## How to Create a Publishable API Key? - -To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). - - -# Stock Location Concepts - -In this document, you’ll learn about the main concepts in the Stock Location Module. - -## Stock Location - -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. - -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. - -*** - -## StockLocationAddress - -The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. - - -# Links between Stock Location Module and Other Modules - -This document showcases the module links defined between the Stock Location Module and other Commerce Modules. - -## Summary - -The Stock Location Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - many-to-one|| -| in ||Stored - many-to-many|| -| in ||Read-only - has many|| -| in ||Stored - many-to-many|| - -*** - -## Fulfillment Module - -A fulfillment set can be conditioned to a specific stock location. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: - -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) - -// stockLocations[0].fulfillment_sets -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) - -// stockLocations[0].fulfillment_sets -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -*** - -## Inventory Module - -Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. ### Retrieve with Query @@ -26053,145 +24345,59 @@ const { data: inventoryLevels } = useQueryGraphStep({ // inventoryLevels[0].stock_locations ``` -*** -## Sales Channel Module +# Order Claim -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +In this document, you’ll learn about order claims. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. -![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) +## What is a Claim? -### Retrieve with Query +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations[0].sales_channels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations[0].sales_channels -``` - -### Manage with Link - -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# Links between Store Module and Other Modules - -This document showcases the module links defined between the Store Module and other Commerce Modules. - -## Summary - -The Store Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Read-only - has many|| +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. *** -## Currency Module +## Claim Type -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. +The `Claim` data model has a `type` property whose value indicates the type of the claim: -Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. -### Retrieve with Query +*** -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: +## Old and Replacement Items -### query.graph +When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). -// stores[0].supported_currencies -``` +If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Claim Shipping Methods -// ... +A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). -// stores[0].supported_currencies -``` +*** + +## Claim Refund + +If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. + +The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. + +*** + +## How Claims Impact an Order’s Version + +When a claim is confirmed, the order’s version is incremented. # Order Concepts @@ -26300,6 +24506,311 @@ Once the Order Edit is confirmed, any additional payment or refund required can This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). +# Order Exchange + +In this document, you’ll learn about order exchanges. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. + +## What is an Exchange? + +An exchange is the replacement of an item that the customer ordered with another. + +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. + +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. + +*** + +## Returned and New Items + +When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. + +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. + +*** + +## Exchange Shipping Methods + +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). + +The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). + +*** + +## Exchange Payment + +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. + +|Condition|Result| +|---|---|---| +|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | +|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | +|\`difference\_due = 0\`|No payment processing is required.| + +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +*** + +## How Exchanges Impact an Order’s Version + +When an exchange is confirmed, the order’s version is incremented. + + +# Order Versioning + +In this document, you’ll learn how an order and its details are versioned. + +## What's Versioning? + +Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. + +When changes are made on an order, such as an item is added or returned, the order's version changes. + +*** + +## version Property + +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. + +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. + +*** + +## How the Version Changes + +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: + +1. The version of the order and its summary is incremented. +2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. + +When the order is retrieved, only the related data having the same version is retrieved. + + +# Order Change + +In this document, you'll learn about the Order Change data model and possible actions in it. + +## OrderChange Data Model + +The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. + +Its `change_type` property indicates what the order change is created for: + +1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). +2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). +3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). +4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +Once the order change is confirmed, its changes are applied on the order. + +*** + +## Order Change Actions + +The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). + +The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. + +The following table lists the possible `action` values that Medusa uses and what `details` they carry. + +|Action|Description|Details| +|---|---|---|---|---| +|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| +|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| +|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| +|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| +|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| + + +# Promotions Adjustments in Orders + +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. + +The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. + +![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg) + +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. + +The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. + +*** + +## Discountable Option + +The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + +When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. + +*** + +## Promotion Actions + +When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. + +Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). + +```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" + +// ... + +// retrieve the order +const order = await orderModuleService.retrieveOrder("ord_123", { + relations: [ + "items.item.adjustments", + "shipping_methods.shipping_method.adjustments", + ], +}) +// retrieve the line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +order.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + ...item.detail, + adjustments: filteredAdjustments, + }) + } +}) + +//retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +order.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + // TODO infer from cart or region + currency_code: "usd", + } +) +``` + +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. + +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" + +// ... + +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` + + +# Order Return + +In this document, you’ll learn about order returns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. + +## What is a Return? + +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). + +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. + +![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) + +Once the merchant receives the returned items, they mark the return as received. + +*** + +## Returned Items + +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). + +The `ReturnItem` model has two properties storing the item's quantity: + +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. + +*** + +## Return Shipping Methods + +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). + +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. + +*** + +## Refund Payment + +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. + +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. + +*** + +## Returns in Exchanges and Claims + +When a merchant creates an exchange or a claim, it includes returning items from the customer. + +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. + +*** + +## How Returns Impact an Order’s Version + +The order’s version is incremented when: + +1. A return is requested. +2. A return is marked as received. + + # Links between Order Module and Other Modules This document showcases the module links defined between the Order Module and other Commerce Modules. @@ -26824,197 +25335,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# Order Change - -In this document, you'll learn about the Order Change data model and possible actions in it. - -## OrderChange Data Model - -The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. - -Its `change_type` property indicates what the order change is created for: - -1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). -2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). -3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). -4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -Once the order change is confirmed, its changes are applied on the order. - -*** - -## Order Change Actions - -The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). - -The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. - -The following table lists the possible `action` values that Medusa uses and what `details` they carry. - -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| - - -# Order Versioning - -In this document, you’ll learn how an order and its details are versioned. - -## What's Versioning? - -Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. - -When changes are made on an order, such as an item is added or returned, the order's version changes. - -*** - -## version Property - -The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. - -Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. - -*** - -## How the Version Changes - -When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: - -1. The version of the order and its summary is incremented. -2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. - -When the order is retrieved, only the related data having the same version is retrieved. - - -# Promotions Adjustments in Orders - -In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. - -## What are Adjustment Lines? - -An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. - -The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. - -![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg) - -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. - -The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. - -*** - -## Discountable Option - -The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. - -When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. - -*** - -## Promotion Actions - -When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. - -Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). - -```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" - -// ... - -// retrieve the order -const order = await orderModuleService.retrieveOrder("ord_123", { - relations: [ - "items.item.adjustments", - "shipping_methods.shipping_method.adjustments", - ], -}) -// retrieve the line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -order.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - ...item.detail, - adjustments: filteredAdjustments, - }) - } -}) - -//retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -order.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) - -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - // TODO infer from cart or region - currency_code: "usd", - } -) -``` - -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. - -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -await orderModuleService.setOrderLineItemAdjustments( - order.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) - -await orderModuleService.setOrderShippingMethodAdjustments( - order.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` - - # Tax Lines in Order Module In this document, you’ll learn about tax lines in an order. @@ -27092,881 +25412,44 @@ The `OrderTransaction` data model has two properties that determine which data m - `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. -# Tax Module Options +# Links between Pricing Module and Other Modules -In this document, you'll learn about the options of the Tax Module. - -## providers - -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. - -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The objects in the array accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# Order Exchange - -In this document, you’ll learn about order exchanges. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. - -## What is an Exchange? - -An exchange is the replacement of an item that the customer ordered with another. - -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. - -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. - -*** - -## Returned and New Items - -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. - -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. - -*** - -## Exchange Shipping Methods - -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). - -*** - -## Exchange Payment - -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. - -|Condition|Result| -|---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| - -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -*** - -## How Exchanges Impact an Order’s Version - -When an exchange is confirmed, the order’s version is incremented. - - -# Order Claim - -In this document, you’ll learn about order claims. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. - -## What is a Claim? - -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. - -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. - -*** - -## Claim Type - -The `Claim` data model has a `type` property whose value indicates the type of the claim: - -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. - -*** - -## Old and Replacement Items - -When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. - -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). - -*** - -## Claim Shipping Methods - -A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). - -*** - -## Claim Refund - -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. - -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. - -*** - -## How Claims Impact an Order’s Version - -When a claim is confirmed, the order’s version is incremented. - - -# Order Return - -In this document, you’ll learn about order returns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. - -## What is a Return? - -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). - -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. - -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) - -Once the merchant receives the returned items, they mark the return as received. - -*** - -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). - -The `ReturnItem` model has two properties storing the item's quantity: - -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. - -*** - -## Return Shipping Methods - -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). - -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. - -*** - -## Refund Payment - -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. - -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. - -*** - -## Returns in Exchanges and Claims - -When a merchant creates an exchange or a claim, it includes returning items from the customer. - -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. - -*** - -## How Returns Impact an Order’s Version - -The order’s version is incremented when: - -1. A return is requested. -2. A return is marked as received. - - -# Tax Calculation with the Tax Provider - -In this document, you’ll learn how tax lines are calculated and what a tax provider is. - -## Tax Lines Calculation - -Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. - -For example: - -```ts -const taxLines = await taxModuleService.getTaxLines( - [ - { - id: "cali_123", - product_id: "prod_123", - unit_price: 1000, - quantity: 1, - }, - { - id: "casm_123", - shipping_option_id: "so_123", - unit_price: 2000, - }, - ], - { - address: { - country_code: "us", - }, - } -) -``` - -The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. - -The example above retrieves the tax lines based on the tax region for the United States. - -The method returns tax lines for the line item and shipping methods. For example: - -```json -[ - { - "line_item_id": "cali_123", - "rate_id": "txr_1", - "rate": 10, - "code": "XXX", - "name": "Tax Rate 1" - }, - { - "shipping_line_id": "casm_123", - "rate_id": "txr_2", - "rate": 5, - "code": "YYY", - "name": "Tax Rate 2" - } -] -``` - -*** - -## Using the Tax Provider in the Calculation - -The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. - -A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. - -The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. - -{/* --- - -TODO add once tax provider guide is updated + add module providers match other modules. - -## Create Tax Provider - -Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} - - -# Tax Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -Each tax region has tax rules and a tax provider. - - -# Inventory Module in Medusa Flows - -This document explains how the Inventory Module is used within the Medusa application's flows. - -## Product Variant Creation - -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. - -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) - -*** - -## Add to Cart - -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. - -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) - -*** - -## Order Placed - -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. - -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) - -*** - -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) - -*** - -## Order Return - -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. - -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) - -### Dismissed Returned Items - -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. - - -# Inventory Concepts - -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. - -## InventoryItem - -An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. - -The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. - -![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) - -### Inventory Shipping Requirement - -An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. - -When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). - -*** - -## InventoryLevel - -An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. - -It has three quantity-related properties: - -- `stocked_quantity`: The available stock quantity of an item in the associated location. -- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. - -### Associated Location - -The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. - -*** - -## ReservationItem - -A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. - -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. - - -# Inventory Kits - -In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. - -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: - -- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). -- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). - -## What is an Inventory Kit? - -An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. - -The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. - -Using inventory kits, you can implement use cases like: - -- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. -- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. - -*** - -## Multi-Part Products - -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. - -To implement this in Medusa, you can: - -- Create inventory items for each of the different parts. -- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. - -Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. - -![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) - -### Create Multi-Part Product - -Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: - -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // Alternatively, you can create a stock location - const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: ["*"], - filters: { - name: "European Warehouse", - }, - }) - - const inventoryItems = createInventoryItemsWorkflow.runAsStep({ - input: { - items: [ - { - sku: "FRAME", - title: "Frame", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "WHEEL", - title: "Wheel", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "SEAT", - title: "Seat", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - ], - }, - }) - - // TODO create the product - } -) -``` - -You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). - -Then, you create the inventory items that the product variant consists of. - -Next, create the product and pass the inventory item's IDs to the product's variant: - -```ts highlights={multiPartHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - // ... - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... - - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bicycle", - variants: [ - { - title: "Bicycle - Small", - prices: [ - { - amount: 100, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - shipping_profile_id: "sp_123", - }, - ], - }, - }) - } -) -``` - -You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Bundled Products - -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. - -![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) - -You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. - -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. - -![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) - -### Create Bundled Product - -You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: - -```ts highlights={bundledHighlights1} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shirt", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Pants", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Pants", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Shoes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) - - // TODO re-retrieve with inventory - } -) -``` - -You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). - -Next, retrieve the products again but with variant information: - -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const productIds = transform({ - products, - }, (data) => data.products.map((product) => product.id)) - - // @ts-ignore - const { data: productsWithInventory } = useQueryGraphStep({ - entity: "product", - fields: [ - "variants.*", - "variants.inventory_items.*", - ], - filters: { - id: productIds, - }, - }) - - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) - - // create bundled product - } -) -``` - -Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. - -Finally, create the bundled product: - -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bundled Clothes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Bundle", - prices: [ - { - amount: 30, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }).config({ name: "create-bundled-product" }) - } -) -``` - -The bundled product has the same inventory items as those of the products part of the bundle. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - - -# Links between Inventory Module and Other Modules - -This document showcases the module links defined between the Inventory Module and other Commerce Modules. +This document showcases the module links defined between the Pricing Module and other Commerce Modules. ## Summary -The Inventory Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +The Pricing Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Stored - many-to-many|| -|| in |Read-only - has many|| +| in ||Stored - one-to-one|| +| in ||Stored - one-to-one|| *** -## Product Module +## Fulfillment Module -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. +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. -![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. - -Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). +![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) ### Retrieve with Query -To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: +To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: ### query.graph ```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "variants.*", + "shipping_option.*", ], }) -// inventoryItems[0].variants +// priceSets[0].shipping_option ``` ### useQueryGraphStep @@ -27976,19 +25459,106 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "variants.*", + "shipping_option.*", ], }) -// inventoryItems[0].variants +// priceSets[0].shipping_option ``` ### Manage with Link -To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## 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. + +### Retrieve with Query + +To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +### query.graph + +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets[0].variant +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -28001,8 +25571,8 @@ await link.create({ [Modules.PRODUCT]: { variant_id: "variant_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -28019,52 +25589,502 @@ createRemoteLinkStep({ [Modules.PRODUCT]: { variant_id: "variant_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` + +# Prices Calculation + +In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. + +## calculatePrices Method + +The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. + +It returns a price object with the best matching price for each price set. + +### 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. + +For example: + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) +``` + +In this example, you retrieve the prices in a price set for the specified currency code and region ID. + +### Returned Price Object + +For each price set, the `calculatePrices` method selects two prices: + +- 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 that has the following properties: + +- id: (\`string\`) The ID of the price set from which the price was selected. +- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. +- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. +- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. +- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. +- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. +- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- calculated\_price: (\`object\`) The calculated price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. + *** -## Stock Location Module +## Examples -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. - -### Retrieve with Query - -To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph +Consider the following price set: ```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", - fields: [ - "stock_locations.*", +const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + // default price + { + amount: 500, + currency_code: "EUR", + rules: {}, + }, + // prices with rules + { + amount: 400, + currency_code: "EUR", + rules: { + region_id: "reg_123", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, ], }) - -// inventoryLevels[0].stock_locations ``` -### useQueryGraphStep +### Default Price Selection + +### Code ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` -// ... +### Result -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", - fields: [ - "stock_locations.*", +### Calculate Prices with Rules + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` + +### Result + +### Price Selection with Price List + +### Code + +```ts +const priceList = pricingModuleService.createPriceLists([{ + title: "Summer Price List", + description: "Price list for summer sale", + starts_at: Date.parse("01/10/2023").toString(), + ends_at: Date.parse("31/10/2023").toString(), + 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 pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` + +### Result + + +# Pricing Concepts + +In this document, you’ll learn about the main concepts in the Pricing Module. + +## Price Set + +A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) 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](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +![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) + +*** + +## Price List + +A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) 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. + + +# Price Rules + +In this Pricing Module guide, you'll learn about price rules for price sets and price lists, and how to add rules to a price. + +## Price Rule + +You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). + +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. + +For exmaple, you create a price restricted to `10557` zip codes. + +![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + +![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) + +*** + +## Price List Rules + +Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). + +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. + +![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) + +*** + +## How to Set Rules on a Price? + +### Using Workflows + +Medusa uses the Pricing Module to store prices of different resources, such as product variants and shipping options. + +When you manage one of these resources using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or using the API routes that use them, you can set rules on a price using the `rules` property of the price object. + +For example, when creating a shipping option using the [createShippingOptionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) to create a shipping option, you can make the shipping price free based on the cart total: + +```ts highlights={workflowHighlights} +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Standard Shipping", + service_zone_id: "serzo_123", + shipping_profile_id: "sp_123", + provider_id: "prov_123", + type: { + label: "Standard", + description: "Standard shipping", + code: "standard", + }, + price_type: "flat", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, + ], + }], + }) +``` + +In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. + +### Using Pricing Module's Service + +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. + +When adding a price using the [addPrices](https://docs.medusajs.com/resources/references/pricing/addPrices/index.html.md) method of the Pricing Module's service, pass the `rules` property to a price object. + +For example: + +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, ], }) - -// inventoryLevels[0].stock_locations ``` +In this example, you set the default price of a resource (for example, a shipping option), to `$10`. You also add a conditioned price that sets the price to `0` when the cart or order's total is greater than or equal to `$100`. + +### How is the Price Rule Applied? + +The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. + +For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "item_total": { + "operator": "gte", + "value": 100, + } + } +} +``` + +The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. + +You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "customer.group.id": { + "operator": "eq", + "value": "cusgrp_123" + } + } +} +``` + +In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. + +These same rules apply to product variant prices as well, or any other resource that has a price. + + +# Tax-Inclusive Pricing + +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. + +## What is Tax-Inclusive Pricing? + +A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. + +For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. + +*** + +## How is Tax-Inclusive Pricing Set? + +The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: + +- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. +- `value`: The attribute’s value. For example, `reg_123` or `usd`. + +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. + +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. + +For example: + +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` + +In this example, tax-inclusivity is enabled for the `USD` currency code. + +*** + +## Tax-Inclusive Pricing in Price Calculation + +### Tax Context + +As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. + +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. + +### Returned Tax Properties + +The `calculatePrices` method returns two properties related to tax-inclusivity: + +Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). + +- `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. + +A price is considered tax-inclusive if: + +1. It belongs to the region or currency code specified in the calculation context; +2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. + +### Tax Context Precedence + +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 + + +# Account Holders and Saved Payment Methods + +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. + +Account holders are available starting from Medusa `v2.5.0`. + +## What's an Account Holder? + +An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. + +It holds fields retrieved from the third-party provider, such as: + +- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. +- `data`: Data returned by the payment provider when the account holder is created. + +A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. + +### Relation between Account Holder and Customer + +The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. + +This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. + +Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. + +*** + +## Save Payment Methods + +If a payment provider supports saving payment methods for a customer, they must implement the following methods: + +- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. +- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. +- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. +- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). + +*** + +## Account Holder in Medusa Payment Flows + +In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. + +Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). + # Links between Payment Module and Other Modules @@ -28467,41 +26487,6 @@ The `providers` option is an array of objects that accept the following properti - `options`: An optional object of the module provider's options. -# Payment - -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. - -## What's a Payment? - -When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. - -A payment carries many of the data and relations of a payment session: - -- It belongs to the same payment collection. -- It’s associated with the same payment provider, which handles further payment processing. -- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. - -*** - -## Capture Payments - -When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. - -The payment can also be captured incrementally, each time a capture record is created for that amount. - -![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) - -*** - -## Refund Payments - -When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. - -A payment can be refunded multiple times, and each time a refund record is created. - -![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) - - # Payment Collection In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. @@ -28539,6 +26524,41 @@ It also implements the payment flow during checkout as explained in [this docume ![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +# Payment + +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. + +## What's a Payment? + +When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. + +A payment carries many of the data and relations of a payment session: + +- It belongs to the same payment collection. +- It’s associated with the same payment provider, which handles further payment processing. +- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. + +*** + +## Capture Payments + +When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. + +The payment can also be captured incrementally, each time a capture record is created for that amount. + +![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) + +*** + +## Refund Payments + +When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. + +A payment can be refunded multiple times, and each time a refund record is created. + +![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) + + # Accept Payment Flow In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. @@ -28757,39 +26777,6 @@ When the Medusa application starts and registers the payment providers, it also This data model is used to reference a payment provider and determine whether it’s installed in the application. -# Payment Session - -In this document, you’ll learn what a payment session is. - -## What's a Payment Session? - -A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. - -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. - -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) - -*** - -## data Property - -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. - -For example, the customer's ID in Stripe is stored in the `data` property. - -*** - -## Payment Session Status - -The `status` property of a payment session indicates its current status. Its value can be: - -- `pending`: The payment session is awaiting authorization. -- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. -- `authorized`: The payment session is authorized. -- `error`: An error occurred while authorizing the payment. -- `canceled`: The authorization of the payment session has been canceled. - - # Webhook Events In this document, you’ll learn how the Payment Module supports listening to webhook events. @@ -28826,53 +26813,2185 @@ If the event's details indicate that the payment should be captured, then the [c After the payment webhook actions are processed and the payment is authorized or captured, the Medusa application completes the cart associated with the payment's collection if it's not completed yet. -# Account Holders and Saved Payment Methods +# Payment Session -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. +In this document, you’ll learn what a payment session is. -Account holders are available starting from Medusa `v2.5.0`. +## What's a Payment Session? -## What's an Account Holder? +A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -It holds fields retrieved from the third-party provider, such as: - -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. - -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. - -### Relation between Account Holder and Customer - -The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. - -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. - -Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) *** -## Save Payment Methods +## data Property -If a payment provider supports saving payment methods for a customer, they must implement the following methods: +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). +For example, the customer's ID in Stripe is stored in the `data` property. *** -## Account Holder in Medusa Payment Flows +## Payment Session Status -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. +The `status` property of a payment session indicates its current status. Its value can be: -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. +- `pending`: The payment session is awaiting authorization. +- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. +- `authorized`: The payment session is authorized. +- `error`: An error occurred while authorizing the payment. +- `canceled`: The authorization of the payment session has been canceled. -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). + +# Links between Product Module and Other Modules + +This document showcases the module links defined between the Product Module and other Commerce Modules. + +## Summary + +The Product Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only - has one|| +|| in |Stored - many-to-one|| +|| in |Stored - many-to-many|| +| in ||Read-only - has one|| +|| in |Stored - one-to-one|| +|| in |Stored - many-to-many|| + +*** + +## Cart Module + +Medusa defines read-only links between: + +- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. +- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +*** + +## Fulfillment Module + +Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). + +### Retrieve with Query + +To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`: + +### query.graph + +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) + +// products[0].shipping_profile +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) + +// products[0].shipping_profile +``` + +### Manage with Link + +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +*** + +## 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. + +Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +### Retrieve with Query + +To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: + +### query.graph + +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "inventory_items.*", + ], +}) + +// variants[0].inventory_items +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "inventory_items.*", + ], +}) + +// variants[0].inventory_items +``` + +### Manage with Link + +To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +*** + +## Order Module + +Medusa defines read-only links between: + +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant +``` + +*** + +## 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. + +### Retrieve with Query + +To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: + +### query.graph + +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) + +// variants[0].price_set +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) + +// variants[0].price_set +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## 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) + +### Retrieve with Query + +To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "sales_channels.*", + ], +}) + +// products[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "sales_channels.*", + ], +}) + +// products[0].sales_channels +``` + +### Manage with Link + +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + + +# Configure Selling Products + +In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. + +The concepts in this guide are applicable starting from Medusa v2.5.1. + +## Scenario + +Businesses can have different selling requirements: + +1. They may sell physical or digital items. +2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. +3. They may sell items whose inventory is managed by an external system, such as an ERP. + +Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. + +*** + +## Configuring Shipping Requirements + +The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. + +When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. + +If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. + +### Overriding Shipping Requirements for Variants + +A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. + +Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: + +1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. +2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. + +*** + +## Use Case Examples + +By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: + +|Use Case|Configurations|Example| +|---|---|---|---|---| +|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| +|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| +|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| +|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| + + +# Product Variant Inventory + +# Product Variant Inventory + +In this guide, you'll learn about the inventory management features related to product variants. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants. + +## Configure Inventory Management of Product Variants + +A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP. + +The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. + +When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. + +*** + +## How the Medusa Application Manages Inventory + +When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. + +![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) + +The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. + +![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) + +Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). + +The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. + +![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) + +Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). + +### Product Inventory in Storefronts + +When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. + +The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. + +For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. + +![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) + +*** + +## Variant Back Orders + +Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. + +You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). + +*** + +## Additional Resources + +The following guides provide more details on inventory management in the Medusa application: + +- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. +- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products. +- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. +- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). + + +# Links between Region Module and Other Modules + +This document showcases the module links defined between the Region Module and other Commerce Modules. + +## Summary + +The Region Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only - has one|| +| in ||Read-only - has one|| +|| in |Stored - many-to-many|| + +*** + +## Cart Module + +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts[0].region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts[0].region +``` + +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders[0].region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders[0].region +``` + +*** + +## Payment Module + +You can specify for each region which payment providers are available for use. + +Medusa defines a module link between the `PaymentProvider` and the `Region` data models. + +![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) + +### Retrieve with Query + +To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: + +### query.graph + +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions[0].payment_providers +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions[0].payment_providers +``` + +### Manage with Link + +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + + +# Stock Location Concepts + +In this document, you’ll learn about the main concepts in the Stock Location Module. + +## Stock Location + +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. + +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. + +*** + +## StockLocationAddress + +The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. + + +# Links between Stock Location Module and Other Modules + +This document showcases the module links defined between the Stock Location Module and other Commerce Modules. + +## Summary + +The Stock Location Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored - many-to-one|| +| in ||Stored - many-to-many|| +| in ||Read-only - has many|| +| in ||Stored - many-to-many|| + +*** + +## Fulfillment Module + +A fulfillment set can be conditioned to a specific stock location. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: + +To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations[0].fulfillment_sets +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations[0].fulfillment_sets +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +*** + +## Inventory Module + +Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. + +### Retrieve with Query + +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + +*** + +## Sales Channel Module + +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels +``` + +### Manage with Link + +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + + +# Publishable API Keys with Sales Channels + +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. + +## Publishable API Keys with Sales Channels + +A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. + +When sending a request to a Store API route, you must pass a publishable API key in the header of the request: + +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} +``` + +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. + +*** + +## How to Create a Publishable API Key? + +To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). + + +# Links between Sales Channel Module and Other Modules + +This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. + +## Summary + +The Sales Channel Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored - many-to-many|| +| in ||Read-only - has one|| +| in ||Read-only - has one|| +| in ||Stored - many-to-many|| +|| in |Stored - many-to-many|| + +*** + +## API Key Module + +A publishable API key allows you to easily specify the sales channel scope in a client request. + +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) + +### Retrieve with Query + +To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels[0].publishable_api_keys +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels[0].publishable_api_keys +``` + +### Manage with Link + +To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +*** + +## Cart Module + +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts[0].sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts[0].sales_channel +``` + +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders.sales_channel +``` + +*** + +## Product Module + +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) + +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. + +### Retrieve with Query + +To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) + +// salesChannels[0].products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) + +// salesChannels[0].products +``` + +### Manage with Link + +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +*** + +## Stock Location Module + +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], +}) + +// salesChannels[0].stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], +}) + +// salesChannels[0].stock_locations +``` + +### Manage with Link + +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + + +# Application Method + +In this document, you'll learn what an application method is. + +## What is an Application Method? + +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: + +|Property|Purpose| +|---|---| +|\`type\`|Does the promotion discount a fixed amount or a percentage?| +|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| +|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| + +## Target Promotion Rules + +When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. + +The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. + +![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) + +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. + +*** + +## Buy Promotion Rules + +When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. + +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. + +![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) + +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. + + +# Promotion Actions + +In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). + +## computeActions Method + +The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. + +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. + +*** + +## Action Types + +### `addItemAdjustment` Action + +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. + +This action has the following format: + +```ts +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string +} +``` + +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. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. + +### `removeItemAdjustment` Action + +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. + +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} +``` + +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. + +### `addShippingMethodAdjustment` Action + +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. + +This action has the following format: + +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string +} +``` + +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. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `removeShippingMethodAdjustment` Action + +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. + +The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveShippingMethodAdjustment { + action: "removeShippingMethodAdjustment" + adjustment_id: string + code: string +} +``` + +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `campaignBudgetExceeded` Action + +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. + +This action has the following format: + +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string +} +``` + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. + + +# Campaign + +In this document, you'll learn about campaigns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. + +## What is a Campaign? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + +![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) + +*** + +## Campaign Limits + +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. + +There are two types of budgets: + +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. + +![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) + + +# Promotion Concepts + +In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. + +## What is a Promotion? + +A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. + +A promotion has two types: + +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. + +|\`standard\`|\`buyget\`| +|---|---| +|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| +|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| +|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| + +The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. + +*** + +## Promotion Rules + +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). + +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`. + +The expected value for the attribute 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. + +### Flexible Rules + +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). + +For example, to restrict the promotion to only `VIP` and `B2B` customer groups: + +- 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) + +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. + +*** + +## How to Apply Rules on a Promotion? + +### Using Workflows + +If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md). + +For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createPromotionsWorkflow/index.html.md): + +```ts +const { result } = await createPromotionsWorkflow(container) + .run({ + input: { + promotionsData: [{ + code: "10OFF", + type: "standard", + status: "active", + application_method: { + type: "percentage", + target_type: "items", + allocation: "across", + value: 10, + currency_code: "usd", + }, + rules: [ + { + attribute: "customer.group.id", + operator: "eq", + values: [ + "cusgrp_123" + ] + } + ] + }], + } + }) +``` + +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. + +### Using Promotion Module's Service + +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. + +If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods. + +For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/resources/references/promotion/createPromotions/index.html.md) method: + +```ts +const promotions = await promotionModuleService.createPromotions([ + { + code: "50OFF", + type: "standard", + status: "active", + application_method: { + type: "percentage", + target_type: "items", + value: 50, + }, + rules: [ + { + attribute: "customer.group.id", + operator: "eq", + values: [ + "cusgrp_123" + ] + } + ] + }, +]) +``` + +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. + +### How is the Promotion Rule Applied? + +A promotion is applied on a resource if its attributes match the promotion's rules. + +For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer: + +```json +{ + "code": "10OFF", + "type": "standard", + "status": "active", + "application_method": { + "type": "percentage", + "target_type": "items", + "allocation": "across", + "value": 10, + "currency_code": "usd" + }, + "rules": [ + { + "attribute": "customer_id", + "operator": "eq", + "values": [ + "cus_123" + ] + } + ] +} +``` + +When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`. + + +# Links between Promotion Module and Other Modules + +This document showcases the module links defined between the Promotion Module and other Commerce Modules. + +## Summary + +The Promotion Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored - many-to-many|| +| in ||Read-only - has one|| +| in ||Stored - many-to-many|| + +*** + +## Cart Module + +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` 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) + +Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. + +### Retrieve with Query + +To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions[0].carts +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions[0].carts +``` + +### Manage with Link + +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Order Module + +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) + +### Retrieve with Query + +To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions[0].orders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions[0].orders +``` + +### Manage with Link + +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + + +# Links between Store Module and Other Modules + +This document showcases the module links defined between the Store Module and other Commerce Modules. + +## Summary + +The Store Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Read-only - has many|| + +*** + +## Currency Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). + +### Retrieve with Query + +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: + +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies +``` + + +# Tax Module Options + +In this document, you'll learn about the options of the Tax Module. + +## providers + +The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. + +When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The objects in the array accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Tax Calculation with the Tax Provider + +In this document, you’ll learn how tax lines are calculated and what a tax provider is. + +## Tax Lines Calculation + +Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. + +For example: + +```ts +const taxLines = await taxModuleService.getTaxLines( + [ + { + id: "cali_123", + product_id: "prod_123", + unit_price: 1000, + quantity: 1, + }, + { + id: "casm_123", + shipping_option_id: "so_123", + unit_price: 2000, + }, + ], + { + address: { + country_code: "us", + }, + } +) +``` + +The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. + +The example above retrieves the tax lines based on the tax region for the United States. + +The method returns tax lines for the line item and shipping methods. For example: + +```json +[ + { + "line_item_id": "cali_123", + "rate_id": "txr_1", + "rate": 10, + "code": "XXX", + "name": "Tax Rate 1" + }, + { + "shipping_line_id": "casm_123", + "rate_id": "txr_2", + "rate": 5, + "code": "YYY", + "name": "Tax Rate 2" + } +] +``` + +*** + +## Using the Tax Provider in the Calculation + +The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. + +A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. + +The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. + +{/* --- + +TODO add once tax provider guide is updated + add module providers match other modules. + +## Create Tax Provider + +Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} + + +# Tax Rates and Rules + +In this document, you’ll learn about tax rates and rules. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. + +## What are Tax Rates? + +A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. + +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. + +### Combinable Tax Rates + +Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. + +Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. + +*** + +## Override Tax Rates with Rules + +You can create tax rates that override the default for specific conditions or rules. + +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. + +A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. + +![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) + +These two properties of the data model identify the rule’s target: + +- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. +- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. + +So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. + + +# Tax Region + +In this document, you’ll learn about tax regions and how to use them with the Region Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +## What is a Tax Region? + +A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. + +Tax regions can inherit settings and rules from a parent tax region. + +Each tax region has tax rules and a tax provider. + + +# User Module Options + +In this document, you'll learn about the options of the User Module. + +## Module Options + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/user", + options: { + jwt_secret: process.env.JWT_SECRET, + }, + }, + ], +}) +``` + +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +JWT_SECRET=supersecret +``` # User Creation Flows @@ -28955,43 +29074,6 @@ if (!count) { ``` -# User Module Options - -In this document, you'll learn about the options of the User Module. - -## Module Options - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], -}) -``` - -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -JWT_SECRET=supersecret -``` - - # Emailpass Auth Module Provider In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. @@ -29054,196 +29136,6 @@ const hashConfig = \{ - [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) -# Get Product Variant Prices using Query - -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. - -So, to retrieve data across the linked records of the two modules, you use Query. - -## Retrieve All Product Variant Prices - -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) -``` - -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -*** - -## Retrieve Calculated Price for a Context - -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -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 that sets the context for the retrieved fields. - -For example: - -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" - -// ... - -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, -}) -``` - -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. - -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - - -# Stripe Module Provider - -In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. - -Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. - -## Register the Stripe Module Provider - -### Prerequisites - -- [Stripe account](https://stripe.com/) -- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) -- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) - -The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -STRIPE_API_KEY= -``` - -### Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| -|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| -|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| -|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| -|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| - -*** - -## Enable Stripe Providers in a Region - -Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. - -Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. - -*** - -## Setup Stripe Webhooks - -For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. - -### Webhook URL - -Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: - -- `{server_url}` is the URL to your deployed Medusa application in server mode. -- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. - -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: - -|Stripe Payment Type|Webhook Endpoint URL| -|---|---|---| -|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| -|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| -|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| -|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| -|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| - -### Webhook Events - -When you set up the webhook in Stripe, choose the following events to listen to: - -- `payment_intent.amount_capturable_updated` -- `payment_intent.succeeded` -- `payment_intent.payment_failed` - -*** - -## Useful Guides - -- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). -- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). -- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). - - # GitHub Auth Module Provider In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. @@ -29413,6 +29305,196 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass - [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). +# Stripe Module Provider + +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. + +Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. + +## Register the Stripe Module Provider + +### Prerequisites + +- [Stripe account](https://stripe.com/) +- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) +- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) + +The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +STRIPE_API_KEY= +``` + +### Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| +|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| +|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| +|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| +|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| + +*** + +## Enable Stripe Providers in a Region + +Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. + +Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. + +*** + +## Setup Stripe Webhooks + +For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. + +### Webhook URL + +Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: + +- `{server_url}` is the URL to your deployed Medusa application in server mode. +- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. + +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: + +|Stripe Payment Type|Webhook Endpoint URL| +|---|---|---| +|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| +|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| +|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| +|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| +|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| + +### Webhook Events + +When you set up the webhook in Stripe, choose the following events to listen to: + +- `payment_intent.amount_capturable_updated` +- `payment_intent.succeeded` +- `payment_intent.payment_failed` + +*** + +## Useful Guides + +- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). +- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). +- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). + + +# Get Product Variant Prices using Query + +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. + +So, to retrieve data across the linked records of the two modules, you use Query. + +## Retrieve All Product Variant Prices + +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. + +For example: + +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", + ], + filters: { + id: [ + "prod_123", + ], + }, +}) +``` + +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +*** + +## Retrieve Calculated Price for a Context + +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. + +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). + +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 that sets the context for the retrieved fields. + +For example: + +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" + +// ... + +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, + }, +}) +``` + +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. + +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). + +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). + + # Calculate Product Variant Price with Taxes In this document, you'll learn how to calculate a product variant's price with taxes. @@ -29600,64 +29682,65 @@ For each product variant, you: ## Workflows -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) +- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) - [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) -- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) -- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) - [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) -- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) -- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) +- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) +- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) +- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) - [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) -- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) - [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) -- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md) +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) +- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) +- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) +- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) - [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) +- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) +- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) +- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) +- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) - [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) - [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) +- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) - [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) - [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) +- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) - [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) -- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) -- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) +- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) - [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) -- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) +- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) +- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) - [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) - [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) - [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) - [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) @@ -29665,537 +29748,536 @@ For each product variant, you: - [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) - [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) - [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) +- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) +- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) - [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) - [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) - [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) -- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) -- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) -- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) -- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) -- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) -- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) +- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) +- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) - [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) - [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) - [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) - [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) - [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) - [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) - [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) - [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) - [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) - [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) - [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) - [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) - [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) - [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) - [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) - [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) - [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) -- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) - [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) - [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) -- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) - [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) - [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) - [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) -- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) - [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) - [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) -- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) - [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) - [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) - [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) - [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) - [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) - [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) - [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) - [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) - [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) - [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) -- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) - [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) - [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) - [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) - [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) +- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) - [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) - [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) +- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) +- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) - [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) -- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) - [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) -- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) - [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) -- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) - [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) - [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) +- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) - [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) - [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) - [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) +- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) - [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) - [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) - [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) +- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) -- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) - [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) +- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) +- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) - [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) - [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) - [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) - [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) - [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) - [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) - [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) - [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) - [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) -- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) - [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) - [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) - [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) - [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) -- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) - [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) - [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) - [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) - [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) +- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) - [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) - [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) - [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) - [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) +- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) +- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) - [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) - [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) - [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) -- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) - [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) -- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) - [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) +- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) +- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) - [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) - [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) +- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) +- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) +- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) - [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) -- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) -- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) -- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) -- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) -- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) -- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) -- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) -- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) -- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) - [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) - [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) -- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) -- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) -- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) -- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) -- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) +- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) +- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) +- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) +- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) +- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) +- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) +- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) - [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) -- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) -- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) - [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) - [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) - [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) +- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) +- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) +- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) +- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) +- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) +- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) - [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) - [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) - [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) - [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) -- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) -- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) - [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) - [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) +- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) +- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) +- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) +- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) ## Steps -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) -- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) -- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) -- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) -- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) -- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) -- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) - [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) - [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) - [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) -- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) - [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) - [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) +- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) - [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) - [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) - [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) - [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) - [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) - [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) - [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) - [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) - [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) +- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) - [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) - [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) - [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) - [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) -- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) - [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) - [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) - [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) - [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) +- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) - [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) - [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) - [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) +- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) +- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) +- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) +- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) +- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) +- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) +- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) +- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) +- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) +- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) +- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) +- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) +- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) +- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) +- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) - [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) -- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) -- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) +- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) +- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) +- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) +- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) +- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) +- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) - [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) -- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) -- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) +- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) +- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) - [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) +- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) - [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) - [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) -- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) +- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) - [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) -- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) -- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) - [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) +- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) - [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) +- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) +- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) - [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) - [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) - [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) -- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) +- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) - [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) +- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) - [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) - [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) -- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) - [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) +- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) - [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) - [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) - [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) - [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) - [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) -- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) -- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) -- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) -- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) -- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) -- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) -- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) -- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) -- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) - [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) - [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) +- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) +- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) +- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) +- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) +- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) +- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) +- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) +- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) +- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) +- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) +- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) +- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) +- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) +- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) +- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) +- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) +- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) +- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) +- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) +- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) +- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) +- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) +- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) - [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) - [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) - [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) - [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) -- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) - [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) -- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) -- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) -- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) -- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) -- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) -- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) -- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) -- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) -- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) -- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) -- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) -- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) -- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) -- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) -- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) -- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) -- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) -- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) -- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) -- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) +- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) +- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) +- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) +- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) +- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) - [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) - [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) - [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) - [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) - [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) -- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) - [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) -- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) -- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) -- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) -- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) -- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) -- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) -- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) +- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) - [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) - [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) - [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) - [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) - [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) - [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) - [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) - [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) +- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) - [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) - [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) -- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) -- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) +- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) - [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) - [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) - [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) - [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) - [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) -- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) -- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) - [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) - [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) - [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) -- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) +- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) - [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) - [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) - [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) -- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) +- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) +- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) +- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) - [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) - [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) - [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) - [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) - [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) - [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) - [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) -- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) -- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) -- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) - [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) - [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) +- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) - [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) - [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) +- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) +- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) +- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) +- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) +- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) +- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) +- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) +- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) +- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) +- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) +- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) +- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) +- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) +- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) +- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) +- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) +- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) +- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) +- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) +- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) +- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) - [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) - [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) +- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) +- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) - [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) - [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) -- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) -- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) -- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) -- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) -- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) -- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) -- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) -- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) -- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) -- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) -- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) -- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) -- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) -- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) -- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) -- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) -- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) -- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) -- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) -- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) # Medusa CLI Reference @@ -30221,6 +30303,68 @@ npx medusa --help *** +# build Command - Medusa CLI Reference + +Create a standalone build of the Medusa application. + +This creates a build that: + +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. + +The build is outputted to a new `.medusa/server` directory. + +```bash +npx medusa build +``` + +Refer to [this section](#run-built-medusa-application) for next steps. + +## Options + +|Option|Description| +|---|---|---| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | + +*** + +## Run Built Medusa Application + +After running the `build` command, use the following step to run the built Medusa application: + +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install +``` + +- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. + +```bash npm2yarn +cp .env .medusa/server/.env.production +``` + +- In the system environment variables, set `NODE_ENV` to `production`: + +```bash +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start +``` + +*** + +## Build Medusa Admin + +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. + +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. + + # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -30341,68 +30485,6 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -# build Command - Medusa CLI Reference - -Create a standalone build of the Medusa application. - -This creates a build that: - -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. - -The build is outputted to a new `.medusa/server` directory. - -```bash -npx medusa build -``` - -Refer to [this section](#run-built-medusa-application) for next steps. - -## Options - -|Option|Description| -|---|---|---| -|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | - -*** - -## Run Built Medusa Application - -After running the `build` command, use the following step to run the built Medusa application: - -- Change to the `.medusa/server` directory and install the dependencies: - -```bash npm2yarn -cd .medusa/server && npm install -``` - -- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. - -```bash npm2yarn -cp .env .medusa/server/.env.production -``` - -- In the system environment variables, set `NODE_ENV` to `production`: - -```bash -NODE_ENV=production -``` - -- Use the `start` command to run the application: - -```bash npm2yarn -cd .medusa/server && npm run start -``` - -*** - -## Build Medusa Admin - -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. - -If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. - - # exec Command - Medusa CLI Reference Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). @@ -30782,6 +30864,51 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# develop Command - Medusa CLI Reference + +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + +```bash +npx medusa develop +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + # exec Command - Medusa CLI Reference Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). @@ -30859,68 +30986,6 @@ npx medusa plugin:build ``` -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# new Command - Medusa CLI Reference - -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. - -```bash -medusa new [ []] -``` - -## Arguments - -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| - -## Options - -|Option|Description| -|---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| - - -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| -|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| - - # telemetry Command - Medusa CLI Reference Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. @@ -30956,6 +31021,23 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| +|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| + + # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -40095,6 +40177,1797 @@ For a quick access to code snippets of the different concepts you learned about, Deployment guides are a collection of guides that help you deploy your Medusa server and admin to different platforms. Learn more in the [Deployment Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md) documentation. +# Implement Loyalty Points System in Medusa + +In this tutorial, you'll learn how to implement a loyalty points system in Medusa. + +Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more. + +A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +## Summary + +By following this tutorial, you will learn how to: + +- Install and set up Medusa. +- Define models to store loyalty points and the logic to manage them. +- Build flows that allow customers to earn and redeem points during checkout. + - Points are redeemed through dynamic promotions specific to the customer. +- Customize the cart completion flow to validate applied loyalty points. + +![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg) + +- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes. + +Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Create Loyalty Module + +In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module. + +In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers. + +Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. + +### Create Module Directory + +Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`. + +### Create Data Models + +A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more. + +For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content: + +```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights} +import { model } from "@medusajs/framework/utils" + +const LoyaltyPoint = model.define("loyalty_point", { + id: model.id().primaryKey(), + points: model.number().default(0), + customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), +}) + +export default LoyaltyPoint +``` + +You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter. + +The `LoyaltyPoint` data model has the following properties: + +- `id`: A unique ID for the loyalty points. +- `points`: The number of loyalty points a customer has. +- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table. + +Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md). + +### Create Module's Service + +You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module. + +A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services. + +Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more. + +To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content: + +```ts title="src/modules/loyalty/service.ts" +import { MedusaError, MedusaService } from "@medusajs/framework/utils" +import LoyaltyPoint from "./models/loyalty-point" +import { InferTypeOf } from "@medusajs/framework/types" + +type LoyaltyPoint = InferTypeOf + +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // TODO add methods +} + +export default LoyaltyModuleService +``` + +The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. + +So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`. + +Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). + +#### Add Methods to the Service + +Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points. + +First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + async addPoints(customerId: string, points: number): Promise { + const existingPoints = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + if (existingPoints.length > 0) { + return await this.updateLoyaltyPoints({ + id: existingPoints[0].id, + points: existingPoints[0].points + points, + }) + } + + return await this.createLoyaltyPoints({ + customer_id: customerId, + points, + }) + } +} +``` + +You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add. + +In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method. + +Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method. + +The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async deductPoints(customerId: string, points: number): Promise { + const existingPoints = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + if (existingPoints.length === 0 || existingPoints[0].points < points) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Insufficient loyalty points" + ) + } + + return await this.updateLoyaltyPoints({ + id: existingPoints[0].id, + points: existingPoints[0].points - points, + }) + } +} +``` + +The `deductPoints` method accepts the customer ID and the points to deduct. + +In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error. + +Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`. + +Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async getPoints(customerId: string): Promise { + const points = await this.listLoyaltyPoints({ + customer_id: customerId, + }) + + return points[0]?.points || 0 + } +} +``` + +The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`. + +#### Add Method to Map Points to Discount + +Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout. + +The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation. + +To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount. + +Add the following method to the `LoyaltyModuleService`: + +```ts title="src/modules/loyalty/service.ts" +class LoyaltyModuleService extends MedusaService({ + LoyaltyPoint, +}) { + // ... + async calculatePointsFromAmount(amount: number): Promise { + // Convert amount to points using a standard conversion rate + // For example, $1 = 1 point + // Round down to nearest whole point + const points = Math.floor(amount) + + if (points < 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Amount cannot be negative" + ) + } + + return points + } +} +``` + +The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error. + +You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points. + +### Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. + +So, create the file `src/modules/loyalty/index.ts` with the following content: + +```ts title="src/modules/loyalty/index.ts" +import { Module } from "@medusajs/framework/utils" +import LoyaltyModuleService from "./service" + +export const LOYALTY_MODULE = "loyalty" + +export default Module(LOYALTY_MODULE, { + service: LoyaltyModuleService, +}) +``` + +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `loyalty`. +2. An object with a required property `service` indicating the module's service. + +You also export the module's name as `LOYALTY_MODULE` so you can reference it later. + +### Add Module to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/loyalty", + }, + ], +}) +``` + +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. + +### Generate Migrations + +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. + +Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more. + +Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory: + +```bash +npx medusa db:generate loyalty +``` + +The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration. + +Then, to reflect these migrations on the database, run the following command: + +```bash +npx medusa db:migrate +``` + +The table for the `LoyaltyPoint` data model is now created in the database. + +*** + +## Step 3: Change Loyalty Points Flow + +Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points. + +The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points. + +To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint. + +In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed. + +Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. +- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. + +Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps. + +### validateCustomerExistsStep + +In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points. + +To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content: + +```ts title="src/workflows/steps/validate-customer-exists.ts" +import { CustomerDTO } from "@medusajs/framework/types" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { MedusaError } from "@medusajs/framework/utils" + +export type ValidateCustomerExistsStepInput = { + customer: CustomerDTO | null | undefined +} + +export const validateCustomerExistsStep = createStep( + "validate-customer-exists", + async ({ customer }: ValidateCustomerExistsStepInput) => { + if (!customer) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer not found" + ) + } + + if (!customer.has_account) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer must have an account to earn or manage points" + ) + } + } +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `validate-customer-exists`. +2. An async function that receives two parameters: + - The step's input, which is in this case an object with the customer's details. + - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. + +In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error. + +### getCartLoyaltyPromoStep + +Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points. + +Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations. + +Create the file `src/utils/promo.ts` with the following content: + +```ts title="src/utils/promo.ts" +import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types" + +export type CartData = CartDTO & { + promotions?: PromotionDTO[] + customer?: CustomerDTO + metadata: { + loyalty_promo_id?: string + } +} + +export function getCartLoyaltyPromotion( + cart: CartData +): PromotionDTO | undefined { + if (!cart?.metadata?.loyalty_promo_id) { + return + } + + return cart.promotions?.find( + (promotion) => promotion.id === cart.metadata.loyalty_promo_id + ) +} +``` + +You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property. + +You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content: + +```ts title="src/workflows/steps/get-cart-loyalty-promo.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" +import { MedusaError } from "@medusajs/framework/utils" + +type GetCartLoyaltyPromoStepInput = { + cart: CartData, + throwErrorOn?: "found" | "not-found" +} + +export const getCartLoyaltyPromoStep = createStep( + "get-cart-loyalty-promo", + async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => { + const loyaltyPromo = getCartLoyaltyPromotion(cart) + + if (throwErrorOn === "found" && loyaltyPromo) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Loyalty promotion already applied to cart" + ) + } else if (throwErrorOn === "not-found" && !loyaltyPromo) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "No loyalty promotion found on cart" + ) + } + + return new StepResponse(loyaltyPromo) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `cart`: The cart's details. +- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found. + +The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows. + +In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error. + +Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error. + +To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case. + +### deductPurchasePointsStep + +If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content: + +```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import LoyaltyModuleService from "../../modules/loyalty/service" + +type DeductPurchasePointsInput = { + customer_id: string + amount: number +} + +export const deductPurchasePointsStep = createStep( + "deduct-purchase-points", + async ({ + customer_id, amount, + }: DeductPurchasePointsInput, { container }) => { + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount( + amount + ) + + const result = await loyaltyModuleService.deductPoints( + customer_id, + pointsToDeduct + ) + + return new StepResponse(result, { + customer_id, + points: pointsToDeduct, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + // Restore points in case of failure + await loyaltyModuleService.addPoints( + data.customer_id, + data.points + ) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `customer_id`: The ID of the customer to deduct points from. +- `amount`: The promotion's amount, which will be used to calculate the points to deduct. + +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount. + +After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points. + +Finally, you return a `StepResponse` with the result of the `deductPoints`. + +#### Compensation Function + +This step has a compensation function, which is passed as a third parameter to the `createStep` function. + +The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems. + +The compensation function accepts two parameters: + +1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance. +2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs. + +### addPurchaseAsPointsStep + +The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout. + +To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content: + +```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import LoyaltyModuleService from "../../modules/loyalty/service" + +type StepInput = { + customer_id: string + amount: number +} + +export const addPurchaseAsPointsStep = createStep( + "add-purchase-as-points", + async (input: StepInput, { container }) => { + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount( + input.amount + ) + + const result = await loyaltyModuleService.addPoints( + input.customer_id, + pointsToAdd + ) + + return new StepResponse(result, { + customer_id: input.customer_id, + points: pointsToAdd, + }) + }, + async (data, { container }) => { + if (!data) { + return + } + + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + await loyaltyModuleService.deductPoints( + data.customer_id, + data.points + ) + } +) +``` + +You create a step that accepts an object having the following properties: + +- `customer_id`: The ID of the customer to add points to. +- `amount`: The order's amount, which will be used to calculate the points to add. + +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount. + +After that, you call the `addPoints` method to add the points to the customer's loyalty points. + +Finally, you return a `StepResponse` with the result of the `addPoints`. + +You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs. + +### Add Utility Functions + +Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute. + +To add the utility function, add the following to `src/utils/promo.ts`: + +```ts title="src/utils/promo.ts" +import { OrderDTO } from "@medusajs/framework/types" + +export type OrderData = OrderDTO & { + promotion?: PromotionDTO[] + customer?: CustomerDTO + cart?: CartData +} + +export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id" + +export function orderHasLoyaltyPromotion(order: OrderData): boolean { + const loyaltyPromotion = getCartLoyaltyPromotion( + order.cart as unknown as CartData + ) + + return loyaltyPromotion?.rules?.some((rule) => { + return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && ( + rule?.values?.some((value) => value.value === order.customer?.id) || false + ) + }) || false +} +``` + +You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details. + +Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid. + +Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if: + +- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion. +- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID. + - When you create the promotion for the cart later, you'll see how to set this rule. + +You'll use this utility in the workflow next. + +### Create the Workflow + +Now that you have all the steps, you can create the workflow that uses them. + +To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content: + +```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { createWorkflow, when } from "@medusajs/framework/workflows-sdk" +import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists" +import { deductPurchasePointsStep } from "./steps/deduct-purchase-points" +import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points" +import { OrderData, CartData } from "../utils/promo" +import { orderHasLoyaltyPromotion } from "../utils/promo" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" + +type WorkflowInput = { + order_id: string +} + +export const handleOrderPointsWorkflow = createWorkflow( + "handle-order-points", + ({ order_id }: WorkflowInput) => { + // @ts-ignore + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "customer.*", + "total", + "cart.*", + "cart.promotions.*", + "cart.promotions.rules.*", + "cart.promotions.rules.values.*", + "cart.promotions.application_method.*", + ], + filters: { + id: order_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateCustomerExistsStep({ + customer: orders[0].customer, + } as ValidateCustomerExistsStepInput) + + const loyaltyPointsPromotion = getCartLoyaltyPromoStep({ + cart: orders[0].cart as unknown as CartData, + }) + + when(orders, (orders) => + orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && + loyaltyPointsPromotion !== undefined + ) + .then(() => { + deductPurchasePointsStep({ + customer_id: orders[0].customer!.id, + amount: loyaltyPointsPromotion.application_method!.value as number, + }) + + updatePromotionsStep([ + { + id: loyaltyPointsPromotion.id, + status: "inactive", + }, + ]) + }) + + + when( + orders, + (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData) + ) + .then(() => { + addPurchaseAsPointsStep({ + customer_id: orders[0].customer!.id, + amount: orders[0].total, + }) + }) + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object with the order's ID. + +In the workflow's constructor function, you: + +- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order. + - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules. +- Validate that the customer is registered using the `validateCustomerExistsStep`. +- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`. +- Use `when` to check whether the order's cart has a loyalty promotion. + - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied. + - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied. + - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method. + - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion. +- You use another `when` to check whether the order's cart doesn't have a loyalty promotion. + - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points. + +You'll use this workflow next when an order is placed. + +To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow. + +*** + +## Step 4: Handle Order Placed Event + +Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed. + +Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event. + +In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow. + +Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content: + +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { handleOrderPointsWorkflow } from "../workflows/handle-order-points" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await handleOrderPointsWorkflow(container).run({ + input: { + order_id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +The subscriber file must export: + +- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. +- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. + +The subscriber function accepts an object with the following properties: + +- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload. +- `container`: The Medusa container, which you can use to resolve services and tools. + +In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input. + +Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow. + +### Test it Out + +To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory. + +So, run the following command in the Medusa application's directory to start the Medusa server: + +```bash title="Medusa Application" +npm run dev +``` + +Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: + +```bash title="Next.js Starter Storefront" +npm run dev +``` + +The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`. + +Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right. + +Once you're logged in, add an item to the cart and go through the checkout flow. + +After you place the order, you'll see the following message in your Medusa application's terminal: + +```bash +info: Processing order.placed which has 1 subscribers +``` + +This message indicates that the `order.placed` event was emitted, and that your subscriber was executed. + +Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step. + +*** + +## Step 5: Retrieve Loyalty Points API Route + +Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout. + +To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. + +You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer. + +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. + +So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content: + +```ts title="src/api/store/customers/me/loyalty-points/route.ts" + +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { LOYALTY_MODULE } from "../../../../../modules/loyalty" +import LoyaltyModuleService from "../../../../../modules/loyalty/service" + +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve( + LOYALTY_MODULE + ) + + const points = await loyaltyModuleService.getPoints( + req.auth_context.actor_id + ) + + res.json({ + points, + }) +} +``` + +Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as body parameters or authenticated customer details. +2. A response object to manipulate and send the response. + +In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`). + +Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`. + +Finally, you return the loyalty points in the response. + +You'll test out this route as you customize the Next.js Starter Storefront next. + +*** + +## Step 6: Show Loyalty Points During Checkout + +Now that you have the API route to retrieve the loyalty points, you can show them during checkout. + +In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page. + +First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function: + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" +export const getLoyaltyPoints = async () => { + const headers = { + ...(await getAuthHeaders()), + } + + return sdk.client.fetch<{ points: number }>( + `/store/customers/me/loyalty-points`, + { + method: "GET", + headers, + } + ) + .then(({ points }) => points) + .catch(() => null) +} +``` + +You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront. + +If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case. + +Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights} +"use client" + +import { HttpTypes } from "@medusajs/types" +import { useEffect, useMemo, useState } from "react" +import { getLoyaltyPoints } from "../../../../lib/data/customer" +import { Button, Heading } from "@medusajs/ui" +import Link from "next/link" + +type LoyaltyPointsProps = { + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } +} + +const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => { + const isLoyaltyPointsPromoApplied = useMemo(() => { + return cart.promotions.find( + (promo) => promo.id === cart.metadata?.loyalty_promo_id + ) !== undefined + }, [cart]) + const [loyaltyPoints, setLoyaltyPoints] = useState< + number | null + >(null) + + useEffect(() => { + getLoyaltyPoints() + .then((points) => { + console.log(points) + setLoyaltyPoints(points) + }) + }, []) + + const handleTogglePromotion = async ( + e: React.MouseEvent + ) => { + e.preventDefault() + // TODO apply or remove loyalty promotion + } + + return ( + <> +
+
+ + Loyalty Points + + {loyaltyPoints === null && ( + + Sign up to get and use loyalty points + + )} + {loyaltyPoints !== null && ( +
+ + + You have {loyaltyPoints} loyalty points + +
+ )} +
+ + ) +} + +export default LoyaltyPoints +``` + +You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you: + +- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this. +- Create a `loyaltyPoints` state to store the customer's loyalty points. +- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points. +- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later. +- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion. + +Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`: + +```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import LoyaltyPoints from "../../components/loyalty-points" +``` + +Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`: + +```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +This will show the loyalty points component at the end of the checkout summary. + +### Test it Out + +To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running. + +Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary. + +![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png) + +If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next. + +*** + +## Step 7: Apply Loyalty Points to Cart + +The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need: + +- A workflow that implements the steps of the apply loyalty points flow. +- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart. +- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. +- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. +- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points. +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart. +- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. + +Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step. + +### getCartLoyaltyPromoAmountStep + +The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart. + +To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content: + +```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights} +import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import LoyaltyModuleService from "../../modules/loyalty/service" +import { LOYALTY_MODULE } from "../../modules/loyalty" + +export type GetCartLoyaltyPromoAmountStepInput = { + cart: { + id: string + customer: CustomerDTO + promotions?: PromotionDTO[] + total: number + } +} + +export const getCartLoyaltyPromoAmountStep = createStep( + "get-cart-loyalty-promo-amount", + async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => { + // Check if customer has any loyalty points + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + const loyaltyPoints = await loyaltyModuleService.getPoints( + cart.customer.id + ) + + if (loyaltyPoints <= 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Customer has no loyalty points" + ) + } + + const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount( + loyaltyPoints + ) + + const amount = Math.min(pointsAmount, cart.total) + + return new StepResponse(amount) + } +) +``` + +You create a step that accepts an object having the cart's details. + +In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error. + +Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total. + +Finally, you return a `StepResponse` with the amount to be discounted. + +### Create the Workflow + +You can now create the workflow that applies a loyalty promotion to the cart. + +To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports" +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + createPromotionsStep, + updateCartPromotionsWorkflow, + updateCartsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { + validateCustomerExistsStep, + ValidateCustomerExistsStepInput, +} from "./steps/validate-customer-exists" +import { + getCartLoyaltyPromoAmountStep, + GetCartLoyaltyPromoAmountStepInput, +} from "./steps/get-cart-loyalty-promo-amount" +import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo" +import { CreatePromotionDTO } from "@medusajs/framework/types" +import { PromotionActions } from "@medusajs/framework/utils" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" + +type WorkflowInput = { + cart_id: string +} + +const fields = [ + "id", + "customer.*", + "promotions.*", + "promotions.application_method.*", + "promotions.rules.*", + "promotions.rules.values.*", + "currency_code", + "total", + "metadata", +] + +export const applyLoyaltyOnCartWorkflow = createWorkflow( + "apply-loyalty-on-cart", + (input: WorkflowInput) => { + // @ts-ignore + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { + id: input.cart_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateCustomerExistsStep({ + customer: carts[0].customer, + } as ValidateCustomerExistsStepInput) + + getCartLoyaltyPromoStep({ + cart: carts[0] as unknown as CartData, + throwErrorOn: "found", + }) + + const amount = getCartLoyaltyPromoAmountStep({ + cart: carts[0], + } as unknown as GetCartLoyaltyPromoAmountStepInput) + + // TODO create and apply the promotion on the cart + } +) +``` + +You create a workflow that accepts an object with the cart's ID as input. + +So far, you: + +- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. +- Validate that the customer is registered using the `validateCustomerExistsStep`. +- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart. +- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`. + +Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created. + +Replace the `TODO` with the following: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights} +const promoToCreate = transform({ + carts, + amount, +}, (data) => { + const randomStr = Math.random().toString(36).substring(2, 8) + const uniqueId = ( + "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr + ).toUpperCase() + return { + code: uniqueId, + type: "standard", + status: "active", + application_method: { + type: "fixed", + value: data.amount, + target_type: "order", + currency_code: data.carts[0].currency_code, + allocation: "across", + }, + rules: [ + { + attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, + operator: "eq", + values: [data.carts[0].customer!.id], + }, + ], + campaign: { + name: uniqueId, + description: "Loyalty points promotion for " + data.carts[0].customer!.email, + campaign_identifier: uniqueId, + budget: { + type: "usage", + limit: 1, + }, + }, + } +}) + +// TODO create promotion and apply it on cart +``` + +Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters: + +- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted. +- A function that receives the data from the first parameter, and returns the transformed data. + +In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include: + +- You set the discount amount in the application method of the promotion. +- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion. +- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again. + +Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation. + +You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following: + +```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights} +const loyaltyPromo = createPromotionsStep([ + promoToCreate, +] as CreatePromotionDTO[]) + +const { metadata, ...updatePromoData } = transform({ + carts, + promoToCreate, + loyaltyPromo, +}, (data) => { + const promos = [ + ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[], + data.promoToCreate.code, + ] + + return { + cart_id: data.carts[0].id, + promo_codes: promos, + action: PromotionActions.ADD, + metadata: { + loyalty_promo_id: data.loyaltyPromo[0].id, + }, + } +}) + +updateCartPromotionsWorkflow.runAsStep({ + input: updatePromoData, +}) + +updateCartsStep([ + { + id: input.cart_id, + metadata, + }, +]) + +// retrieve cart with updated promotions +// @ts-ignore +const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { id: input.cart_id }, +}).config({ name: "retrieve-cart" }) + +return new WorkflowResponse(updatedCarts[0]) +``` + +In the rest of the workflow, you: + +- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`. +- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata. +- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow. +- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`. +- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. + +To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details. + +### Create the API Route + +Next, you'll create the API route that executes this workflow. + +To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content: + +```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const { id: cart_id } = req.params + + const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope) + .run({ + input: { + cart_id, + }, + }) + + res.json({ cart }) +} +``` + +Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`. + +In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. + +You can now use this API route in the Next.js Starter Storefront. + +### Apply Loyalty Points in the Storefront + +In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button. + +To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront: + +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" +export async function applyLoyaltyPointsOnCart() { + const cartId = await getCartId() + const headers = { + ...(await getAuthHeaders()), + } + + return await sdk.client.fetch<{ + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } + }>(`/store/carts/${cartId}/loyalty-points`, { + method: "POST", + headers, + }) + .then(async (result) => { + const cartCacheTag = await getCacheTag("carts") + revalidateTag(cartCacheTag) + + return result + }) +} +``` + +You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. + +In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. + +Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page. + +Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart" +``` + +Then, replace the `handleTogglePromotion` function with the following: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const handleTogglePromotion = async ( + e: React.MouseEvent +) => { + e.preventDefault() + if (!isLoyaltyPointsPromoApplied) { + await applyLoyaltyPointsOnCart() + } else { + // TODO remove loyalty points + } +} +``` + +In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart. + +You'll implement removing the loyalty points promotion in a later step. + +### Test it Out + +To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront. + +Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount. + +If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`. + +![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png) + +*** + +## Step 8: Remove Loyalty Points From Cart + +In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion. + +To implement this functionality, you'll need to: + +- Create a workflow that removes the loyalty points promotion from the cart. +- Create an API route that executes the workflow. +- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier. +- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +### Create the Workflow + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. +- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. +- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata. +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. + +Since you already have all the steps, you can create the workflow. + +To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content: + +```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights} +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, + updateCartPromotionsWorkflow, + updateCartsStep, + updatePromotionsStep, +} from "@medusajs/medusa/core-flows" +import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" +import { PromotionActions } from "@medusajs/framework/utils" +import { CartData } from "../utils/promo" + +type WorkflowInput = { + cart_id: string +} + +const fields = [ + "id", + "customer.*", + "promotions.*", + "promotions.application_method.*", + "promotions.rules.*", + "promotions.rules.values.*", + "currency_code", + "total", + "metadata", +] + +export const removeLoyaltyFromCartWorkflow = createWorkflow( + "remove-loyalty-from-cart", + (input: WorkflowInput) => { + // @ts-ignore + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { + id: input.cart_id, + }, + }) + + const loyaltyPromo = getCartLoyaltyPromoStep({ + cart: carts[0] as unknown as CartData, + throwErrorOn: "not-found", + }) + + updateCartPromotionsWorkflow.runAsStep({ + input: { + cart_id: input.cart_id, + promo_codes: [loyaltyPromo.code!], + action: PromotionActions.REMOVE, + }, + }) + + const newMetadata = transform({ + carts, + }, (data) => { + const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {} + + return { + ...rest, + loyalty_promo_id: null, + } + }) + + updateCartsStep([ + { + id: input.cart_id, + metadata: newMetadata, + }, + ]) + + updatePromotionsStep([ + { + id: loyaltyPromo.id, + status: "inactive", + }, + ]) + + // retrieve cart with updated promotions + // @ts-ignore + const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + fields, + filters: { id: input.cart_id }, + }).config({ name: "retrieve-cart" }) + + return new WorkflowResponse(updatedCarts[0]) + } +) +``` + +You create a workflow that accepts an object with the cart's ID as input. + +In the workflow, you: + +- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. +- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart. +- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion. +- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata. +- Update the cart's metadata with the new metadata using the `updateCartsStep`. +- Deactivate the loyalty promotion using the `updatePromotionsStep`. +- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. +- Return the cart's details in a `WorkflowResponse` instance. + +### Create the API Route + +Next, you'll create the API route that executes this workflow. + +To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`: + +```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" +// other imports... +import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart" + +// ... +export async function DELETE( + req: MedusaRequest, + res: MedusaResponse +) { + const { id: cart_id } = req.params + + const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope) + .run({ + input: { + cart_id, + }, + }) + + res.json({ cart }) +} +``` + +You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`. + +In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. + +You can now use this API route in the Next.js Starter Storefront. + +### Remove Loyalty Points in the Storefront + +In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied. + +To add the function, add the following to `src/lib/data/cart.ts`: + +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" +export async function removeLoyaltyPointsOnCart() { + const cartId = await getCartId() + const headers = { + ...(await getAuthHeaders()), + } + const next = { + ...(await getCacheOptions("carts")), + } + + return await sdk.client.fetch<{ + cart: HttpTypes.StoreCart & { + promotions: HttpTypes.StorePromotion[] + } + }>(`/store/carts/${cartId}/loyalty-points`, { + method: "DELETE", + headers, + }) + .then(async (result) => { + const cartCacheTag = await getCacheTag("carts") + revalidateTag(cartCacheTag) + + return result + }) +} +``` + +You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. + +In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. + +Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page. + +Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. + +At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart" +``` + +Then, replace the `TODO` in `handleTogglePromotion` with the following: + +```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" +await removeLoyaltyPointsOnCart() +``` + +In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart. + +### Test it Out + +To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront. + +Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount. + +![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png) + +*** + +## Step 9: Validate Loyalty Points on Cart Completion + +After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore. + +So, in this step, you'll hook into Medusa's cart completion flow to perform the validation. + +Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. + +Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed. + +To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content: + +```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { completeCartWorkflow } from "@medusajs/medusa/core-flows" +import LoyaltyModuleService from "../../modules/loyalty/service" +import { LOYALTY_MODULE } from "../../modules/loyalty" +import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" +import { MedusaError } from "@medusajs/framework/utils" + +completeCartWorkflow.hooks.validate( + async ({ cart }, { container }) => { + const query = container.resolve("query") + const loyaltyModuleService: LoyaltyModuleService = container.resolve( + LOYALTY_MODULE + ) + + const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "id", + "promotions.*", + "customer.*", + "promotions.rules.*", + "promotions.rules.values.*", + "promotions.application_method.*", + "metadata", + ], + filters: { + id: cart.id, + }, + }, { + throwIfKeyNotFound: true, + }) + + const loyaltyPromo = getCartLoyaltyPromotion( + carts[0] as unknown as CartData + ) + + if (!loyaltyPromo) { + return + } + + const customerLoyaltyPoints = await loyaltyModuleService.getPoints( + carts[0].customer!.id + ) + const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount( + loyaltyPromo.application_method!.value as number + ) + + if (customerLoyaltyPoints < requiredPoints) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Customer does not have enough loyalty points. Required: ${ + requiredPoints + }, Available: ${customerLoyaltyPoints}` + ) + } + } +) +``` + +Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. + +Since the hook is essentially a step function, it accepts the following parameters: + +- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details. +- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions. + +In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata. + +After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion. + +If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points. + +*** + +## Test Out Cart Completion with Loyalty Points + +Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart. + +When you place the order, if the customer has sufficient loyalty points, the validation hook will pass. + +Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`. + +In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive. + +You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront. + +*** + +## Next Steps + +You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case: + +- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/events-reference/index.html.md) for a full list of available events you can listen to. +- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history. +- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics. + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # Implement Product Reviews in Medusa In this tutorial, you'll learn how to implement product reviews in Medusa. @@ -42596,1797 +44469,6 @@ If you are new to Medusa, check out the [main documentation](https://docs.medusa To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). -# Implement Loyalty Points System in Medusa - -In this tutorial, you'll learn how to implement a loyalty points system in Medusa. - -Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access. - -When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more. - -A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system. - -You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. - -## Summary - -By following this tutorial, you will learn how to: - -- Install and set up Medusa. -- Define models to store loyalty points and the logic to manage them. -- Build flows that allow customers to earn and redeem points during checkout. - - Points are redeemed through dynamic promotions specific to the customer. -- Customize the cart completion flow to validate applied loyalty points. - -![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg) - -- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman. - -*** - -## Step 1: Install a Medusa Application - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) - -Start by installing the Medusa application on your machine with the following command: - -```bash -npx create-medusa-app@latest -``` - -You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes. - -Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. - -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. - -Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. - -*** - -## Step 2: Create Loyalty Module - -In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module. - -In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers. - -Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. - -### Create Module Directory - -Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`. - -### Create Data Models - -A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. - -Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more. - -For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content: - -```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights} -import { model } from "@medusajs/framework/utils" - -const LoyaltyPoint = model.define("loyalty_point", { - id: model.id().primaryKey(), - points: model.number().default(0), - customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), -}) - -export default LoyaltyPoint -``` - -You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter. - -The `LoyaltyPoint` data model has the following properties: - -- `id`: A unique ID for the loyalty points. -- `points`: The number of loyalty points a customer has. -- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table. - -Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md). - -### Create Module's Service - -You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module. - -A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services. - -Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more. - -To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content: - -```ts title="src/modules/loyalty/service.ts" -import { MedusaError, MedusaService } from "@medusajs/framework/utils" -import LoyaltyPoint from "./models/loyalty-point" -import { InferTypeOf } from "@medusajs/framework/types" - -type LoyaltyPoint = InferTypeOf - -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // TODO add methods -} - -export default LoyaltyModuleService -``` - -The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. - -So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`. - -Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). - -#### Add Methods to the Service - -Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points. - -First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - async addPoints(customerId: string, points: number): Promise { - const existingPoints = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - if (existingPoints.length > 0) { - return await this.updateLoyaltyPoints({ - id: existingPoints[0].id, - points: existingPoints[0].points + points, - }) - } - - return await this.createLoyaltyPoints({ - customer_id: customerId, - points, - }) - } -} -``` - -You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add. - -In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method. - -Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method. - -The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async deductPoints(customerId: string, points: number): Promise { - const existingPoints = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - if (existingPoints.length === 0 || existingPoints[0].points < points) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Insufficient loyalty points" - ) - } - - return await this.updateLoyaltyPoints({ - id: existingPoints[0].id, - points: existingPoints[0].points - points, - }) - } -} -``` - -The `deductPoints` method accepts the customer ID and the points to deduct. - -In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error. - -Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`. - -Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async getPoints(customerId: string): Promise { - const points = await this.listLoyaltyPoints({ - customer_id: customerId, - }) - - return points[0]?.points || 0 - } -} -``` - -The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`. - -#### Add Method to Map Points to Discount - -Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout. - -The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation. - -To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount. - -Add the following method to the `LoyaltyModuleService`: - -```ts title="src/modules/loyalty/service.ts" -class LoyaltyModuleService extends MedusaService({ - LoyaltyPoint, -}) { - // ... - async calculatePointsFromAmount(amount: number): Promise { - // Convert amount to points using a standard conversion rate - // For example, $1 = 1 point - // Round down to nearest whole point - const points = Math.floor(amount) - - if (points < 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Amount cannot be negative" - ) - } - - return points - } -} -``` - -The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error. - -You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points. - -### Export Module Definition - -The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. - -So, create the file `src/modules/loyalty/index.ts` with the following content: - -```ts title="src/modules/loyalty/index.ts" -import { Module } from "@medusajs/framework/utils" -import LoyaltyModuleService from "./service" - -export const LOYALTY_MODULE = "loyalty" - -export default Module(LOYALTY_MODULE, { - service: LoyaltyModuleService, -}) -``` - -You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: - -1. The module's name, which is `loyalty`. -2. An object with a required property `service` indicating the module's service. - -You also export the module's name as `LOYALTY_MODULE` so you can reference it later. - -### Add Module to Medusa's Configurations - -Once you finish building the module, add it to Medusa's configurations to start using it. - -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/loyalty", - }, - ], -}) -``` - -Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. - -### Generate Migrations - -Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. - -Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more. - -Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory: - -```bash -npx medusa db:generate loyalty -``` - -The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration. - -Then, to reflect these migrations on the database, run the following command: - -```bash -npx medusa db:migrate -``` - -The table for the `LoyaltyPoint` data model is now created in the database. - -*** - -## Step 3: Change Loyalty Points Flow - -Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points. - -The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points. - -To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint. - -In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed. - -Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. -- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. - -Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps. - -### validateCustomerExistsStep - -In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points. - -To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content: - -```ts title="src/workflows/steps/validate-customer-exists.ts" -import { CustomerDTO } from "@medusajs/framework/types" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { MedusaError } from "@medusajs/framework/utils" - -export type ValidateCustomerExistsStepInput = { - customer: CustomerDTO | null | undefined -} - -export const validateCustomerExistsStep = createStep( - "validate-customer-exists", - async ({ customer }: ValidateCustomerExistsStepInput) => { - if (!customer) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer not found" - ) - } - - if (!customer.has_account) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer must have an account to earn or manage points" - ) - } - } -) -``` - -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's unique name, which is `validate-customer-exists`. -2. An async function that receives two parameters: - - The step's input, which is in this case an object with the customer's details. - - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. - -In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error. - -### getCartLoyaltyPromoStep - -Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points. - -Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations. - -Create the file `src/utils/promo.ts` with the following content: - -```ts title="src/utils/promo.ts" -import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types" - -export type CartData = CartDTO & { - promotions?: PromotionDTO[] - customer?: CustomerDTO - metadata: { - loyalty_promo_id?: string - } -} - -export function getCartLoyaltyPromotion( - cart: CartData -): PromotionDTO | undefined { - if (!cart?.metadata?.loyalty_promo_id) { - return - } - - return cart.promotions?.find( - (promotion) => promotion.id === cart.metadata.loyalty_promo_id - ) -} -``` - -You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property. - -You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content: - -```ts title="src/workflows/steps/get-cart-loyalty-promo.ts" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" -import { MedusaError } from "@medusajs/framework/utils" - -type GetCartLoyaltyPromoStepInput = { - cart: CartData, - throwErrorOn?: "found" | "not-found" -} - -export const getCartLoyaltyPromoStep = createStep( - "get-cart-loyalty-promo", - async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => { - const loyaltyPromo = getCartLoyaltyPromotion(cart) - - if (throwErrorOn === "found" && loyaltyPromo) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Loyalty promotion already applied to cart" - ) - } else if (throwErrorOn === "not-found" && !loyaltyPromo) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "No loyalty promotion found on cart" - ) - } - - return new StepResponse(loyaltyPromo) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `cart`: The cart's details. -- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found. - -The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows. - -In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error. - -Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error. - -To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case. - -### deductPurchasePointsStep - -If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content: - -```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import LoyaltyModuleService from "../../modules/loyalty/service" - -type DeductPurchasePointsInput = { - customer_id: string - amount: number -} - -export const deductPurchasePointsStep = createStep( - "deduct-purchase-points", - async ({ - customer_id, amount, - }: DeductPurchasePointsInput, { container }) => { - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount( - amount - ) - - const result = await loyaltyModuleService.deductPoints( - customer_id, - pointsToDeduct - ) - - return new StepResponse(result, { - customer_id, - points: pointsToDeduct, - }) - }, - async (data, { container }) => { - if (!data) { - return - } - - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - // Restore points in case of failure - await loyaltyModuleService.addPoints( - data.customer_id, - data.points - ) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `customer_id`: The ID of the customer to deduct points from. -- `amount`: The promotion's amount, which will be used to calculate the points to deduct. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount. - -After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points. - -Finally, you return a `StepResponse` with the result of the `deductPoints`. - -#### Compensation Function - -This step has a compensation function, which is passed as a third parameter to the `createStep` function. - -The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems. - -The compensation function accepts two parameters: - -1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance. -2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs. - -### addPurchaseAsPointsStep - -The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout. - -To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content: - -```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import LoyaltyModuleService from "../../modules/loyalty/service" - -type StepInput = { - customer_id: string - amount: number -} - -export const addPurchaseAsPointsStep = createStep( - "add-purchase-as-points", - async (input: StepInput, { container }) => { - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount( - input.amount - ) - - const result = await loyaltyModuleService.addPoints( - input.customer_id, - pointsToAdd - ) - - return new StepResponse(result, { - customer_id: input.customer_id, - points: pointsToAdd, - }) - }, - async (data, { container }) => { - if (!data) { - return - } - - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - await loyaltyModuleService.deductPoints( - data.customer_id, - data.points - ) - } -) -``` - -You create a step that accepts an object having the following properties: - -- `customer_id`: The ID of the customer to add points to. -- `amount`: The order's amount, which will be used to calculate the points to add. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount. - -After that, you call the `addPoints` method to add the points to the customer's loyalty points. - -Finally, you return a `StepResponse` with the result of the `addPoints`. - -You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs. - -### Add Utility Functions - -Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute. - -To add the utility function, add the following to `src/utils/promo.ts`: - -```ts title="src/utils/promo.ts" -import { OrderDTO } from "@medusajs/framework/types" - -export type OrderData = OrderDTO & { - promotion?: PromotionDTO[] - customer?: CustomerDTO - cart?: CartData -} - -export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id" - -export function orderHasLoyaltyPromotion(order: OrderData): boolean { - const loyaltyPromotion = getCartLoyaltyPromotion( - order.cart as unknown as CartData - ) - - return loyaltyPromotion?.rules?.some((rule) => { - return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && ( - rule?.values?.some((value) => value.value === order.customer?.id) || false - ) - }) || false -} -``` - -You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details. - -Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid. - -Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if: - -- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion. -- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID. - - When you create the promotion for the cart later, you'll see how to set this rule. - -You'll use this utility in the workflow next. - -### Create the Workflow - -Now that you have all the steps, you can create the workflow that uses them. - -To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content: - -```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { createWorkflow, when } from "@medusajs/framework/workflows-sdk" -import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists" -import { deductPurchasePointsStep } from "./steps/deduct-purchase-points" -import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points" -import { OrderData, CartData } from "../utils/promo" -import { orderHasLoyaltyPromotion } from "../utils/promo" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" - -type WorkflowInput = { - order_id: string -} - -export const handleOrderPointsWorkflow = createWorkflow( - "handle-order-points", - ({ order_id }: WorkflowInput) => { - // @ts-ignore - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "id", - "customer.*", - "total", - "cart.*", - "cart.promotions.*", - "cart.promotions.rules.*", - "cart.promotions.rules.values.*", - "cart.promotions.application_method.*", - ], - filters: { - id: order_id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - validateCustomerExistsStep({ - customer: orders[0].customer, - } as ValidateCustomerExistsStepInput) - - const loyaltyPointsPromotion = getCartLoyaltyPromoStep({ - cart: orders[0].cart as unknown as CartData, - }) - - when(orders, (orders) => - orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && - loyaltyPointsPromotion !== undefined - ) - .then(() => { - deductPurchasePointsStep({ - customer_id: orders[0].customer!.id, - amount: loyaltyPointsPromotion.application_method!.value as number, - }) - - updatePromotionsStep([ - { - id: loyaltyPointsPromotion.id, - status: "inactive", - }, - ]) - }) - - - when( - orders, - (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData) - ) - .then(() => { - addPurchaseAsPointsStep({ - customer_id: orders[0].customer!.id, - amount: orders[0].total, - }) - }) - } -) -``` - -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. - -It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object with the order's ID. - -In the workflow's constructor function, you: - -- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order. - - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules. -- Validate that the customer is registered using the `validateCustomerExistsStep`. -- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`. -- Use `when` to check whether the order's cart has a loyalty promotion. - - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied. - - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied. - - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method. - - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion. -- You use another `when` to check whether the order's cart doesn't have a loyalty promotion. - - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points. - -You'll use this workflow next when an order is placed. - -To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow. - -*** - -## Step 4: Handle Order Placed Event - -Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed. - -Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event. - -In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow. - -Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more. - -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content: - -```ts title="src/subscribers/order-placed.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { handleOrderPointsWorkflow } from "../workflows/handle-order-points" - -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await handleOrderPointsWorkflow(container).run({ - input: { - order_id: data.id, - }, - }) -} - -export const config: SubscriberConfig = { - event: "order.placed", -} -``` - -The subscriber file must export: - -- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. -- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. - -The subscriber function accepts an object with the following properties: - -- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload. -- `container`: The Medusa container, which you can use to resolve services and tools. - -In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input. - -Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow. - -### Test it Out - -To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory. - -So, run the following command in the Medusa application's directory to start the Medusa server: - -```bash title="Medusa Application" -npm run dev -``` - -Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server: - -```bash title="Next.js Starter Storefront" -npm run dev -``` - -The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`. - -Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right. - -Once you're logged in, add an item to the cart and go through the checkout flow. - -After you place the order, you'll see the following message in your Medusa application's terminal: - -```bash -info: Processing order.placed which has 1 subscribers -``` - -This message indicates that the `order.placed` event was emitted, and that your subscriber was executed. - -Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step. - -*** - -## Step 5: Retrieve Loyalty Points API Route - -Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout. - -To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. - -You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer. - -Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). - -An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. - -So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content: - -```ts title="src/api/store/customers/me/loyalty-points/route.ts" - -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { LOYALTY_MODULE } from "../../../../../modules/loyalty" -import LoyaltyModuleService from "../../../../../modules/loyalty/service" - -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) { - const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve( - LOYALTY_MODULE - ) - - const points = await loyaltyModuleService.getPoints( - req.auth_context.actor_id - ) - - res.json({ - points, - }) -} -``` - -Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`. The route handler function accepts two parameters: - -1. A request object with details and context on the request, such as body parameters or authenticated customer details. -2. A response object to manipulate and send the response. - -In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`). - -Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`. - -Finally, you return the loyalty points in the response. - -You'll test out this route as you customize the Next.js Starter Storefront next. - -*** - -## Step 6: Show Loyalty Points During Checkout - -Now that you have the API route to retrieve the loyalty points, you can show them during checkout. - -In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page. - -First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function: - -```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" -export const getLoyaltyPoints = async () => { - const headers = { - ...(await getAuthHeaders()), - } - - return sdk.client.fetch<{ points: number }>( - `/store/customers/me/loyalty-points`, - { - method: "GET", - headers, - } - ) - .then(({ points }) => points) - .catch(() => null) -} -``` - -You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront. - -If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case. - -Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights} -"use client" - -import { HttpTypes } from "@medusajs/types" -import { useEffect, useMemo, useState } from "react" -import { getLoyaltyPoints } from "../../../../lib/data/customer" -import { Button, Heading } from "@medusajs/ui" -import Link from "next/link" - -type LoyaltyPointsProps = { - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } -} - -const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => { - const isLoyaltyPointsPromoApplied = useMemo(() => { - return cart.promotions.find( - (promo) => promo.id === cart.metadata?.loyalty_promo_id - ) !== undefined - }, [cart]) - const [loyaltyPoints, setLoyaltyPoints] = useState< - number | null - >(null) - - useEffect(() => { - getLoyaltyPoints() - .then((points) => { - console.log(points) - setLoyaltyPoints(points) - }) - }, []) - - const handleTogglePromotion = async ( - e: React.MouseEvent - ) => { - e.preventDefault() - // TODO apply or remove loyalty promotion - } - - return ( - <> -
-
- - Loyalty Points - - {loyaltyPoints === null && ( - - Sign up to get and use loyalty points - - )} - {loyaltyPoints !== null && ( -
- - - You have {loyaltyPoints} loyalty points - -
- )} -
- - ) -} - -export default LoyaltyPoints -``` - -You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you: - -- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this. -- Create a `loyaltyPoints` state to store the customer's loyalty points. -- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points. -- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later. -- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion. - -Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`: - -```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import LoyaltyPoints from "../../components/loyalty-points" -``` - -Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`: - -```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue" - -``` - -This will show the loyalty points component at the end of the checkout summary. - -### Test it Out - -To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running. - -Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary. - -![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png) - -If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next. - -*** - -## Step 7: Apply Loyalty Points to Cart - -The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need: - -- A workflow that implements the steps of the apply loyalty points flow. -- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart. -- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier. - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. -- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. -- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points. -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart. -- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion. -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. - -Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step. - -### getCartLoyaltyPromoAmountStep - -The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart. - -To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content: - -```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights} -import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import LoyaltyModuleService from "../../modules/loyalty/service" -import { LOYALTY_MODULE } from "../../modules/loyalty" - -export type GetCartLoyaltyPromoAmountStepInput = { - cart: { - id: string - customer: CustomerDTO - promotions?: PromotionDTO[] - total: number - } -} - -export const getCartLoyaltyPromoAmountStep = createStep( - "get-cart-loyalty-promo-amount", - async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => { - // Check if customer has any loyalty points - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - const loyaltyPoints = await loyaltyModuleService.getPoints( - cart.customer.id - ) - - if (loyaltyPoints <= 0) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Customer has no loyalty points" - ) - } - - const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount( - loyaltyPoints - ) - - const amount = Math.min(pointsAmount, cart.total) - - return new StepResponse(amount) - } -) -``` - -You create a step that accepts an object having the cart's details. - -In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error. - -Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total. - -Finally, you return a `StepResponse` with the amount to be discounted. - -### Create the Workflow - -You can now create the workflow that applies a loyalty promotion to the cart. - -To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports" -import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - createPromotionsStep, - updateCartPromotionsWorkflow, - updateCartsStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { - validateCustomerExistsStep, - ValidateCustomerExistsStepInput, -} from "./steps/validate-customer-exists" -import { - getCartLoyaltyPromoAmountStep, - GetCartLoyaltyPromoAmountStepInput, -} from "./steps/get-cart-loyalty-promo-amount" -import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo" -import { CreatePromotionDTO } from "@medusajs/framework/types" -import { PromotionActions } from "@medusajs/framework/utils" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" - -type WorkflowInput = { - cart_id: string -} - -const fields = [ - "id", - "customer.*", - "promotions.*", - "promotions.application_method.*", - "promotions.rules.*", - "promotions.rules.values.*", - "currency_code", - "total", - "metadata", -] - -export const applyLoyaltyOnCartWorkflow = createWorkflow( - "apply-loyalty-on-cart", - (input: WorkflowInput) => { - // @ts-ignore - const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { - id: input.cart_id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - validateCustomerExistsStep({ - customer: carts[0].customer, - } as ValidateCustomerExistsStepInput) - - getCartLoyaltyPromoStep({ - cart: carts[0] as unknown as CartData, - throwErrorOn: "found", - }) - - const amount = getCartLoyaltyPromoAmountStep({ - cart: carts[0], - } as unknown as GetCartLoyaltyPromoAmountStepInput) - - // TODO create and apply the promotion on the cart - } -) -``` - -You create a workflow that accepts an object with the cart's ID as input. - -So far, you: - -- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. -- Validate that the customer is registered using the `validateCustomerExistsStep`. -- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart. -- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`. - -Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created. - -Replace the `TODO` with the following: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights} -const promoToCreate = transform({ - carts, - amount, -}, (data) => { - const randomStr = Math.random().toString(36).substring(2, 8) - const uniqueId = ( - "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr - ).toUpperCase() - return { - code: uniqueId, - type: "standard", - status: "active", - application_method: { - type: "fixed", - value: data.amount, - target_type: "order", - currency_code: data.carts[0].currency_code, - allocation: "across", - }, - rules: [ - { - attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE, - operator: "eq", - values: [data.carts[0].customer!.id], - }, - ], - campaign: { - name: uniqueId, - description: "Loyalty points promotion for " + data.carts[0].customer!.email, - campaign_identifier: uniqueId, - budget: { - type: "usage", - limit: 1, - }, - }, - } -}) - -// TODO create promotion and apply it on cart -``` - -Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters: - -- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted. -- A function that receives the data from the first parameter, and returns the transformed data. - -In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include: - -- You set the discount amount in the application method of the promotion. -- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion. -- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again. - -Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation. - -You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following: - -```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights} -const loyaltyPromo = createPromotionsStep([ - promoToCreate, -] as CreatePromotionDTO[]) - -const { metadata, ...updatePromoData } = transform({ - carts, - promoToCreate, - loyaltyPromo, -}, (data) => { - const promos = [ - ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[], - data.promoToCreate.code, - ] - - return { - cart_id: data.carts[0].id, - promo_codes: promos, - action: PromotionActions.ADD, - metadata: { - loyalty_promo_id: data.loyaltyPromo[0].id, - }, - } -}) - -updateCartPromotionsWorkflow.runAsStep({ - input: updatePromoData, -}) - -updateCartsStep([ - { - id: input.cart_id, - metadata, - }, -]) - -// retrieve cart with updated promotions -// @ts-ignore -const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { id: input.cart_id }, -}).config({ name: "retrieve-cart" }) - -return new WorkflowResponse(updatedCarts[0]) -``` - -In the rest of the workflow, you: - -- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`. -- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata. -- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow. -- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`. -- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. - -To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details. - -### Create the API Route - -Next, you'll create the API route that executes this workflow. - -To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content: - -```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const { id: cart_id } = req.params - - const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope) - .run({ - input: { - cart_id, - }, - }) - - res.json({ cart }) -} -``` - -Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`. - -In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. - -You can now use this API route in the Next.js Starter Storefront. - -### Apply Loyalty Points in the Storefront - -In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button. - -To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront: - -```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" -export async function applyLoyaltyPointsOnCart() { - const cartId = await getCartId() - const headers = { - ...(await getAuthHeaders()), - } - - return await sdk.client.fetch<{ - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } - }>(`/store/carts/${cartId}/loyalty-points`, { - method: "POST", - headers, - }) - .then(async (result) => { - const cartCacheTag = await getCacheTag("carts") - revalidateTag(cartCacheTag) - - return result - }) -} -``` - -You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. - -In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. - -Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page. - -Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart" -``` - -Then, replace the `handleTogglePromotion` function with the following: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const handleTogglePromotion = async ( - e: React.MouseEvent -) => { - e.preventDefault() - if (!isLoyaltyPointsPromoApplied) { - await applyLoyaltyPointsOnCart() - } else { - // TODO remove loyalty points - } -} -``` - -In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart. - -You'll implement removing the loyalty points promotion in a later step. - -### Test it Out - -To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront. - -Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount. - -If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`. - -![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png) - -*** - -## Step 8: Remove Loyalty Points From Cart - -In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion. - -To implement this functionality, you'll need to: - -- Create a workflow that removes the loyalty points promotion from the cart. -- Create an API route that executes the workflow. -- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier. -- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -### Create the Workflow - -The workflow will have the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. -- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion. -- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion. -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata. -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again. - -Since you already have all the steps, you can create the workflow. - -To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content: - -```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights} -import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, - updateCartPromotionsWorkflow, - updateCartsStep, - updatePromotionsStep, -} from "@medusajs/medusa/core-flows" -import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo" -import { PromotionActions } from "@medusajs/framework/utils" -import { CartData } from "../utils/promo" - -type WorkflowInput = { - cart_id: string -} - -const fields = [ - "id", - "customer.*", - "promotions.*", - "promotions.application_method.*", - "promotions.rules.*", - "promotions.rules.values.*", - "currency_code", - "total", - "metadata", -] - -export const removeLoyaltyFromCartWorkflow = createWorkflow( - "remove-loyalty-from-cart", - (input: WorkflowInput) => { - // @ts-ignore - const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { - id: input.cart_id, - }, - }) - - const loyaltyPromo = getCartLoyaltyPromoStep({ - cart: carts[0] as unknown as CartData, - throwErrorOn: "not-found", - }) - - updateCartPromotionsWorkflow.runAsStep({ - input: { - cart_id: input.cart_id, - promo_codes: [loyaltyPromo.code!], - action: PromotionActions.REMOVE, - }, - }) - - const newMetadata = transform({ - carts, - }, (data) => { - const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {} - - return { - ...rest, - loyalty_promo_id: null, - } - }) - - updateCartsStep([ - { - id: input.cart_id, - metadata: newMetadata, - }, - ]) - - updatePromotionsStep([ - { - id: loyaltyPromo.id, - status: "inactive", - }, - ]) - - // retrieve cart with updated promotions - // @ts-ignore - const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - fields, - filters: { id: input.cart_id }, - }).config({ name: "retrieve-cart" }) - - return new WorkflowResponse(updatedCarts[0]) - } -) -``` - -You create a workflow that accepts an object with the cart's ID as input. - -In the workflow, you: - -- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart. -- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart. -- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion. -- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata. -- Update the cart's metadata with the new metadata using the `updateCartsStep`. -- Deactivate the loyalty promotion using the `updatePromotionsStep`. -- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion. -- Return the cart's details in a `WorkflowResponse` instance. - -### Create the API Route - -Next, you'll create the API route that executes this workflow. - -To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`: - -```ts title="src/api/store/carts/[id]/loyalty-points/route.ts" -// other imports... -import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart" - -// ... -export async function DELETE( - req: MedusaRequest, - res: MedusaResponse -) { - const { id: cart_id } = req.params - - const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope) - .run({ - input: { - cart_id, - }, - }) - - res.json({ cart }) -} -``` - -You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`. - -In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response. - -You can now use this API route in the Next.js Starter Storefront. - -### Remove Loyalty Points in the Storefront - -In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied. - -To add the function, add the following to `src/lib/data/cart.ts`: - -```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" -export async function removeLoyaltyPointsOnCart() { - const cartId = await getCartId() - const headers = { - ...(await getAuthHeaders()), - } - const next = { - ...(await getCacheOptions("carts")), - } - - return await sdk.client.fetch<{ - cart: HttpTypes.StoreCart & { - promotions: HttpTypes.StorePromotion[] - } - }>(`/store/carts/${cartId}/loyalty-points`, { - method: "DELETE", - headers, - }) - .then(async (result) => { - const cartCacheTag = await getCacheTag("carts") - revalidateTag(cartCacheTag) - - return result - }) -} -``` - -You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier. - -In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront. - -Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page. - -Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier. - -At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart" -``` - -Then, replace the `TODO` in `handleTogglePromotion` with the following: - -```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" -await removeLoyaltyPointsOnCart() -``` - -In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart. - -### Test it Out - -To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront. - -Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount. - -![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png) - -*** - -## Step 9: Validate Loyalty Points on Cart Completion - -After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore. - -So, in this step, you'll hook into Medusa's cart completion flow to perform the validation. - -Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. - -Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed. - -To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content: - -```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { completeCartWorkflow } from "@medusajs/medusa/core-flows" -import LoyaltyModuleService from "../../modules/loyalty/service" -import { LOYALTY_MODULE } from "../../modules/loyalty" -import { CartData, getCartLoyaltyPromotion } from "../../utils/promo" -import { MedusaError } from "@medusajs/framework/utils" - -completeCartWorkflow.hooks.validate( - async ({ cart }, { container }) => { - const query = container.resolve("query") - const loyaltyModuleService: LoyaltyModuleService = container.resolve( - LOYALTY_MODULE - ) - - const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "id", - "promotions.*", - "customer.*", - "promotions.rules.*", - "promotions.rules.values.*", - "promotions.application_method.*", - "metadata", - ], - filters: { - id: cart.id, - }, - }, { - throwIfKeyNotFound: true, - }) - - const loyaltyPromo = getCartLoyaltyPromotion( - carts[0] as unknown as CartData - ) - - if (!loyaltyPromo) { - return - } - - const customerLoyaltyPoints = await loyaltyModuleService.getPoints( - carts[0].customer!.id - ) - const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount( - loyaltyPromo.application_method!.value as number - ) - - if (customerLoyaltyPoints < requiredPoints) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Customer does not have enough loyalty points. Required: ${ - requiredPoints - }, Available: ${customerLoyaltyPoints}` - ) - } - } -) -``` - -Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property. - -Since the hook is essentially a step function, it accepts the following parameters: - -- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details. -- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions. - -In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata. - -After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion. - -If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points. - -*** - -## Test Out Cart Completion with Loyalty Points - -Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart. - -When you place the order, if the customer has sufficient loyalty points, the validation hook will pass. - -Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`. - -In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive. - -You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront. - -*** - -## Next Steps - -You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case: - -- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/events-reference/index.html.md) for a full list of available events you can listen to. -- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history. -- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics. - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # Integrations You can integrate any third-party service to Medusa, including storage services, notification systems, Content-Management Systems (CMS), etc… By integrating third-party services, you build flows and synchronize data around these integrations, making Medusa not only your commerce application, but a middleware layer between your data sources and operations. @@ -46734,1820 +46816,6 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). -# Integrate Medusa with Sanity (CMS) - -In this guide, you'll learn how to integrate Medusa with Sanity. - -When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features. - -Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing. - -This guide will teach you how to: - -- Install and set up Medusa. -- Install and set up Sanity with Medusa's Next.js Starter storefront. -- Sync product data from Medusa to Sanity when a product is created or updated. -- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity. - -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs). - -[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository. - -*** - -## Step 1: Install a Medusa Application - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) - -Start by installing the Medusa application on your machine with the following command: - -```bash -npx create-medusa-app@latest -``` - -You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js storefront, choose `Y` for yes. - -Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js storefront in a directory with the `{project-name}-storefront` name. - -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. - -Afterwards, you can login with the new user and explore the dashboard. The Next.js storefront is also running at `http://localhost:8000`. - -Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. - -*** - -## Step 2: Install Sanity Client SDK - -In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity. - -In your terminal, move to the Medusa application's directory and run the following command: - -```bash npm2yarn -cd project-name # replace with directory name -npm install @sanity/client -``` - -*** - -## Step 3: Create a Sanity Project - -When the Medusa application connects to Sanity, it must connect to a project in Sanity. - -So, before building the integration in Medusa, create a project in Sanity using their website: - -1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login) -2. On your account's dashboard, click the "Create new project" button. - -![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png) - -3. Enter a project name and click "Create Project" - -![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png) - -You'll go back to the project's setting page in a later step. - -*** - -## Step 4: Create Sanity Module - -To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it. - -Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - -### Create Module Directory - -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/sanity`. - -### Create Service - -You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. - -Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods. - -The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity. - -Start by creating the file `src/modules/sanity/service.ts` with the following content: - -```ts title="src/modules/sanity/service.ts" -import { - Logger, -} from "@medusajs/framework/types" -import { - SanityClient, -} from "@sanity/client" - -class SanityModuleService { - private client: SanityClient - private studioUrl?: string - private logger: Logger - - // TODO -} - -export default SanityModuleService -``` - -You create the `SanityModuleService` class that for now only has three properties: - -- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity. -- `studioUrl` property which will hold the URL to access the Sanity studio. -- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. - -In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID. - -So, add after the import at the top of the file the following types: - -```ts title="src/modules/sanity/service.ts" -// other imports... - -const SyncDocumentTypes = { - PRODUCT: "product", -} as const - -type SyncDocumentTypes = - (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes]; - -type ModuleOptions = { - api_token: string; - project_id: string; - api_version: string; - dataset: "production" | "development"; - type_map?: Record; - studio_url?: string; -} -``` - -The `ModuleOptions` type defines the type of options that the module expects: - -- `api_token`: API token to connect to Sanity. -- `project_id`: The ID of the Sanity project. -- `api_version`: The Sanity API version. -- `dataset`: The dataset to use, which is either `production` or `development`. -- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too. -- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard. - -You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`: - -```ts title="src/modules/sanity/service.ts" -import { - // other imports... - createClient, -} from "@sanity/client" - -// types... - -type InjectedDependencies = { - logger: Logger -}; - -class SanityModuleService { - // properties... - constructor({ - logger, - }: InjectedDependencies, options: ModuleOptions) { - this.client = createClient({ - projectId: options.project_id, - apiVersion: options.api_version, - dataset: options.dataset, - token: options.api_token, - }) - this.logger = logger - - this.logger.info("Connected to Sanity") - - this.studioUrl = options.studio_url - - // TODO initialize more properties - } -} -``` - -The service's constructor accepts two parameters: - -1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). -2. The options passed to the module. - -In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives. - -You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful. - -#### Transform Product Data - -When you create or update products in Sanity, you must prepare the product object based on what Sanity expects. - -So, you'll add methods to the service that transform a Medusa product to a Sanity document object. - -Start by adding the following types and class properties to `src/modules/sanity/service.ts`: - -```ts title="src/modules/sanity/service.ts" -type SyncDocumentInputs = T extends "product" - ? ProductDTO - : never - -type TransformationMap = Record< - SyncDocumentTypes, - (data: SyncDocumentInputs) => any ->; - -class SanityModuleService { - // other properties... - private typeMap: Record - private createTransformationMap: TransformationMap - private updateTransformationMap: TransformationMap - - // ... -} -``` - -First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data. - -Then, in the service, you define three new properties: - -- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity. -- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created. -- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation. - -Next, add the following two methods to transform a product: - -```ts title="src/modules/sanity/service.ts" -// other imports... -import { - ProductDTO, -} from "@medusajs/framework/types" - -class SanityModuleService { - // ... - private transformProductForCreate = (product: ProductDTO) => { - return { - _type: this.typeMap[SyncDocumentTypes.PRODUCT], - _id: product.id, - title: product.title, - specs: [ - { - _key: product.id, - _type: "spec", - title: product.title, - lang: "en", - }, - ], - } - } - - private transformProductForUpdate = (product: ProductDTO) => { - return { - set: { - title: product.title, - }, - } - } -} -``` - -The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document. - -The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods. - -Finally, initialize the new properties you added in the `SanityModuleService`'s constructor: - -```ts title="src/modules/sanity/service.ts" -class SanityModuleService { - // ... - constructor({ - logger, - }: InjectedDependencies, options: ModuleOptions) { - // ... - this.typeMap = Object.assign( - {}, - { - [SyncDocumentTypes.PRODUCT]: "product", - }, - options.type_map || {} - ) - - this.createTransformationMap = { - [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate, - } - - this.updateTransformationMap = { - [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate, - } - } - // ... -} -``` - -You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update. - -You can modify these properties to add support for other schema types, such as product categories or collections. - -#### Methods to Manage Documents - -In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity. - -Add the following methods to the `SanityModuleService` class: - -```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights} -// other imports... -import { - // ... - FirstDocumentMutationOptions, -} from "@sanity/client" - -class SanityModuleService { - // ... - async upsertSyncDocument( - type: T, - data: SyncDocumentInputs - ) { - const existing = await this.client.getDocument(data.id) - if (existing) { - return await this.updateSyncDocument(type, data) - } - - return await this.createSyncDocument(type, data) - } - - async createSyncDocument( - type: T, - data: SyncDocumentInputs, - options?: FirstDocumentMutationOptions - ) { - const doc = this.createTransformationMap[type](data) - return await this.client.create(doc, options) - } - - async updateSyncDocument( - type: T, - data: SyncDocumentInputs - ) { - const operations = this.updateTransformationMap[type](data) - return await this.client.patch(data.id, operations).commit() - } -} -``` - -You add three methods: - -- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa. -- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). -- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). - -You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`: - -```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights} -class SanityModuleService { - // ... - async retrieve(id: string) { - return this.client.getDocument(id) - } - - async delete(id: string) { - return this.client.delete(id) - } - - async update(id: string, data: any) { - return await this.client.patch(id, { - set: data, - }).commit() - } - - async list( - filter: { - id: string | string[] - } - ) { - const data = await this.client.getDocuments( - Array.isArray(filter.id) ? filter.id : [filter.id] - ) - - return data.map((doc) => ({ - id: doc?._id, - ...doc, - })) - } -} -``` - -You add other three methods: - -- `retrieve` to retrieve a document by its ID. -- `delete` to delete a document by its ID. -- `update` to update a document by its ID with new data. -- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents. - -### Export Module Definition - -The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity. - -Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service. - -Create the file `src/modules/sanity/index.ts` with the following content: - -```ts title="src/modules/sanity/index.ts" -import { Module } from "@medusajs/framework/utils" -import SanityModuleService from "./service" - -export const SANITY_MODULE = "sanity" - -export default Module(SANITY_MODULE, { - service: SanityModuleService, -}) -``` - -In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container. - -You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service. - -### Add Module to Configurations - -Finally, to register a module in Medusa, you must add it to Medusa's configurations. - -Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application. - -Add the `modules` property to the exported configurations in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/sanity", - options: { - api_token: process.env.SANITY_API_TOKEN, - project_id: process.env.SANITY_PROJECT_ID, - api_version: new Date().toISOString().split("T")[0], - dataset: "production", - studio_url: process.env.SANITY_STUDIO_URL || - "http://localhost:3000/studio", - type_map: { - product: "product", - }, - }, - }, - ], -}) -``` - -In the `modules` array, you pass a module object having the following properties: - -- `resolve`: The path to the module to register in the application. It can also be the name of an NPM package. -- `options`: An object of options to pass to the module. These are the options you expect and use in the module's service. - -Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to `.env`: - -```shell -SANITY_API_TOKEN= -SANITY_PROJECT_ID= -SANITY_STUDIO_URL=http://localhost:8000/studio -``` - -Where: - -- `SANITY_API_TOKEN`: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard: - - Go to the API tab. - -![The API tab is at the top of the project dashboard next to Settings.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) - -- Scroll down to Tokens and click on the "Add API Token" button. - -![The Add API token button is at the top right of the Tokens section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.52_AM_ccgsum.png) - -- Enter a name for the API token, choose "Editor" for the permissions, then click Save. - -![In the Token form, enter the name and choose "Editor" for permisions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png) - -- `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard. - -![The project ID is in the top information of the project.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091988/Medusa%20Resources/Screenshot_2024-11-20_at_10.39.24_AM_cscir8.png) - -- `SANITY_STUDIO_URL`: The URL to access the studio. You'll set the studio up in a later section, but for now set it to `http://localhost:8000/studio`. - -### Test the Module - -To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console. - -To start the Medusa application, run the following command in the application's directory: - -```bash npm2yarn -npm run dev -``` - -If you see the following message among the logs: - -```bash -info: Connected to Sanity -``` - -That means your Sanity credentials were correct, and Medusa was able to connect to Sanity. - -In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data. - -*** - -## Step 5: Link Product and Sanity Modules - -Since a product has a document in Sanity, you want to build an association between the [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document. - -However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). - -A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems. - -In this section, you'll define a link between the Product and Sanity modules. - -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-sanity.ts` with the following content: - -```ts title="src/links/product-sanity.ts" -import { defineLink } from "@medusajs/framework/utils" -import ProductModule from "@medusajs/medusa/product" -import { SANITY_MODULE } from "../modules/sanity" - -defineLink( - { - ...ProductModule.linkable.product.id, - field: "id", - }, - { - linkable: { - serviceName: SANITY_MODULE, - alias: "sanity_product", - primaryKey: "id", - }, - }, - { - readOnly: true, - } -) -``` - -You define a link using `defineLink` from the Modules SDK. It accepts three parameters: - -1. The first data model part of the link. In this case, it's the Product Module's `product` data model. A module has a special `linkable` property that contain link configurations for its data models. -2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a `linkable` object that has the following properties: - - `serviceName`: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name). - - `alias`: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section. - - `primaryKey`: The name of the data model's primary key field. -3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the `readOnly` configuration, which will tell Medusa not to create a table in the database for this link. - -In the next steps, you'll see how this link allows you to retrieve documents when retrieving products. - -*** - -## Step 6: Sync Data to Sanity - -After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need a workflow. - -A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. You'll see how all of this works in the upcoming sections. - -Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route. - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) - -In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually. - -### Create Step - -The syncing workflow will have a single step that syncs products provided as an input to Sanity. - -So, to implement that step, create the file `src/workflows/sanity-sync-products/steps/sync.ts` with the following content: - -```ts title="src/workflows/sanity-sync-products/steps/sync.ts" highlights={syncStepHighlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { ProductDTO } from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, - promiseAll, -} from "@medusajs/framework/utils" -import SanityModuleService from "../../../modules/sanity/service" -import { SANITY_MODULE } from "../../../modules/sanity" - -export type SyncStepInput = { - product_ids?: string[]; -} - -export const syncStep = createStep( - { name: "sync-step", async: true }, - async (input: SyncStepInput, { container }) => { - const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const total = 0 - const upsertMap: { - before: any - after: any - }[] = [] - - const batchSize = 200 - const hasMore = true - const offset = 0 - const filters = input.product_ids ? { - id: input.product_ids, - } : {} - - while (hasMore) { - const { - data: products, - metadata: { count } = {}, - } = await query.graph({ - entity: "product", - fields: [ - "id", - "title", - // @ts-ignore - "sanity_product.*", - ], - filters, - pagination: { - skip: offset, - take: batchSize, - order: { - id: "ASC", - }, - }, - }) - - // TODO sync products - } - } -) -``` - -You define the `syncStep` using the `createStep` function, which accepts two parameters: - -- An object of step configurations. The object must have the `name` property, which is this step's unique name. Enabling the `async` property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing. -- The step's function definition as a second parameter. - -The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources. - -In the step, you resolve from the Medusa container Sanity Module's service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that allows you to retrieve data across modules and links. - -You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve. - -Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retrieve the Sanity document of each product using Sanity Module's `list` method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this: - -```json title="Example Product Object" -{ - "id": "prod_123", - "title": "Shirt", - "sanity_product": { - "id": "prod_123", - "_type": "product", - // other Sanity fields... - } -} -``` - -Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following: - -```ts title="src/workflows/sanity-sync-products/steps/sync.ts" -while (hasMore) { - // ... - try { - await promiseAll( - products.map(async (prod) => { - const after = await sanityModule.upsertSyncDocument( - "product", - prod as ProductDTO - ) - - upsertMap.push({ - // @ts-ignore - before: prod.sanity_product, - after, - }) - - return after - }) - ) - } catch (e) { - return StepResponse.permanentFailure( - `An error occurred while syncing documents: ${e}`, - upsertMap - ) - } - - offset += batchSize - hasMore = offset < count - total += products.length -} -``` - -In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. - -For each product, you upsert it into Sanity, then push its document before and after the update to the `upsertMap`. You'll learn more about its use later. - -You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism. - -Finally, after the `while` loop and at the end of the step, add the following return statement: - -```ts title="src/workflows/sanity-sync-products/steps/sync.ts" -return new StepResponse({ total }, upsertMap) -``` - -If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step. - -#### Add Compensation Function - -`StepResponse` accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully. - -Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md). - -The `syncStep` creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs. - -To define the compensation function, pass a third-parameter to the `createStep` function: - -```ts title="src/workflows/sanity-sync-products/steps/sync.ts" -export const syncStep = createStep( - { name: "sync-step", async: true }, - async (input: SyncStepInput, { container }) => { - // ... - }, - async (upsertMap, { container }) => { - if (!upsertMap) { - return - } - - const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) - - await promiseAll( - upsertMap.map(({ before, after }) => { - if (!before) { - // delete the document - return sanityModule.delete(after._id) - } - - const { _id: id, ...oldData } = before - - return sanityModule.update( - id, - oldData - ) - }) - ) - } -) -``` - -The compensation function accepts the data passed in the step's `StepResponse` second parameter (in this case, `upsertMap`), and an object of options similar to that of the step. - -In the compensation function, you resolve the Sanity Module's service, then loop over the `upsertMap` to delete created documents, or revert existing ones. - -### Create Workflow - -You'll now create the workflow that uses the `syncStep`. This is the workflow that you'll later execute to sync data automatically or manually. - -Workflows are created in a file under the `src/workflows` directory. So, create the file `src/workflows/sanity-sync-products/index.ts` with the following content: - -```ts title="src/workflows/sanity-sync-products/index.ts" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { syncStep } from "./steps/sync" - -export type SanitySyncProductsWorkflowInput = { - product_ids?: string[]; -}; - -export const sanitySyncProductsWorkflow = createWorkflow( - { name: "sanity-sync-products", retentionTime: 10000 }, - function (input: SanitySyncProductsWorkflowInput) { - const result = syncStep(input) - - return new WorkflowResponse(result) - } -) -``` - -You create a workflow using the `createWorkflow` from the Workflows SDK. It accepts an object of options as a first parameter, where the `name` property is required and indicates the workflow's unique name. - -The `retentionTime` property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing. - -`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the `syncStep` to sync the specified products in the input, then return its result. Workflows must return an instance of `WorkflowResponse`. - -A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). - -You'll execute and test this workflow in the next steps. - -*** - -## Step 7: Handle Product Changes in Medusa - -You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated. - -Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber. - -A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application. - -Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created. - -Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). You can also find the list of emitted events in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/events-reference/index.html.md). - -So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the `product.created` and `product.updated` events. - -Subscribers are created under the `src/subscribers` directory. So, create the file `src/subscribers/sanity-product-sync.ts` with the following content: - -```ts title="src/subscribers/sanity-product-sync.ts" highlights={subscriberHighlights} -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/medusa" -import { - sanitySyncProductsWorkflow, -} from "../workflows/sanity-sync-products" - -export default async function upsertSanityProduct({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await sanitySyncProductsWorkflow(container).run({ - input: { - product_ids: [data.id], - }, - }) -} - -export const config: SubscriberConfig = { - event: ["product.created", "product.updated"], -} -``` - -The subscriber function `upsertSanityProduct` accepts an object as a parameter that has the following properties: - -- `event`: An object of the event's details. Its `data` property holds the data payload emitted with the event, which in this case is the ID of the product created or updated. -- `container`: An instance of the Medusa container to resolve resources. - -In the subscriber, you execute the `sanitySyncProductsWorkflow` by invoking it, passing it the container, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run`'s object parameter. - -The subscriber file must also export a configuration object. It has an `event` property, which is the names of the events that the subscriber is listening to. - -### Test it Out - -To test it out, run the Medusa application, then open the Medusa Admin in your browser at `http://localhost:9000/app`. Try creating or updating a product. You'll see the following message in the console: - -```bash -info: Processing product.created which has 1 subscribers -``` - -This means that the `product.created` event was emitted and your subscriber was executed. - -In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio. - -*** - -## Step 8: Setup Sanity with Next.js Starter Storefront - -In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page. - -Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory). - -Then, run the following command: - -```bash badgeLabel="Storefront" badgeColor="blue" -npx sanity@latest init -``` - -You'll then be asked a few questions: - -- For the project, select the Sanity project you created earlier in this guide. -- For dataset, use `production` unless you changed it in the Sanity project. -- Select yes for adding the Sanity configuration files to the Next.js folder. -- Select yes for TypeScript. -- Select yes for Sanity studio, and choose the `/studio` route. -- Select clean project template. -- Select yes for adding the project ID and dataset to `.env.local`. - -Afterwards, the command will install the necessary dependencies for Sanity. - -### Error during installation - -If you run into an error during the installation of peer dependencies, try running the following command to install them: - -```bash -yarn add next-sanity@9.8.15 @sanity/client@^6.22.4 @sanity/icons@^3.4.0 @sanity/types@^3.62.0 @sanity/ui@^2.8.10 next@^15.0.0 react@^19.0.0 react-dom@^19.0.0 sanity@^3.62.0 styled-components@^6.1 -``` - -### Update Middleware - -The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, `/us`). - -Since the Sanity studio runs at `/studio`, the middleware should ignore requests to this path. - -Open the file `src/middleware.ts` and find the following `if` condition: - -```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" -if ( - urlHasCountryCode && - (!isOnboarding || onboardingCookie) && - (!cartId || cartIdCookie) -) { - return NextResponse.next() -} -``` - -Replace it with the following condition: - -```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" -if ( - request.nextUrl.pathname.startsWith("/studio") || - urlHasCountryCode && - (!isOnboarding || onboardingCookie) && - (!cartId || cartIdCookie) -) { - return NextResponse.next() -} -``` - -If the path starts with `/studio`, the middleware will stop executing and the page will open. - -### Set CORS Settings - -Every Sanity project has a configured set of CORS origins allowed, with the default being `http://localhost:3333`. - -The Next.js Starter runs on the `8000` port, so you must add it to the allowed CORS origins. - -In your Sanity project's dashboard: - -1. Click on the API tab. - -![Find the API tab at the top of the dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096643/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) - -2. Scroll down to CORS origins and click the "Add CORS origin" button. - -![Find the CORS origins section and click the Add CORS origin button at its top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096997/Medusa%20Resources/Screenshot_2024-11-20_at_12.02.50_PM_ahsthb.png) - -3. Enter `http://localhost:8000` in the Origin field. -4. Enable the "Allow credentials" checkbox. - -![After filling out the Origin field, click on the Allow credentials checkbox to enable it.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732097074/Medusa%20Resources/Screenshot_2024-11-20_at_12.04.09_PM_nxdvwh.png) - -5. Click the Save button. - -### Open Sanity Studio - -To open the Sanity studio, start the Next.js Starter's development server: - -```bash npm2yarn -npm run dev -``` - -Then, open `http://localhost:8000/studio` in your browser. The Sanity studio will open, but right now it's empty. - -*** - -## Step 9: Add Product Schema Type in Sanity - -In this step, you'll define the `product` schema type in Sanity. You' can then view the documents of that schema in the studio and update their content. - -To create the schema type, create the file `src/sanity/schemaTypes/documents/product.ts` with the following content: - -```ts title="src/sanity/schemaTypes/documents/product.ts" badgeLabel="Storefront" badgeColor="blue" -import { ComposeIcon } from "@sanity/icons" -import { DocumentDefinition } from "sanity" - -const productSchema: DocumentDefinition = { - fields: [ - { - name: "title", - type: "string", - }, - { - group: "content", - name: "specs", - of: [ - { - fields: [ - { name: "lang", title: "Language", type: "string" }, - { name: "title", title: "Title", type: "string" }, - { - name: "content", - rows: 3, - title: "Content", - type: "text", - }, - ], - name: "spec", - type: "object", - }, - ], - type: "array", - }, - { - fields: [ - { name: "title", title: "Title", type: "string" }, - { - name: "products", - of: [{ to: [{ type: "product" }], type: "reference" }], - title: "Addons", - type: "array", - validation: (Rule) => Rule.max(3), - }, - ], - name: "addons", - type: "object", - }, - ], - name: "product", - preview: { - select: { - title: "title", - }, - }, - title: "Product Page", - type: "document", - groups: [{ - default: true, - // @ts-ignore - icon: ComposeIcon, - name: "content", - title: "Content", - }], -} - -export default productSchema -``` - -This creates a schema that has the following fields: - -- `title`: The title of a document, which is in this case the product's type. -- `specs`: An array of product specs. Each object in the array has the following fields: - - `lang`: This is useful if you want to have localized content. - - `title`: The product's title. - - `content`: Textual content, such as the product's description. -- `addons`: An object of products related to this product. - -When you sync the products from Medusa, you only sync the title. You manage the `specs` and `addons` fields within Sanity. - -Next, replace the content of `src/sanity/schemaTypes/index.ts` with the following: - -```ts title="src/sanity/schemaTypes/index.ts" badgeLabel="Storefront" badgeColor="blue" -import { SchemaPluginOptions } from "sanity" -import productSchema from "./documents/product" - -export const schema: SchemaPluginOptions = { - types: [productSchema], - templates: (templates) => templates.filter( - (template) => template.schemaType !== "product" - ), -} -``` - -You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa. - -### Test it Out - -To ensure that your schema is defined correctly and working, start the Next.js storefront's server, and open the Sanity studio again at `http://localhost:8000/studio`. - -You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa. - -If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio. - -If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page. - -*** - -## Step 10: Show Sanity Content in Next.js Starter Storefront - -Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity. - -A product's details are retrieved in the file `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, replace the `ProductPage` function with the following: - -```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={sanityContentHighlights} -// other imports... -import { client } from "../../../../../sanity/lib/client" - -// ... - -export default async function ProductPage(props: Props) { - const params = await props.params - const region = await getRegion(params.countryCode) - - if (!region) { - notFound() - } - - const pricedProduct = await listProducts({ - countryCode: params.countryCode, - queryParams: { handle: params.handle }, - }).then(({ response }) => response.products[0]) - - if (!pricedProduct) { - notFound() - } - - // alternatively, you can filter the content by the language - const sanity = (await client.getDocument(pricedProduct.id))?.specs[0] - - return ( - - ) -} -``` - -You import the Sanity client defined in `src/sanity/lib/client.ts` (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the `ProductTemplate` component. - -This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language. - -Next, you need to customize the `ProductTemplate` to accept the `sanity` prop. In the file `src/modules/products/templates/index.tsx` add the following to `ProductTemplateProps`: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" -type ProductTemplateProps = { - // ... - sanity?: { - content: string - } -} -``` - -Then, add the `sanity` property to the expanded props of the component: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const ProductTemplate: React.FC = ({ - // ... - sanity, -}) => { - // ... -} -``` - -Finally, pass the `sanity` prop to the `ProductInfo` component in the return statement: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" - -``` - -Next, you need to update the `ProductInfo` component to accept and use the `sanity` prop. - -In `src/modules/products/templates/product-info/index.tsx`, update the `ProductInfoProps` to accept the `sanity` prop: - -```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" -type ProductInfoProps = { - // ... - sanity?: { - content: string - } -} -``` - -Then, add the `sanity` property to the expanded props of the component: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const ProductInfo = ({ - // ... - sanity, -}: ProductInfoProps) => { - // ... -} -``` - -Next, find the following line in the return statement: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" -{product.description} -``` - -And replace it with the following: - -```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" -{sanity?.content || product.description} -``` - -Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available. - -### Test it Out - -To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product. - -Then, open the Next.js Starter storefront at `http://localhost:8000` and go to "Store" from the menu, then select the product you edited in Sanity. - -In the product's page, you'll find under the product's name the content you put in Sanity. - -You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems. - -*** - -## Step 11: Customize Admin to Manually Sync Data - -There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration. - -The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into existing pages, or adding new pages, called [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). In these customizations, you can send requests to the Medusa application to perform custom operations. - -In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually. - -![The widget in the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732093722/Medusa%20Resources/Screenshot_2024-11-20_at_11.08.23_AM_wzftfv.png) - -Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data. - -An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). - -### Get Sanity Document API Route - -In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio. - -To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in `src/modules/sanity/service.ts`: - -```ts title="src/modules/sanity/service.ts" -class SanityModuleService { - // ... - async getStudioLink( - type: string, - id: string, - config: { explicit_type?: boolean } = {} - ) { - const resolvedType = config.explicit_type ? type : this.typeMap[type] - if (!this.studioUrl) { - throw new Error("No studio URL provided") - } - return `${this.studioUrl}/structure/${resolvedType};${id}` - } -} -``` - -The method uses the `studioUrl` property, which you set in the `constructor` using the `studio_url` module option, to get the studio link. - -Then, to create the API route, create the file `src/api/admin/sanity/documents/[id]/route.ts` with the following content: - -```ts title="src/api/admin/sanity/documents/[id]/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import SanityModuleService from "src/modules/sanity/service" -import { SANITY_MODULE } from "../../../../../modules/sanity" - -export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const { id } = req.params - - const sanityModule: SanityModuleService = req.scope.resolve( - SANITY_MODULE - ) - const sanityDocument = await sanityModule.retrieve(id) - - const url = sanityDocument ? - await sanityModule.getStudioLink( - sanityDocument._type, - sanityDocument._id, - { explicit_type: true } - ) - : "" - - res.json({ sanity_document: sanityDocument, studio_url: url }) -} -``` - -This defines a `GET` API route at `/admin/sanity/documents/:id`, where `:id` is a dynamic path parameter indicating the ID of a document to retrieve. - -In the `GET` route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document. - -You return in the JSON response an object having the `sanity_document` and `studio_url` properties. - -You'll test out this route in a later section. - -Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). - -### Trigger Sanity Sync API Route - -In this section, you'll add the API route that manually triggers syncing a product to Sanity. - -Since you already have the workflow to sync products, you only need to create an API route that executes it. - -Create the file `src/api/admin/sanity/documents/[id]/sync/route.ts` with the following content: - -```ts title="src/api/admin/sanity/documents/[id]/sync/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - sanitySyncProductsWorkflow, -} from "../../../../../../workflows/sanity-sync-products" - -export const POST = async (req: MedusaRequest, res: MedusaResponse) => { - const { transaction } = await sanitySyncProductsWorkflow(req.scope) - .run({ - input: { product_ids: [req.params.id] }, - }) - - res.json({ transaction_id: transaction.transactionId }) -} -``` - -You add a `POST` API route at `/admin/sanity/documents/:id/sync`, where `:id` is a dynamic path parameter that indicates the ID of a product to sync to Sanity. - -In the `POST` API route handler, you execute the `sanitySyncProductsWorkflow`, passing it the ID of the product from the path parameter as an input. - -In the next section, you'll customize the admin dashboard and send requests to the API route from there. - -### Sanity Product Widget - -In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created. - -To send requests from admin customizations to the Medusa server, you need to use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You'll also use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content: - -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). - -Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier. - -Create the file `src/admin/hooks/sanity.tsx` with the following content: - -```ts title="src/admin/hooks/sanity.tsx" -import { - useMutation, - UseMutationOptions, - useQueryClient, -} from "@tanstack/react-query" -import { sdk } from "../lib/sdk" - -export const useTriggerSanityProductSync = ( - id: string, - options?: UseMutationOptions -) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: () => - sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, { - method: "post", - }), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [`sanity_document`, `sanity_document_${id}`], - }) - - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} -``` - -You define the `useTriggerSanityProductSync` hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity. - -Add in the same file another hook: - -```ts title="src/admin/hooks/sanity.tsx" -// other imports... -import { - // ... - QueryKey, - useQuery, - UseQueryOptions, -} from "@tanstack/react-query" -import { FetchError } from "@medusajs/js-sdk" - -// ... - -export const useSanityDocument = ( - id: string, - query?: Record, - options?: Omit< - UseQueryOptions< - Record, - FetchError, - { sanity_document: Record; studio_url: string }, - QueryKey - >, - "queryKey" | "queryFn" - > -) => { - const fetchSanityProductStatus = async (query?: Record) => { - return await sdk.client.fetch>( - `/admin/sanity/documents/${id}`, - { - query, - } - ) - } - - const { data, ...rest } = useQuery({ - queryFn: async () => fetchSanityProductStatus(query), - queryKey: [`sanity_document_${id}`], - ...options, - }) - - return { ...data, ...rest } -} -``` - -You define the hook `useSanityDocument` which retrieves the Sanity document of a product using Tankstack Query. - -You can now create the widget injected in a product's details page. Widgets are react components created in a file under the `src/admin/widgets` directory. - -So, create the file `src/admin/widgets/sanity-product.tsx` with the following content: - -```tsx title="src/admin/widgets/sanity-product.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { AdminProduct, DetailWidgetProps } from "@medusajs/types" -import { ArrowUpRightOnBox } from "@medusajs/icons" -import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui" -import { useState } from "react" -import { - useSanityDocument, - useTriggerSanityProductSync, -} from "../hooks/sanity" - -const ProductWidget = ({ data }: DetailWidgetProps) => { - const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id) - const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id) - const [showCodeBlock, setShowCodeBlock] = useState(false) - - const handleSync = async () => { - try { - await mutateAsync(undefined) - toast.success(`Sync triggered.`) - } catch (err) { - toast.error(`Couldn't trigger sync: ${ - (err as Record).message - }`) - } - } - - return ( - -
-
-

Sanity Status

-
- {isLoading ? ( - "Loading..." - ) : sanity_document?.title === data.title ? ( - Synced - ) : ( - Not Synced - )} -
-
- -
-
-
- - {studio_url && ( - - - - )} -
- {!isLoading && showCodeBlock && ( - - - - )} -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.after", -}) - -export default ProductWidget -``` - -The file exports a `ProductWidget` component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. - -Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md). - -In the widget, you use the `useSanityDocument` to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data. - -When the "Sync" button is clicked, you use the `useTriggerSanityProductSync` hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async. - -To render a widget that matches the rest of the admin dashboard's design, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md), such as the `CodeBlock` or `Container` components. - -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). - -### Test it Out - -To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page. - -You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity. - -*** - -## Step 12: Add Track Syncs Page to Medusa Admin - -Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the `sanitySyncProductsWorkflow` and their status. You'll also add the ability to sync all products to Sanity from that page. - -![A screenshot of the page to track and trigger syncs.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732095185/Medusa%20Resources/Screenshot_2024-11-20_at_11.09.42_AM_te8xic.png) - -### Retrieve Sync Executions API Route - -Medusa has a [workflow engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md) that manages workflow executions, roll-backs, and other functionalities under the hood. - -The workflow engine is an [Infrastructure Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/infrastructure-modules/index.html.md), which can be replaced with a [Redis Workflow Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md), or a custom one of your choice, allowing you to take ownership of your application's tooling. - -In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress. - -In this section, you'll create an API route to retrieve the stored executions of the `sanitySyncProductsWorkflow` workflow, so that you can display them later on the dashboard. - -When you defined the `sanitySyncProductsWorkflow`, you set its `retentionTime` option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking. - -Create the file `src/api/admin/sanity/syncs/route.ts` with the following content: - -```ts title="src/api/admin/sanity/syncs/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" -import { - sanitySyncProductsWorkflow, -} from "../../../../workflows/sanity-sync-products" - -export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const workflowEngine = req.scope.resolve( - Modules.WORKFLOW_ENGINE - ) - - const [executions, count] = await workflowEngine - .listAndCountWorkflowExecutions( - { - workflow_id: sanitySyncProductsWorkflow.getName(), - }, - { order: { created_at: "DESC" } } - ) - - res.json({ workflow_executions: executions, count }) -} -``` - -You add a `GET` API route at `/admin/sanity/syncs`. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the `listAndCountWorkflowExecutions` method to retrieve the executions of the `sanitySyncProductsWorkflow` workflow, filtering by its name. - -You return the executions in the JSON response of the route. - -### Trigger Sync API Route - -In this section, you'll add another API route that triggers syncing all products to Sanity. - -In the same file `src/api/admin/sanity/syncs/route.ts`, add the following: - -```ts title="api/admin/sanity/syncs/route.ts" -export const POST = async (req: MedusaRequest, res: MedusaResponse) => { - const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({ - input: {}, - }) - - res.json({ transaction_id: transaction.transactionId }) -} -``` - -This adds a `POST` API route at `/admin/sanity/syncs`. In the route handler, you execute the `sanitySyncProductsWorkflow` without passing it a `product_ids` input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity. - -You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a [Get Execution API route](https://docs.medusajs.com/apiadmin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id/index.html.md) that you can use to get the details of a workflow's execution. - -### Add Sanity UI Route - -In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of `sanitySyncProductsWorkflow` executions and allows triggering sync of all products in Medusa. - -A UI route is React component exported in a file under the `src/admin/routes` directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes. - -Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file `src/admin/hooks/sanity.tsx`, add the following two new hooks: - -```tsx title="src/admin/hooks/sanity.tsx" -export const useTriggerSanitySync = (options?: UseMutationOptions) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: () => - sdk.client.fetch(`/admin/sanity/syncs`, { - method: "post", - }), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [`sanity_sync`], - }) - - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} - -export const useSanitySyncs = ( - query?: Record, - options?: Omit< - UseQueryOptions< - Record, - FetchError, - { workflow_executions: Record[] }, - QueryKey - >, - "queryKey" | "queryFn" - > -) => { - const fetchSanitySyncs = async (query?: Record) => { - return await sdk.client.fetch>(`/admin/sanity/syncs`, { - query, - }) - } - - const { data, ...rest } = useQuery({ - queryFn: async () => fetchSanitySyncs(query), - queryKey: [`sanity_sync`], - ...options, - }) - - return { ...data, ...rest } -} -``` - -The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products. - -The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections. - -Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content: - -```tsx title="src/admin/routes/sanity/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Sanity } from "@medusajs/icons" -import { - Badge, - Button, - Container, - Heading, - Table, - Toaster, - toast, -} from "@medusajs/ui" -import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity" - -const SanityRoute = () => { - const { mutateAsync, isPending } = useTriggerSanitySync() - const { workflow_executions, refetch } = useSanitySyncs() - - const handleSync = async () => { - try { - await mutateAsync() - toast.success(`Sync triggered.`) - refetch() - } catch (err) { - toast.error(`Couldn't trigger sync: ${ - (err as Record).message - }`) - } - } - - const getBadgeColor = (state: string) => { - switch (state) { - case "invoking": - return "blue" - case "done": - return "green" - case "failed": - return "red" - default: - return "grey" - } - } - - return ( - <> - -
- - Sanity Syncs - - -
- - - - Sync ID - Status - Created At - Updated At - - - - - {(workflow_executions || []).map((execution) => ( - - (window.location.href = `/app/sanity/${execution.id}`) - } - > - {execution.id} - - - {execution.state} - - - {execution.created_at} - {execution.updated_at} - - ))} - -
-
- - - ) -} - -export const config = defineRouteConfig({ - label: "Sanity", - icon: Sanity, -}) - -export default SanityRoute -``` - -The file's path relative to the `src/admin/routes` directory indicates its path in the admin dashboard. So, this adds a new route at the path `http://localhost:9000/app/sanity`. - -The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with `defineRouteConfig` from the Admin Extension SDK. The function accepts the following properties: - -- `label`: The sidebar item's label. -- `icon`: The icon to the show in the sidebar. - -In the UI route, you use the `useSanitySyncs` hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the `useTriggerSanitySync` hook to send a request to the Medusa application and trigger the sync. - -To display components that match the design of the Medusa Admin, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). - -Learn more about UI routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). - -### Test it Out - -To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar. - -If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table. - -*** - -## Next Steps - -You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features. - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # Integrate Medusa with Resend (Email Notifications) In this guide, you'll learn how to integrate Medusa with Resend. @@ -50538,6 +48806,1820 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# Integrate Medusa with Sanity (CMS) + +In this guide, you'll learn how to integrate Medusa with Sanity. + +When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features. + +Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing. + +This guide will teach you how to: + +- Install and set up Medusa. +- Install and set up Sanity with Medusa's Next.js Starter storefront. +- Sync product data from Medusa to Sanity when a product is created or updated. +- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs). + +[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js storefront, choose `Y` for yes. + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js storefront in a directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. + +Afterwards, you can login with the new user and explore the dashboard. The Next.js storefront is also running at `http://localhost:8000`. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Install Sanity Client SDK + +In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity. + +In your terminal, move to the Medusa application's directory and run the following command: + +```bash npm2yarn +cd project-name # replace with directory name +npm install @sanity/client +``` + +*** + +## Step 3: Create a Sanity Project + +When the Medusa application connects to Sanity, it must connect to a project in Sanity. + +So, before building the integration in Medusa, create a project in Sanity using their website: + +1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login) +2. On your account's dashboard, click the "Create new project" button. + +![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png) + +3. Enter a project name and click "Create Project" + +![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png) + +You'll go back to the project's setting page in a later step. + +*** + +## Step 4: Create Sanity Module + +To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it. + +Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +### Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/sanity`. + +### Create Service + +You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. + +Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods. + +The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity. + +Start by creating the file `src/modules/sanity/service.ts` with the following content: + +```ts title="src/modules/sanity/service.ts" +import { + Logger, +} from "@medusajs/framework/types" +import { + SanityClient, +} from "@sanity/client" + +class SanityModuleService { + private client: SanityClient + private studioUrl?: string + private logger: Logger + + // TODO +} + +export default SanityModuleService +``` + +You create the `SanityModuleService` class that for now only has three properties: + +- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity. +- `studioUrl` property which will hold the URL to access the Sanity studio. +- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. + +In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID. + +So, add after the import at the top of the file the following types: + +```ts title="src/modules/sanity/service.ts" +// other imports... + +const SyncDocumentTypes = { + PRODUCT: "product", +} as const + +type SyncDocumentTypes = + (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes]; + +type ModuleOptions = { + api_token: string; + project_id: string; + api_version: string; + dataset: "production" | "development"; + type_map?: Record; + studio_url?: string; +} +``` + +The `ModuleOptions` type defines the type of options that the module expects: + +- `api_token`: API token to connect to Sanity. +- `project_id`: The ID of the Sanity project. +- `api_version`: The Sanity API version. +- `dataset`: The dataset to use, which is either `production` or `development`. +- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too. +- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard. + +You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`: + +```ts title="src/modules/sanity/service.ts" +import { + // other imports... + createClient, +} from "@sanity/client" + +// types... + +type InjectedDependencies = { + logger: Logger +}; + +class SanityModuleService { + // properties... + constructor({ + logger, + }: InjectedDependencies, options: ModuleOptions) { + this.client = createClient({ + projectId: options.project_id, + apiVersion: options.api_version, + dataset: options.dataset, + token: options.api_token, + }) + this.logger = logger + + this.logger.info("Connected to Sanity") + + this.studioUrl = options.studio_url + + // TODO initialize more properties + } +} +``` + +The service's constructor accepts two parameters: + +1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). +2. The options passed to the module. + +In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives. + +You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful. + +#### Transform Product Data + +When you create or update products in Sanity, you must prepare the product object based on what Sanity expects. + +So, you'll add methods to the service that transform a Medusa product to a Sanity document object. + +Start by adding the following types and class properties to `src/modules/sanity/service.ts`: + +```ts title="src/modules/sanity/service.ts" +type SyncDocumentInputs = T extends "product" + ? ProductDTO + : never + +type TransformationMap = Record< + SyncDocumentTypes, + (data: SyncDocumentInputs) => any +>; + +class SanityModuleService { + // other properties... + private typeMap: Record + private createTransformationMap: TransformationMap + private updateTransformationMap: TransformationMap + + // ... +} +``` + +First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data. + +Then, in the service, you define three new properties: + +- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity. +- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created. +- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation. + +Next, add the following two methods to transform a product: + +```ts title="src/modules/sanity/service.ts" +// other imports... +import { + ProductDTO, +} from "@medusajs/framework/types" + +class SanityModuleService { + // ... + private transformProductForCreate = (product: ProductDTO) => { + return { + _type: this.typeMap[SyncDocumentTypes.PRODUCT], + _id: product.id, + title: product.title, + specs: [ + { + _key: product.id, + _type: "spec", + title: product.title, + lang: "en", + }, + ], + } + } + + private transformProductForUpdate = (product: ProductDTO) => { + return { + set: { + title: product.title, + }, + } + } +} +``` + +The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document. + +The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods. + +Finally, initialize the new properties you added in the `SanityModuleService`'s constructor: + +```ts title="src/modules/sanity/service.ts" +class SanityModuleService { + // ... + constructor({ + logger, + }: InjectedDependencies, options: ModuleOptions) { + // ... + this.typeMap = Object.assign( + {}, + { + [SyncDocumentTypes.PRODUCT]: "product", + }, + options.type_map || {} + ) + + this.createTransformationMap = { + [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate, + } + + this.updateTransformationMap = { + [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate, + } + } + // ... +} +``` + +You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update. + +You can modify these properties to add support for other schema types, such as product categories or collections. + +#### Methods to Manage Documents + +In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity. + +Add the following methods to the `SanityModuleService` class: + +```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights} +// other imports... +import { + // ... + FirstDocumentMutationOptions, +} from "@sanity/client" + +class SanityModuleService { + // ... + async upsertSyncDocument( + type: T, + data: SyncDocumentInputs + ) { + const existing = await this.client.getDocument(data.id) + if (existing) { + return await this.updateSyncDocument(type, data) + } + + return await this.createSyncDocument(type, data) + } + + async createSyncDocument( + type: T, + data: SyncDocumentInputs, + options?: FirstDocumentMutationOptions + ) { + const doc = this.createTransformationMap[type](data) + return await this.client.create(doc, options) + } + + async updateSyncDocument( + type: T, + data: SyncDocumentInputs + ) { + const operations = this.updateTransformationMap[type](data) + return await this.client.patch(data.id, operations).commit() + } +} +``` + +You add three methods: + +- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa. +- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). +- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). + +You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`: + +```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights} +class SanityModuleService { + // ... + async retrieve(id: string) { + return this.client.getDocument(id) + } + + async delete(id: string) { + return this.client.delete(id) + } + + async update(id: string, data: any) { + return await this.client.patch(id, { + set: data, + }).commit() + } + + async list( + filter: { + id: string | string[] + } + ) { + const data = await this.client.getDocuments( + Array.isArray(filter.id) ? filter.id : [filter.id] + ) + + return data.map((doc) => ({ + id: doc?._id, + ...doc, + })) + } +} +``` + +You add other three methods: + +- `retrieve` to retrieve a document by its ID. +- `delete` to delete a document by its ID. +- `update` to update a document by its ID with new data. +- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents. + +### Export Module Definition + +The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity. + +Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service. + +Create the file `src/modules/sanity/index.ts` with the following content: + +```ts title="src/modules/sanity/index.ts" +import { Module } from "@medusajs/framework/utils" +import SanityModuleService from "./service" + +export const SANITY_MODULE = "sanity" + +export default Module(SANITY_MODULE, { + service: SanityModuleService, +}) +``` + +In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container. + +You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service. + +### Add Module to Configurations + +Finally, to register a module in Medusa, you must add it to Medusa's configurations. + +Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application. + +Add the `modules` property to the exported configurations in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/sanity", + options: { + api_token: process.env.SANITY_API_TOKEN, + project_id: process.env.SANITY_PROJECT_ID, + api_version: new Date().toISOString().split("T")[0], + dataset: "production", + studio_url: process.env.SANITY_STUDIO_URL || + "http://localhost:3000/studio", + type_map: { + product: "product", + }, + }, + }, + ], +}) +``` + +In the `modules` array, you pass a module object having the following properties: + +- `resolve`: The path to the module to register in the application. It can also be the name of an NPM package. +- `options`: An object of options to pass to the module. These are the options you expect and use in the module's service. + +Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to `.env`: + +```shell +SANITY_API_TOKEN= +SANITY_PROJECT_ID= +SANITY_STUDIO_URL=http://localhost:8000/studio +``` + +Where: + +- `SANITY_API_TOKEN`: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard: + - Go to the API tab. + +![The API tab is at the top of the project dashboard next to Settings.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) + +- Scroll down to Tokens and click on the "Add API Token" button. + +![The Add API token button is at the top right of the Tokens section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.52_AM_ccgsum.png) + +- Enter a name for the API token, choose "Editor" for the permissions, then click Save. + +![In the Token form, enter the name and choose "Editor" for permisions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png) + +- `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard. + +![The project ID is in the top information of the project.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091988/Medusa%20Resources/Screenshot_2024-11-20_at_10.39.24_AM_cscir8.png) + +- `SANITY_STUDIO_URL`: The URL to access the studio. You'll set the studio up in a later section, but for now set it to `http://localhost:8000/studio`. + +### Test the Module + +To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console. + +To start the Medusa application, run the following command in the application's directory: + +```bash npm2yarn +npm run dev +``` + +If you see the following message among the logs: + +```bash +info: Connected to Sanity +``` + +That means your Sanity credentials were correct, and Medusa was able to connect to Sanity. + +In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data. + +*** + +## Step 5: Link Product and Sanity Modules + +Since a product has a document in Sanity, you want to build an association between the [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document. + +However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). + +A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems. + +In this section, you'll define a link between the Product and Sanity modules. + +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-sanity.ts` with the following content: + +```ts title="src/links/product-sanity.ts" +import { defineLink } from "@medusajs/framework/utils" +import ProductModule from "@medusajs/medusa/product" +import { SANITY_MODULE } from "../modules/sanity" + +defineLink( + { + ...ProductModule.linkable.product.id, + field: "id", + }, + { + linkable: { + serviceName: SANITY_MODULE, + alias: "sanity_product", + primaryKey: "id", + }, + }, + { + readOnly: true, + } +) +``` + +You define a link using `defineLink` from the Modules SDK. It accepts three parameters: + +1. The first data model part of the link. In this case, it's the Product Module's `product` data model. A module has a special `linkable` property that contain link configurations for its data models. +2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a `linkable` object that has the following properties: + - `serviceName`: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name). + - `alias`: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section. + - `primaryKey`: The name of the data model's primary key field. +3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the `readOnly` configuration, which will tell Medusa not to create a table in the database for this link. + +In the next steps, you'll see how this link allows you to retrieve documents when retrieving products. + +*** + +## Step 6: Sync Data to Sanity + +After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need a workflow. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. You'll see how all of this works in the upcoming sections. + +Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route. + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) + +In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually. + +### Create Step + +The syncing workflow will have a single step that syncs products provided as an input to Sanity. + +So, to implement that step, create the file `src/workflows/sanity-sync-products/steps/sync.ts` with the following content: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" highlights={syncStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { ProductDTO } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + promiseAll, +} from "@medusajs/framework/utils" +import SanityModuleService from "../../../modules/sanity/service" +import { SANITY_MODULE } from "../../../modules/sanity" + +export type SyncStepInput = { + product_ids?: string[]; +} + +export const syncStep = createStep( + { name: "sync-step", async: true }, + async (input: SyncStepInput, { container }) => { + const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const total = 0 + const upsertMap: { + before: any + after: any + }[] = [] + + const batchSize = 200 + const hasMore = true + const offset = 0 + const filters = input.product_ids ? { + id: input.product_ids, + } : {} + + while (hasMore) { + const { + data: products, + metadata: { count } = {}, + } = await query.graph({ + entity: "product", + fields: [ + "id", + "title", + // @ts-ignore + "sanity_product.*", + ], + filters, + pagination: { + skip: offset, + take: batchSize, + order: { + id: "ASC", + }, + }, + }) + + // TODO sync products + } + } +) +``` + +You define the `syncStep` using the `createStep` function, which accepts two parameters: + +- An object of step configurations. The object must have the `name` property, which is this step's unique name. Enabling the `async` property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing. +- The step's function definition as a second parameter. + +The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources. + +In the step, you resolve from the Medusa container Sanity Module's service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that allows you to retrieve data across modules and links. + +You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve. + +Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retrieve the Sanity document of each product using Sanity Module's `list` method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this: + +```json title="Example Product Object" +{ + "id": "prod_123", + "title": "Shirt", + "sanity_product": { + "id": "prod_123", + "_type": "product", + // other Sanity fields... + } +} +``` + +Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +while (hasMore) { + // ... + try { + await promiseAll( + products.map(async (prod) => { + const after = await sanityModule.upsertSyncDocument( + "product", + prod as ProductDTO + ) + + upsertMap.push({ + // @ts-ignore + before: prod.sanity_product, + after, + }) + + return after + }) + ) + } catch (e) { + return StepResponse.permanentFailure( + `An error occurred while syncing documents: ${e}`, + upsertMap + ) + } + + offset += batchSize + hasMore = offset < count + total += products.length +} +``` + +In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. + +For each product, you upsert it into Sanity, then push its document before and after the update to the `upsertMap`. You'll learn more about its use later. + +You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism. + +Finally, after the `while` loop and at the end of the step, add the following return statement: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +return new StepResponse({ total }, upsertMap) +``` + +If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step. + +#### Add Compensation Function + +`StepResponse` accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully. + +Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md). + +The `syncStep` creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs. + +To define the compensation function, pass a third-parameter to the `createStep` function: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +export const syncStep = createStep( + { name: "sync-step", async: true }, + async (input: SyncStepInput, { container }) => { + // ... + }, + async (upsertMap, { container }) => { + if (!upsertMap) { + return + } + + const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) + + await promiseAll( + upsertMap.map(({ before, after }) => { + if (!before) { + // delete the document + return sanityModule.delete(after._id) + } + + const { _id: id, ...oldData } = before + + return sanityModule.update( + id, + oldData + ) + }) + ) + } +) +``` + +The compensation function accepts the data passed in the step's `StepResponse` second parameter (in this case, `upsertMap`), and an object of options similar to that of the step. + +In the compensation function, you resolve the Sanity Module's service, then loop over the `upsertMap` to delete created documents, or revert existing ones. + +### Create Workflow + +You'll now create the workflow that uses the `syncStep`. This is the workflow that you'll later execute to sync data automatically or manually. + +Workflows are created in a file under the `src/workflows` directory. So, create the file `src/workflows/sanity-sync-products/index.ts` with the following content: + +```ts title="src/workflows/sanity-sync-products/index.ts" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { syncStep } from "./steps/sync" + +export type SanitySyncProductsWorkflowInput = { + product_ids?: string[]; +}; + +export const sanitySyncProductsWorkflow = createWorkflow( + { name: "sanity-sync-products", retentionTime: 10000 }, + function (input: SanitySyncProductsWorkflowInput) { + const result = syncStep(input) + + return new WorkflowResponse(result) + } +) +``` + +You create a workflow using the `createWorkflow` from the Workflows SDK. It accepts an object of options as a first parameter, where the `name` property is required and indicates the workflow's unique name. + +The `retentionTime` property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing. + +`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the `syncStep` to sync the specified products in the input, then return its result. Workflows must return an instance of `WorkflowResponse`. + +A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). + +You'll execute and test this workflow in the next steps. + +*** + +## Step 7: Handle Product Changes in Medusa + +You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated. + +Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber. + +A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application. + +Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created. + +Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). You can also find the list of emitted events in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/events-reference/index.html.md). + +So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the `product.created` and `product.updated` events. + +Subscribers are created under the `src/subscribers` directory. So, create the file `src/subscribers/sanity-product-sync.ts` with the following content: + +```ts title="src/subscribers/sanity-product-sync.ts" highlights={subscriberHighlights} +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/medusa" +import { + sanitySyncProductsWorkflow, +} from "../workflows/sanity-sync-products" + +export default async function upsertSanityProduct({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sanitySyncProductsWorkflow(container).run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: ["product.created", "product.updated"], +} +``` + +The subscriber function `upsertSanityProduct` accepts an object as a parameter that has the following properties: + +- `event`: An object of the event's details. Its `data` property holds the data payload emitted with the event, which in this case is the ID of the product created or updated. +- `container`: An instance of the Medusa container to resolve resources. + +In the subscriber, you execute the `sanitySyncProductsWorkflow` by invoking it, passing it the container, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run`'s object parameter. + +The subscriber file must also export a configuration object. It has an `event` property, which is the names of the events that the subscriber is listening to. + +### Test it Out + +To test it out, run the Medusa application, then open the Medusa Admin in your browser at `http://localhost:9000/app`. Try creating or updating a product. You'll see the following message in the console: + +```bash +info: Processing product.created which has 1 subscribers +``` + +This means that the `product.created` event was emitted and your subscriber was executed. + +In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio. + +*** + +## Step 8: Setup Sanity with Next.js Starter Storefront + +In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page. + +Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory). + +Then, run the following command: + +```bash badgeLabel="Storefront" badgeColor="blue" +npx sanity@latest init +``` + +You'll then be asked a few questions: + +- For the project, select the Sanity project you created earlier in this guide. +- For dataset, use `production` unless you changed it in the Sanity project. +- Select yes for adding the Sanity configuration files to the Next.js folder. +- Select yes for TypeScript. +- Select yes for Sanity studio, and choose the `/studio` route. +- Select clean project template. +- Select yes for adding the project ID and dataset to `.env.local`. + +Afterwards, the command will install the necessary dependencies for Sanity. + +### Error during installation + +If you run into an error during the installation of peer dependencies, try running the following command to install them: + +```bash +yarn add next-sanity@9.8.15 @sanity/client@^6.22.4 @sanity/icons@^3.4.0 @sanity/types@^3.62.0 @sanity/ui@^2.8.10 next@^15.0.0 react@^19.0.0 react-dom@^19.0.0 sanity@^3.62.0 styled-components@^6.1 +``` + +### Update Middleware + +The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, `/us`). + +Since the Sanity studio runs at `/studio`, the middleware should ignore requests to this path. + +Open the file `src/middleware.ts` and find the following `if` condition: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +if ( + urlHasCountryCode && + (!isOnboarding || onboardingCookie) && + (!cartId || cartIdCookie) +) { + return NextResponse.next() +} +``` + +Replace it with the following condition: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +if ( + request.nextUrl.pathname.startsWith("/studio") || + urlHasCountryCode && + (!isOnboarding || onboardingCookie) && + (!cartId || cartIdCookie) +) { + return NextResponse.next() +} +``` + +If the path starts with `/studio`, the middleware will stop executing and the page will open. + +### Set CORS Settings + +Every Sanity project has a configured set of CORS origins allowed, with the default being `http://localhost:3333`. + +The Next.js Starter runs on the `8000` port, so you must add it to the allowed CORS origins. + +In your Sanity project's dashboard: + +1. Click on the API tab. + +![Find the API tab at the top of the dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096643/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) + +2. Scroll down to CORS origins and click the "Add CORS origin" button. + +![Find the CORS origins section and click the Add CORS origin button at its top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096997/Medusa%20Resources/Screenshot_2024-11-20_at_12.02.50_PM_ahsthb.png) + +3. Enter `http://localhost:8000` in the Origin field. +4. Enable the "Allow credentials" checkbox. + +![After filling out the Origin field, click on the Allow credentials checkbox to enable it.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732097074/Medusa%20Resources/Screenshot_2024-11-20_at_12.04.09_PM_nxdvwh.png) + +5. Click the Save button. + +### Open Sanity Studio + +To open the Sanity studio, start the Next.js Starter's development server: + +```bash npm2yarn +npm run dev +``` + +Then, open `http://localhost:8000/studio` in your browser. The Sanity studio will open, but right now it's empty. + +*** + +## Step 9: Add Product Schema Type in Sanity + +In this step, you'll define the `product` schema type in Sanity. You' can then view the documents of that schema in the studio and update their content. + +To create the schema type, create the file `src/sanity/schemaTypes/documents/product.ts` with the following content: + +```ts title="src/sanity/schemaTypes/documents/product.ts" badgeLabel="Storefront" badgeColor="blue" +import { ComposeIcon } from "@sanity/icons" +import { DocumentDefinition } from "sanity" + +const productSchema: DocumentDefinition = { + fields: [ + { + name: "title", + type: "string", + }, + { + group: "content", + name: "specs", + of: [ + { + fields: [ + { name: "lang", title: "Language", type: "string" }, + { name: "title", title: "Title", type: "string" }, + { + name: "content", + rows: 3, + title: "Content", + type: "text", + }, + ], + name: "spec", + type: "object", + }, + ], + type: "array", + }, + { + fields: [ + { name: "title", title: "Title", type: "string" }, + { + name: "products", + of: [{ to: [{ type: "product" }], type: "reference" }], + title: "Addons", + type: "array", + validation: (Rule) => Rule.max(3), + }, + ], + name: "addons", + type: "object", + }, + ], + name: "product", + preview: { + select: { + title: "title", + }, + }, + title: "Product Page", + type: "document", + groups: [{ + default: true, + // @ts-ignore + icon: ComposeIcon, + name: "content", + title: "Content", + }], +} + +export default productSchema +``` + +This creates a schema that has the following fields: + +- `title`: The title of a document, which is in this case the product's type. +- `specs`: An array of product specs. Each object in the array has the following fields: + - `lang`: This is useful if you want to have localized content. + - `title`: The product's title. + - `content`: Textual content, such as the product's description. +- `addons`: An object of products related to this product. + +When you sync the products from Medusa, you only sync the title. You manage the `specs` and `addons` fields within Sanity. + +Next, replace the content of `src/sanity/schemaTypes/index.ts` with the following: + +```ts title="src/sanity/schemaTypes/index.ts" badgeLabel="Storefront" badgeColor="blue" +import { SchemaPluginOptions } from "sanity" +import productSchema from "./documents/product" + +export const schema: SchemaPluginOptions = { + types: [productSchema], + templates: (templates) => templates.filter( + (template) => template.schemaType !== "product" + ), +} +``` + +You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa. + +### Test it Out + +To ensure that your schema is defined correctly and working, start the Next.js storefront's server, and open the Sanity studio again at `http://localhost:8000/studio`. + +You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa. + +If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio. + +If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page. + +*** + +## Step 10: Show Sanity Content in Next.js Starter Storefront + +Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity. + +A product's details are retrieved in the file `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, replace the `ProductPage` function with the following: + +```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={sanityContentHighlights} +// other imports... +import { client } from "../../../../../sanity/lib/client" + +// ... + +export default async function ProductPage(props: Props) { + const params = await props.params + const region = await getRegion(params.countryCode) + + if (!region) { + notFound() + } + + const pricedProduct = await listProducts({ + countryCode: params.countryCode, + queryParams: { handle: params.handle }, + }).then(({ response }) => response.products[0]) + + if (!pricedProduct) { + notFound() + } + + // alternatively, you can filter the content by the language + const sanity = (await client.getDocument(pricedProduct.id))?.specs[0] + + return ( + + ) +} +``` + +You import the Sanity client defined in `src/sanity/lib/client.ts` (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the `ProductTemplate` component. + +This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language. + +Next, you need to customize the `ProductTemplate` to accept the `sanity` prop. In the file `src/modules/products/templates/index.tsx` add the following to `ProductTemplateProps`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductTemplateProps = { + // ... + sanity?: { + content: string + } +} +``` + +Then, add the `sanity` property to the expanded props of the component: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const ProductTemplate: React.FC = ({ + // ... + sanity, +}) => { + // ... +} +``` + +Finally, pass the `sanity` prop to the `ProductInfo` component in the return statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +Next, you need to update the `ProductInfo` component to accept and use the `sanity` prop. + +In `src/modules/products/templates/product-info/index.tsx`, update the `ProductInfoProps` to accept the `sanity` prop: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductInfoProps = { + // ... + sanity?: { + content: string + } +} +``` + +Then, add the `sanity` property to the expanded props of the component: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const ProductInfo = ({ + // ... + sanity, +}: ProductInfoProps) => { + // ... +} +``` + +Next, find the following line in the return statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +{product.description} +``` + +And replace it with the following: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +{sanity?.content || product.description} +``` + +Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available. + +### Test it Out + +To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product. + +Then, open the Next.js Starter storefront at `http://localhost:8000` and go to "Store" from the menu, then select the product you edited in Sanity. + +In the product's page, you'll find under the product's name the content you put in Sanity. + +You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems. + +*** + +## Step 11: Customize Admin to Manually Sync Data + +There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration. + +The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into existing pages, or adding new pages, called [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). In these customizations, you can send requests to the Medusa application to perform custom operations. + +In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually. + +![The widget in the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732093722/Medusa%20Resources/Screenshot_2024-11-20_at_11.08.23_AM_wzftfv.png) + +Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data. + +An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + +### Get Sanity Document API Route + +In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio. + +To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in `src/modules/sanity/service.ts`: + +```ts title="src/modules/sanity/service.ts" +class SanityModuleService { + // ... + async getStudioLink( + type: string, + id: string, + config: { explicit_type?: boolean } = {} + ) { + const resolvedType = config.explicit_type ? type : this.typeMap[type] + if (!this.studioUrl) { + throw new Error("No studio URL provided") + } + return `${this.studioUrl}/structure/${resolvedType};${id}` + } +} +``` + +The method uses the `studioUrl` property, which you set in the `constructor` using the `studio_url` module option, to get the studio link. + +Then, to create the API route, create the file `src/api/admin/sanity/documents/[id]/route.ts` with the following content: + +```ts title="src/api/admin/sanity/documents/[id]/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import SanityModuleService from "src/modules/sanity/service" +import { SANITY_MODULE } from "../../../../../modules/sanity" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { id } = req.params + + const sanityModule: SanityModuleService = req.scope.resolve( + SANITY_MODULE + ) + const sanityDocument = await sanityModule.retrieve(id) + + const url = sanityDocument ? + await sanityModule.getStudioLink( + sanityDocument._type, + sanityDocument._id, + { explicit_type: true } + ) + : "" + + res.json({ sanity_document: sanityDocument, studio_url: url }) +} +``` + +This defines a `GET` API route at `/admin/sanity/documents/:id`, where `:id` is a dynamic path parameter indicating the ID of a document to retrieve. + +In the `GET` route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document. + +You return in the JSON response an object having the `sanity_document` and `studio_url` properties. + +You'll test out this route in a later section. + +Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). + +### Trigger Sanity Sync API Route + +In this section, you'll add the API route that manually triggers syncing a product to Sanity. + +Since you already have the workflow to sync products, you only need to create an API route that executes it. + +Create the file `src/api/admin/sanity/documents/[id]/sync/route.ts` with the following content: + +```ts title="src/api/admin/sanity/documents/[id]/sync/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + sanitySyncProductsWorkflow, +} from "../../../../../../workflows/sanity-sync-products" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { transaction } = await sanitySyncProductsWorkflow(req.scope) + .run({ + input: { product_ids: [req.params.id] }, + }) + + res.json({ transaction_id: transaction.transactionId }) +} +``` + +You add a `POST` API route at `/admin/sanity/documents/:id/sync`, where `:id` is a dynamic path parameter that indicates the ID of a product to sync to Sanity. + +In the `POST` API route handler, you execute the `sanitySyncProductsWorkflow`, passing it the ID of the product from the path parameter as an input. + +In the next section, you'll customize the admin dashboard and send requests to the API route from there. + +### Sanity Product Widget + +In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created. + +To send requests from admin customizations to the Medusa server, you need to use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You'll also use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content: + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). + +Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier. + +Create the file `src/admin/hooks/sanity.tsx` with the following content: + +```ts title="src/admin/hooks/sanity.tsx" +import { + useMutation, + UseMutationOptions, + useQueryClient, +} from "@tanstack/react-query" +import { sdk } from "../lib/sdk" + +export const useTriggerSanityProductSync = ( + id: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => + sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, { + method: "post", + }), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [`sanity_document`, `sanity_document_${id}`], + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} +``` + +You define the `useTriggerSanityProductSync` hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity. + +Add in the same file another hook: + +```ts title="src/admin/hooks/sanity.tsx" +// other imports... +import { + // ... + QueryKey, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" +import { FetchError } from "@medusajs/js-sdk" + +// ... + +export const useSanityDocument = ( + id: string, + query?: Record, + options?: Omit< + UseQueryOptions< + Record, + FetchError, + { sanity_document: Record; studio_url: string }, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const fetchSanityProductStatus = async (query?: Record) => { + return await sdk.client.fetch>( + `/admin/sanity/documents/${id}`, + { + query, + } + ) + } + + const { data, ...rest } = useQuery({ + queryFn: async () => fetchSanityProductStatus(query), + queryKey: [`sanity_document_${id}`], + ...options, + }) + + return { ...data, ...rest } +} +``` + +You define the hook `useSanityDocument` which retrieves the Sanity document of a product using Tankstack Query. + +You can now create the widget injected in a product's details page. Widgets are react components created in a file under the `src/admin/widgets` directory. + +So, create the file `src/admin/widgets/sanity-product.tsx` with the following content: + +```tsx title="src/admin/widgets/sanity-product.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { AdminProduct, DetailWidgetProps } from "@medusajs/types" +import { ArrowUpRightOnBox } from "@medusajs/icons" +import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui" +import { useState } from "react" +import { + useSanityDocument, + useTriggerSanityProductSync, +} from "../hooks/sanity" + +const ProductWidget = ({ data }: DetailWidgetProps) => { + const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id) + const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id) + const [showCodeBlock, setShowCodeBlock] = useState(false) + + const handleSync = async () => { + try { + await mutateAsync(undefined) + toast.success(`Sync triggered.`) + } catch (err) { + toast.error(`Couldn't trigger sync: ${ + (err as Record).message + }`) + } + } + + return ( + +
+
+

Sanity Status

+
+ {isLoading ? ( + "Loading..." + ) : sanity_document?.title === data.title ? ( + Synced + ) : ( + Not Synced + )} +
+
+ +
+
+
+ + {studio_url && ( + + + + )} +
+ {!isLoading && showCodeBlock && ( + + + + )} +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.after", +}) + +export default ProductWidget +``` + +The file exports a `ProductWidget` component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. + +Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md). + +In the widget, you use the `useSanityDocument` to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data. + +When the "Sync" button is clicked, you use the `useTriggerSanityProductSync` hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async. + +To render a widget that matches the rest of the admin dashboard's design, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md), such as the `CodeBlock` or `Container` components. + +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). + +### Test it Out + +To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page. + +You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity. + +*** + +## Step 12: Add Track Syncs Page to Medusa Admin + +Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the `sanitySyncProductsWorkflow` and their status. You'll also add the ability to sync all products to Sanity from that page. + +![A screenshot of the page to track and trigger syncs.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732095185/Medusa%20Resources/Screenshot_2024-11-20_at_11.09.42_AM_te8xic.png) + +### Retrieve Sync Executions API Route + +Medusa has a [workflow engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md) that manages workflow executions, roll-backs, and other functionalities under the hood. + +The workflow engine is an [Infrastructure Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/infrastructure-modules/index.html.md), which can be replaced with a [Redis Workflow Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md), or a custom one of your choice, allowing you to take ownership of your application's tooling. + +In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress. + +In this section, you'll create an API route to retrieve the stored executions of the `sanitySyncProductsWorkflow` workflow, so that you can display them later on the dashboard. + +When you defined the `sanitySyncProductsWorkflow`, you set its `retentionTime` option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking. + +Create the file `src/api/admin/sanity/syncs/route.ts` with the following content: + +```ts title="src/api/admin/sanity/syncs/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { + sanitySyncProductsWorkflow, +} from "../../../../workflows/sanity-sync-products" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const workflowEngine = req.scope.resolve( + Modules.WORKFLOW_ENGINE + ) + + const [executions, count] = await workflowEngine + .listAndCountWorkflowExecutions( + { + workflow_id: sanitySyncProductsWorkflow.getName(), + }, + { order: { created_at: "DESC" } } + ) + + res.json({ workflow_executions: executions, count }) +} +``` + +You add a `GET` API route at `/admin/sanity/syncs`. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the `listAndCountWorkflowExecutions` method to retrieve the executions of the `sanitySyncProductsWorkflow` workflow, filtering by its name. + +You return the executions in the JSON response of the route. + +### Trigger Sync API Route + +In this section, you'll add another API route that triggers syncing all products to Sanity. + +In the same file `src/api/admin/sanity/syncs/route.ts`, add the following: + +```ts title="api/admin/sanity/syncs/route.ts" +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({ + input: {}, + }) + + res.json({ transaction_id: transaction.transactionId }) +} +``` + +This adds a `POST` API route at `/admin/sanity/syncs`. In the route handler, you execute the `sanitySyncProductsWorkflow` without passing it a `product_ids` input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity. + +You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a [Get Execution API route](https://docs.medusajs.com/apiadmin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id/index.html.md) that you can use to get the details of a workflow's execution. + +### Add Sanity UI Route + +In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of `sanitySyncProductsWorkflow` executions and allows triggering sync of all products in Medusa. + +A UI route is React component exported in a file under the `src/admin/routes` directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes. + +Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file `src/admin/hooks/sanity.tsx`, add the following two new hooks: + +```tsx title="src/admin/hooks/sanity.tsx" +export const useTriggerSanitySync = (options?: UseMutationOptions) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => + sdk.client.fetch(`/admin/sanity/syncs`, { + method: "post", + }), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [`sanity_sync`], + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useSanitySyncs = ( + query?: Record, + options?: Omit< + UseQueryOptions< + Record, + FetchError, + { workflow_executions: Record[] }, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const fetchSanitySyncs = async (query?: Record) => { + return await sdk.client.fetch>(`/admin/sanity/syncs`, { + query, + }) + } + + const { data, ...rest } = useQuery({ + queryFn: async () => fetchSanitySyncs(query), + queryKey: [`sanity_sync`], + ...options, + }) + + return { ...data, ...rest } +} +``` + +The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products. + +The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections. + +Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content: + +```tsx title="src/admin/routes/sanity/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Sanity } from "@medusajs/icons" +import { + Badge, + Button, + Container, + Heading, + Table, + Toaster, + toast, +} from "@medusajs/ui" +import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity" + +const SanityRoute = () => { + const { mutateAsync, isPending } = useTriggerSanitySync() + const { workflow_executions, refetch } = useSanitySyncs() + + const handleSync = async () => { + try { + await mutateAsync() + toast.success(`Sync triggered.`) + refetch() + } catch (err) { + toast.error(`Couldn't trigger sync: ${ + (err as Record).message + }`) + } + } + + const getBadgeColor = (state: string) => { + switch (state) { + case "invoking": + return "blue" + case "done": + return "green" + case "failed": + return "red" + default: + return "grey" + } + } + + return ( + <> + +
+ + Sanity Syncs + + +
+ + + + Sync ID + Status + Created At + Updated At + + + + + {(workflow_executions || []).map((execution) => ( + + (window.location.href = `/app/sanity/${execution.id}`) + } + > + {execution.id} + + + {execution.state} + + + {execution.created_at} + {execution.updated_at} + + ))} + +
+
+ + + ) +} + +export const config = defineRouteConfig({ + label: "Sanity", + icon: Sanity, +}) + +export default SanityRoute +``` + +The file's path relative to the `src/admin/routes` directory indicates its path in the admin dashboard. So, this adds a new route at the path `http://localhost:9000/app/sanity`. + +The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with `defineRouteConfig` from the Admin Extension SDK. The function accepts the following properties: + +- `label`: The sidebar item's label. +- `icon`: The icon to the show in the sidebar. + +In the UI route, you use the `useSanitySyncs` hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the `useTriggerSanitySync` hook to send a request to the Medusa application and trigger the sync. + +To display components that match the design of the Medusa Admin, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). + +Learn more about UI routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). + +### Test it Out + +To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar. + +If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table. + +*** + +## Next Steps + +You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features. + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # How to Build a Wishlist Plugin In this guide, you'll learn how to build a wishlist [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) in Medusa. @@ -52375,325 +52457,325 @@ To learn more about the commerce features that Medusa provides, check out Medusa ## JS SDK Admin -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) +- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) +- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) +- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) - [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) - [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) - [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) - [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) - [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) - [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) - [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) - [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) -- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) -- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) -- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) +- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) - [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) - [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) -- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) -- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md) -- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) -- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) -- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) -- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) -- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) -- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) -- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md) - [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md) - [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md) - [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) -- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md) - [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md) -- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) +- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) -- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) +- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) - [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md) +- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) - [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md) -- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md) - [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md) +- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md) - [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md) -- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) -- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) -- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) +- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) +- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) +- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) +- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) +- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md) +- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) +- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) +- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) +- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) +- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) +- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) - [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) +- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) - [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) - [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) - [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) - [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) - [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) -- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) +- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) - [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) -- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) - [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) +- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) +- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) +- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) +- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) +- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) +- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) +- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) +- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) +- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) - [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) -- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) -- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) -- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) -- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) -- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) -- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) -- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) +- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) +- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) +- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) +- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) +- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) +- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) +- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) +- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) +- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) +- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) - [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) -- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) - [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) -- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) -- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) -- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) - [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) +- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) +- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) - [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) -- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) - [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) +- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) - [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) - [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) +- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) - [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) +- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) - [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) -- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) +- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) - [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) +- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) +- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) +- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) +- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) - [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) ## JS SDK Auth @@ -52702,9 +52784,9 @@ To learn more about the commerce features that Medusa provides, check out Medusa - [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) - [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) - [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) -- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) - [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) +- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) ## JS SDK Store @@ -52713,11 +52795,11 @@ To learn more about the commerce features that Medusa provides, check out Medusa - [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) - [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) - [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) -- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) +- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) - [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) - [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) +- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) # Configure Medusa Backend @@ -53385,6 +53467,85 @@ These layout components allow you to set the layout of your [UI routes](https:// These components allow you to use common Medusa Admin components in your custom UI routes and widgets. +# Two Column Layout - Admin Components + +The Medusa Admin has pages with two columns of content. + +This doesn't include the sidebar, only the main content. + +![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) + +To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: + +```tsx title="src/admin/layouts/two-column.tsx" +export type TwoColumnLayoutProps = { + firstCol: React.ReactNode + secondCol: React.ReactNode +} + +export const TwoColumnLayout = ({ + firstCol, + secondCol, +}: TwoColumnLayoutProps) => { + return ( +
+
+ {firstCol} +
+
+ {secondCol} +
+
+ ) +} +``` + +The `TwoColumnLayout` accepts two props: + +- `firstCol` indicating the content of the first column. +- `secondCol` indicating the content of the second column. + +*** + +## Example + +Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: + +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container } from "../../components/container" +import { Header } from "../../components/header" +import { TwoColumnLayout } from "../../layouts/two-column" + +const CustomPage = () => { + return ( + +
+ + } + secondCol={ + +
+ + } + /> + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. + + # Single Column Layout - Admin Components The Medusa Admin has pages with a single column of content. @@ -53670,777 +53831,6 @@ export default ProductWidget ``` -# Two Column Layout - Admin Components - -The Medusa Admin has pages with two columns of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) - -To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: - -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} - -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( -
-
- {firstCol} -
-
- {secondCol} -
-
- ) -} -``` - -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. - -*** - -## Example - -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: - -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { Header } from "../../components/header" -import { TwoColumnLayout } from "../../layouts/two-column" - -const CustomPage = () => { - return ( - -
- - } - secondCol={ - -
- - } - /> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. - - -# Data Table - Admin Components - -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). - -The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. - -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) - -You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. - -This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. - -## Example: DataTable with Data Fetching - -In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. - -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" -import { - HttpTypes, -} from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] -``` - -`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: - -1. The column's key in the table's data. -2. An object with the following properties: - - `header`: The column's header. - - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. - - `enableSorting`: (optional) A boolean that enables sorting data by this column. - - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. - - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". - - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". - -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. - -To define the filters, add the following: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, -} from "@medusajs/ui" - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] -``` - -`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - -1. The key of a column in the table's data. -2. An object with the following properties: - - `type`: The type of filter. It can be either: - - `select`: A select dropdown allowing users to choose multiple values. - - `radio`: A radio button allowing users to choose one value. - - `date`: A date picker allowing users to choose a date. - - `label`: The filter's label. - - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. - -You'll now start creating the UI widget's component. Start by adding the necessary state variables: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" - -// ... - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - // TODO add data fetching logic -} -``` - -In the component, you've added the following state variables: - -- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: - - `pageSize`: The number of items to show per page. - - `pageIndex`: The current page index. -- `search`: A string that holds the search query. -- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. -- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. - -You've also added two memoized variables: - -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. - -Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" -``` - -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], -}) - -// TODO configure data table -``` - -You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: - -- `limit`: The number of products to fetch per page. -- `offset`: The number of products to skip based on the current page. -- `q`: The search query, if set. -- `status`: The status filters, if set. -- `order`: The sorting order, if set. - -So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. - -Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -``` - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const navigate = useNavigate() - -const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - onRowClick: (event, row) => { - // Handle row click, for example - navigate(`/products/${row.id}`) - }, -}) - -// TODO render component -``` - -The `useDataTable` hook accepts an object with the following properties: - -- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. -- data: (\`array\`) The products fetched from the Medusa application. -- getRowId: (\`function\`) A function that returns the unique ID of a row. -- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. -- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. -- pagination: (\`object\`) An object to configure pagination. - - - state: (\`object\`) The pagination React state variable. - - - onPaginationChange: (\`function\`) A function that updates the pagination state. -- search: (\`object\`) An object to configure searching. - - - state: (\`string\`) The search query React state variable. - - - onSearchChange: (\`function\`) A function that updates the search query state. -- filtering: (\`object\`) An object to configure filtering. - - - state: (\`object\`) The filtering React state variable. - - - onFilteringChange: (\`function\`) A function that updates the filtering state. -- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. -- sorting: (\`object\`) An object to configure sorting. - - - state: (\`object\`) The sorting React state variable. - - - onSortingChange: (\`function\`) A function that updates the sorting state. -- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. - - - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. - - - row: (\`object\`) The data of the row that was clicked. - -Finally, you'll render the data table. But first, add the following imports at the top of the page: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" -``` - -Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -return ( - - - - - Products -
- - - -
-
- - -
-
-
-) -``` - -You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. - -Lastly, export the component and the UI widget's configuration at the end of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" - -// ... - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. - -### Full Example Code - -```tsx title="src/admin/routes/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { - Badge, - createDataTableColumnHelper, - createDataTableFilterHelper, - DataTable, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, - Heading, - useDataTable, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { HttpTypes, ProductStatus } from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], - }) - - const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - }) - - return ( - - - - - Products -
- - - -
-
- - -
-
-
- ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - - -# Header - Admin Components - -Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. - -![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) - -To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: - -```tsx title="src/admin/components/header.tsx" -import { Heading, Button, Text } from "@medusajs/ui" -import React from "react" -import { Link, LinkProps } from "react-router-dom" -import { ActionMenu, ActionMenuProps } from "./action-menu" - -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} - -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( -
-
- {title} - {subtitle && ( - - {subtitle} - - )} -
- {actions.length > 0 && ( -
- {actions.map((action, index) => ( - <> - {action.type === "button" && ( - - )} - {action.type === "action-menu" && ( - - )} - {action.type === "custom" && action.children} - - ))} -
- )} -
- ) -} -``` - -The `Header` component shows a title, and optionally a subtitle and action buttons. - -The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. - -It accepts the following props: - -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. - - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. - - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. - - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. - - \- If its value is \`custom\`, you can pass any React nodes to render. - - - props: (object) - - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. - -*** - -## Example - -Use the `Header` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. - - -# Container - Admin Components - -The Medusa Admin wraps each section of a page in a container. - -![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) - -To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: - -```tsx -import { - Container as UiContainer, - clx, -} from "@medusajs/ui" - -type ContainerProps = React.ComponentProps - -export const Container = (props: ContainerProps) => { - return ( - - ) -} -``` - -The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. - -*** - -## Example - -Use that `Container` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - -
- - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - - # Forms - Admin Components The Medusa Admin has two types of forms: @@ -55014,6 +54404,639 @@ This component uses the [Container](https://docs.medusajs.com/Users/shahednasser It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. +# Header - Admin Components + +Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. + +![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) + +To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: + +```tsx title="src/admin/components/header.tsx" +import { Heading, Button, Text } from "@medusajs/ui" +import React from "react" +import { Link, LinkProps } from "react-router-dom" +import { ActionMenu, ActionMenuProps } from "./action-menu" + +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} + +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { + return ( +
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {actions.length > 0 && ( +
+ {actions.map((action, index) => ( + <> + {action.type === "button" && ( + + )} + {action.type === "action-menu" && ( + + )} + {action.type === "custom" && action.children} + + ))} +
+ )} +
+ ) +} +``` + +The `Header` component shows a title, and optionally a subtitle and action buttons. + +The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. + +It accepts the following props: + +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. + + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. + + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. + + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. + + \- If its value is \`custom\`, you can pass any React nodes to render. + + - props: (object) + + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. + +*** + +## Example + +Use the `Header` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
{ + alert("You clicked the button.") + }, + }, + }, + ]} + /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. + + +# Data Table - Admin Components + +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). + +The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. + +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) + +You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. + +This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. + +## Example: DataTable with Data Fetching + +In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. + +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] +``` + +`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: + +1. The column's key in the table's data. +2. An object with the following properties: + - `header`: The column's header. + - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. + - `enableSorting`: (optional) A boolean that enables sorting data by this column. + - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. + - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". + - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". + +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. + +To define the filters, add the following: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] +``` + +`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: + +1. The key of a column in the table's data. +2. An object with the following properties: + - `type`: The type of filter. It can be either: + - `select`: A select dropdown allowing users to choose multiple values. + - `radio`: A radio button allowing users to choose one value. + - `date`: A date picker allowing users to choose a date. + - `label`: The filter's label. + - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. + +You'll now start creating the UI widget's component. Start by adding the necessary state variables: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" + +// ... + +const limit = 15 + +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + // TODO add data fetching logic +} +``` + +In the component, you've added the following state variables: + +- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: + - `pageSize`: The number of items to show per page. + - `pageIndex`: The current page index. +- `search`: A string that holds the search query. +- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. +- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. + +You've also added two memoized variables: + +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. + +Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" +``` + +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], +}) + +// TODO configure data table +``` + +You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: + +- `limit`: The number of products to fetch per page. +- `offset`: The number of products to skip based on the current page. +- `q`: The search query, if set. +- `status`: The status filters, if set. +- `order`: The sorting order, if set. + +So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. + +Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + useDataTable, +} from "@medusajs/ui" +import { useNavigate } from "react-router-dom" +``` + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const navigate = useNavigate() + +const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + onRowClick: (event, row) => { + // Handle row click, for example + navigate(`/products/${row.id}`) + }, +}) + +// TODO render component +``` + +The `useDataTable` hook accepts an object with the following properties: + +- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. +- data: (\`array\`) The products fetched from the Medusa application. +- getRowId: (\`function\`) A function that returns the unique ID of a row. +- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. +- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. +- pagination: (\`object\`) An object to configure pagination. + + - state: (\`object\`) The pagination React state variable. + + - onPaginationChange: (\`function\`) A function that updates the pagination state. +- search: (\`object\`) An object to configure searching. + + - state: (\`string\`) The search query React state variable. + + - onSearchChange: (\`function\`) A function that updates the search query state. +- filtering: (\`object\`) An object to configure filtering. + + - state: (\`object\`) The filtering React state variable. + + - onFilteringChange: (\`function\`) A function that updates the filtering state. +- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. +- sorting: (\`object\`) An object to configure sorting. + + - state: (\`object\`) The sorting React state variable. + + - onSortingChange: (\`function\`) A function that updates the sorting state. +- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. + + - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. + + - row: (\`object\`) The data of the row that was clicked. + +Finally, you'll render the data table. But first, add the following imports at the top of the page: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + DataTable, +} from "@medusajs/ui" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Container } from "../../components/container" +``` + +Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +return ( + + + + + Products +
+ + + +
+
+ + +
+
+
+) +``` + +You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. + +Lastly, export the component and the UI widget's configuration at the end of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" + +// ... + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. + +### Full Example Code + +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { + Badge, + createDataTableColumnHelper, + createDataTableFilterHelper, + DataTable, + DataTableFilteringState, + DataTablePaginationState, + DataTableSortingState, + Heading, + useDataTable, +} from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { HttpTypes, ProductStatus } from "@medusajs/framework/types" + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] + +const limit = 15 + +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], + }) + + const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + }) + + return ( + + + + + Products +
+ + + +
+
+ + +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + + # JSON View - Admin Components Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. @@ -55334,6 +55357,65 @@ export default ProductWidget This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. +# Container - Admin Components + +The Medusa Admin wraps each section of a page in a container. + +![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) + +To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: + +```tsx +import { + Container as UiContainer, + clx, +} from "@medusajs/ui" + +type ContainerProps = React.ComponentProps + +export const Container = (props: ContainerProps) => { + return ( + + ) +} +``` + +The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. + +*** + +## Example + +Use that `Container` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
+ + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + + # Table - Admin Components If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API. @@ -55729,124 +55811,6 @@ To delete records matching a set of filters, pass an object of filters as a para Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). -# list Method - Service Factory Reference - -This method retrieves a list of records. - -## Retrieve List of Records - -```ts -const posts = await postModuleService.listPosts() -``` - -If no parameters are passed, the method returns an array of the first `15` records. - -*** - -## Filter Records - -```ts -const posts = await postModuleService.listPosts({ - id: ["123", "321"], -}) -``` - -### Parameters - -To retrieve records matching a set of filters, pass an object of the filters as a first parameter. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Retrieve Relations - -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -```ts -const posts = await postModuleService.listPosts({}, { - relations: ["author"], -}) -``` - -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Select Properties - -```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) -``` - -### Parameters - -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Paginate Relations - -```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) -``` - -### Parameters - -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. - -### Returns - -The method returns an array of records. The number of records is less than or equal to `take`'s value. - -*** - -## Sort Records - -```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### Parameters - -To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: - -- `ASC` to sort by this property in the ascending order. -- `DESC` to sort by this property in the descending order. - -### Returns - -The method returns an array of the first `15` records matching the filters. - - # listAndCount Method - Service Factory Reference This method retrieves a list of records with the total count. @@ -55983,6 +55947,124 @@ The method returns an array with two items: 2. The second is the total count of records. +# list Method - Service Factory Reference + +This method retrieves a list of records. + +## Retrieve List of Records + +```ts +const posts = await postModuleService.listPosts() +``` + +If no parameters are passed, the method returns an array of the first `15` records. + +*** + +## Filter Records + +```ts +const posts = await postModuleService.listPosts({ + id: ["123", "321"], +}) +``` + +### Parameters + +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Retrieve Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +```ts +const posts = await postModuleService.listPosts({}, { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Select Properties + +```ts +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. + +`select`'s value is an array of property names to retrieve. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Paginate Relations + +```ts +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, +}) +``` + +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array of records. The number of records is less than or equal to `take`'s value. + +*** + +## Sort Records + +```ts +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### Parameters + +To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: + +- `ASC` to sort by this property in the ascending order. +- `DESC` to sort by this property in the descending order. + +### Returns + +The method returns an array of the first `15` records matching the filters. + + # restore Method - Service Factory Reference This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md). @@ -56214,6 +56296,129 @@ By default, all of the record's properties are retrieved. To select specific one The method returns the record as an object. +# update Method - Service Factory Reference + +This method updates one or more records of the data model. + +## Update One Record + +```ts +const post = await postModuleService.updatePosts({ + id: "123", + name: "My Post", +}) +``` + +### Parameters + +To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. + +You can pass in the same object any other properties to update. + +### Returns + +The method returns the updated record as an object. + +*** + +## Update Multiple Records + +```ts +const posts = await postModuleService.updatePosts([ + { + id: "123", + name: "My Post", + }, + { + id: "321", + published_at: new Date(), + }, +]) +``` + +### Parameters + +To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. + +You can pass in each object any other properties to update. + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Update Records Matching a Filter + +```ts +const posts = await postModuleService.updatePosts({ + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, +}) +``` + +### Parameters + +To update records that match specified filters, pass as a parameter an object having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of every post record whose name is `My Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Multiple Record Updates with Filters + +```ts +const posts = await postModuleService.updatePosts([ + { + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, + }, + { + selector: { + name: "Another Post", + }, + data: { + metadata: { + external_id: "123", + }, + }, + }, +]) +``` + +### Parameters + +To update records matching different sets of filters, pass an array of objects, each having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + + # Filter Records - Service Factory Reference Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters. @@ -56472,129 +56677,6 @@ In the example above, posts are retrieved if they meet either of the following c By combining `and` and `or` conditions, you can create complex filters to retrieve records that meet specific criteria. -# update Method - Service Factory Reference - -This method updates one or more records of the data model. - -## Update One Record - -```ts -const post = await postModuleService.updatePosts({ - id: "123", - name: "My Post", -}) -``` - -### Parameters - -To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. - -You can pass in the same object any other properties to update. - -### Returns - -The method returns the updated record as an object. - -*** - -## Update Multiple Records - -```ts -const posts = await postModuleService.updatePosts([ - { - id: "123", - name: "My Post", - }, - { - id: "321", - published_at: new Date(), - }, -]) -``` - -### Parameters - -To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. - -You can pass in each object any other properties to update. - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Update Records Matching a Filter - -```ts -const posts = await postModuleService.updatePosts({ - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, -}) -``` - -### Parameters - -To update records that match specified filters, pass as a parameter an object having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of every post record whose name is `My Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Multiple Record Updates with Filters - -```ts -const posts = await postModuleService.updatePosts([ - { - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, - }, - { - selector: { - name: "Another Post", - }, - data: { - metadata: { - external_id: "123", - }, - }, - }, -]) -``` - -### Parameters - -To update records matching different sets of filters, pass an array of objects, each having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - -

Just Getting Started?

@@ -57053,125 +57135,6 @@ How to install and setup Medusa UI. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # Alert A component for displaying important messages. @@ -63561,6 +63524,125 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # clx Utility function for working with classNames. diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index f6e2d56ae1..286a1ad377 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -332,7 +332,12 @@ export const sidebars = [ { type: "link", path: "/learn/fundamentals/api-routes/additional-data", - title: "Additional Data", + title: "Pass Additional Data", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/retrieve-custom-links", + title: "Retrieve Custom Links", }, ], }, diff --git a/www/utils/generated/oas-output/operations/admin/delete_admin_customers_[id]_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/admin/delete_admin_customers_[id]_addresses_[address_id].ts index cfdef7c2c0..752df90a8c 100644 --- a/www/utils/generated/oas-output/operations/admin/delete_admin_customers_[id]_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/admin/delete_admin_customers_[id]_addresses_[address_id].ts @@ -19,14 +19,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_customers.ts b/www/utils/generated/oas-output/operations/admin/get_admin_customers.ts index 5071041e62..88c792b326 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_customers.ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_customers.ts @@ -7,14 +7,16 @@ * parameters: * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * - name: offset diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id].ts b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id].ts index 09db246584..053b324e60 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id].ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id].ts @@ -13,14 +13,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses.ts b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses.ts index 24982a2270..1a737bbeee 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses.ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses.ts @@ -13,14 +13,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * - name: offset diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses_[address_id].ts index 317ec2b821..3e19b951a0 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_customers_[id]_addresses_[address_id].ts @@ -19,14 +19,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_product-categories.ts b/www/utils/generated/oas-output/operations/admin/get_admin_product-categories.ts index 258579144c..33e09eb2e4 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_product-categories.ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_product-categories.ts @@ -7,14 +7,16 @@ * parameters: * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * - name: offset diff --git a/www/utils/generated/oas-output/operations/admin/get_admin_product-categories_[id].ts b/www/utils/generated/oas-output/operations/admin/get_admin_product-categories_[id].ts index ab9822997b..4d938d43c1 100644 --- a/www/utils/generated/oas-output/operations/admin/get_admin_product-categories_[id].ts +++ b/www/utils/generated/oas-output/operations/admin/get_admin_product-categories_[id].ts @@ -13,14 +13,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * - name: include_ancestors_tree diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_customers.ts b/www/utils/generated/oas-output/operations/admin/post_admin_customers.ts index 46d5b63ca0..127bee8943 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_customers.ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_customers.ts @@ -7,14 +7,16 @@ * parameters: * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id].ts b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id].ts index 0ac13be923..cb2fad6b40 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id].ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id].ts @@ -13,14 +13,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses.ts b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses.ts index 04456ad82a..f41f1a6319 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses.ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses.ts @@ -14,14 +14,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses_[address_id].ts index 8ae2fe3385..7f4ce2901e 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_addresses_[address_id].ts @@ -20,14 +20,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_customer-groups.ts b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_customer-groups.ts index 55c9f88d09..8e7bfe5d91 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_customer-groups.ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_customers_[id]_customer-groups.ts @@ -15,15 +15,17 @@ * - name: fields * in: query * description: |- - * "Comma-separated fields that should be included in the returned data. + * Comma-separated fields that should be included in the returned data. * if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default fields. - * without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. If a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. Without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. If a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. Without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories.ts b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories.ts index 0a839fbc58..5d001ece05 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories.ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories.ts @@ -7,14 +7,16 @@ * parameters: * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id].ts b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id].ts index f60df8c6c3..8a9335f0b9 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id].ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id].ts @@ -13,14 +13,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id]_products.ts b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id]_products.ts index 7a6f1b2c77..a0d69fe3f6 100644 --- a/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id]_products.ts +++ b/www/utils/generated/oas-output/operations/admin/post_admin_product-categories_[id]_products.ts @@ -14,14 +14,16 @@ * type: string * - name: fields * in: query - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields - * description: "Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. NOTE: This route doesn't allow expanding custom relations." + * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/delete_store_customers_me_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/store/delete_store_customers_me_addresses_[address_id].ts index 11e6ffde7f..634d8c234c 100644 --- a/www/utils/generated/oas-output/operations/store/delete_store_customers_me_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/store/delete_store_customers_me_addresses_[address_id].ts @@ -26,13 +26,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/get_store_customers_me.ts b/www/utils/generated/oas-output/operations/store/get_store_customers_me.ts index 7c26d7ae86..a04bc6ccf4 100644 --- a/www/utils/generated/oas-output/operations/store/get_store_customers_me.ts +++ b/www/utils/generated/oas-output/operations/store/get_store_customers_me.ts @@ -20,13 +20,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses.ts b/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses.ts index 1cf70571a9..10cda02f97 100644 --- a/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses.ts +++ b/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses.ts @@ -20,13 +20,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * - name: offset diff --git a/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses_[address_id].ts index 4b7ccab367..a2ec26c7bf 100644 --- a/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/store/get_store_customers_me_addresses_[address_id].ts @@ -23,13 +23,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/post_store_customers.ts b/www/utils/generated/oas-output/operations/store/post_store_customers.ts index 44449f545c..e918c2793a 100644 --- a/www/utils/generated/oas-output/operations/store/post_store_customers.ts +++ b/www/utils/generated/oas-output/operations/store/post_store_customers.ts @@ -25,7 +25,8 @@ * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * requestBody: diff --git a/www/utils/generated/oas-output/operations/store/post_store_customers_me.ts b/www/utils/generated/oas-output/operations/store/post_store_customers_me.ts index 3888a1876d..ab7df7ce21 100644 --- a/www/utils/generated/oas-output/operations/store/post_store_customers_me.ts +++ b/www/utils/generated/oas-output/operations/store/post_store_customers_me.ts @@ -25,7 +25,8 @@ * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses.ts b/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses.ts index 7a8073ae13..97f89dd94d 100644 --- a/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses.ts +++ b/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses.ts @@ -20,13 +20,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: diff --git a/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses_[address_id].ts b/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses_[address_id].ts index 1257bd0c54..99c02f9270 100644 --- a/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses_[address_id].ts +++ b/www/utils/generated/oas-output/operations/store/post_store_customers_me_addresses_[address_id].ts @@ -26,13 +26,15 @@ * - name: fields * in: query * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * required: false * schema: * type: string * title: fields * description: Comma-separated fields that should be included in the returned data. if a field is prefixed with `+` it will be added to the default fields, using `-` will remove it from the default - * fields. without prefix it will replace the entire default fields. + * fields. without prefix it will replace the entire default fields. This API route restricts the fields that can be selected. Learn how to override the retrievable fields in + * the [Retrieve Custom Links](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links) documentation. * externalDocs: * url: "#select-fields-and-relations" * security: