diff --git a/.changeset/cool-nails-provide.md b/.changeset/cool-nails-provide.md new file mode 100644 index 0000000000000..198cbe6496e16 --- /dev/null +++ b/.changeset/cool-nails-provide.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): guest email change to another guest email diff --git a/.changeset/dirty-boats-add.md b/.changeset/dirty-boats-add.md new file mode 100644 index 0000000000000..74bdf3fe651a6 --- /dev/null +++ b/.changeset/dirty-boats-add.md @@ -0,0 +1,5 @@ +--- +"@medusajs/pricing": minor +--- + +feat(pricing): Calculate prices with multiple rule values diff --git a/.changeset/khaki-buses-look.md b/.changeset/khaki-buses-look.md new file mode 100644 index 0000000000000..d88b627e2893f --- /dev/null +++ b/.changeset/khaki-buses-look.md @@ -0,0 +1,5 @@ +--- +"@medusajs/promotion": patch +--- + +chore(promotion): in operator work as In insted of equal logic diff --git a/.changeset/light-gorillas-play.md b/.changeset/light-gorillas-play.md new file mode 100644 index 0000000000000..b7f7c19cc0fb7 --- /dev/null +++ b/.changeset/light-gorillas-play.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): refresh payment collection using raw total diff --git a/.changeset/moody-eggs-appear.md b/.changeset/moody-eggs-appear.md new file mode 100644 index 0000000000000..9a7e5132223e8 --- /dev/null +++ b/.changeset/moody-eggs-appear.md @@ -0,0 +1,6 @@ +--- +"integration-tests-http": patch +"@medusajs/utils": patch +--- + +Fix on precision for high quantities for items when promotion is applied diff --git a/.changeset/ninety-kangaroos-grin.md b/.changeset/ninety-kangaroos-grin.md new file mode 100644 index 0000000000000..3e87bedb2722e --- /dev/null +++ b/.changeset/ninety-kangaroos-grin.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": minor +--- + +chore(code-flows): throw error on invalid promo code diff --git a/.changeset/odd-apricots-marry.md b/.changeset/odd-apricots-marry.md deleted file mode 100644 index abf60e3a6ac97..0000000000000 --- a/.changeset/odd-apricots-marry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@medusajs/link-modules": patch -"@medusajs/core-flows": patch ---- - -fix(link-modules,core-flows): Carry over cart promotions to order promotions diff --git a/.changeset/pink-baboons-wash.md b/.changeset/pink-baboons-wash.md new file mode 100644 index 0000000000000..1d67271e3df79 --- /dev/null +++ b/.changeset/pink-baboons-wash.md @@ -0,0 +1,11 @@ +--- +"@medusajs/promotion": patch +"@medusajs/core-flows": patch +"integration-tests-http": patch +"@medusajs/cart": patch +"@medusajs/types": patch +"@medusajs/utils": patch +"@medusajs/medusa": patch +--- + +This fixes the discount\_ calculation logic and promotion tax inclusiveness calculation diff --git a/.changeset/rich-starfishes-decide.md b/.changeset/rich-starfishes-decide.md new file mode 100644 index 0000000000000..388828019aa66 --- /dev/null +++ b/.changeset/rich-starfishes-decide.md @@ -0,0 +1,5 @@ +--- +"@medusajs/promotion": patch +--- + +fix(promotion): check currency when computing actions for promotions diff --git a/.changeset/serious-seahorses-switch.md b/.changeset/serious-seahorses-switch.md new file mode 100644 index 0000000000000..134bec588f8d4 --- /dev/null +++ b/.changeset/serious-seahorses-switch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): createCustomerGroupsStep rollback delete created customer groups. diff --git a/.changeset/sour-horses-beam.md b/.changeset/sour-horses-beam.md new file mode 100644 index 0000000000000..4d5ebc7978342 --- /dev/null +++ b/.changeset/sour-horses-beam.md @@ -0,0 +1,7 @@ +--- +"@medusajs/promotion": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Moved calculation logic from total to original_total to ensure consistent base values diff --git a/.changeset/tiny-carrots-wave.md b/.changeset/tiny-carrots-wave.md new file mode 100644 index 0000000000000..c03de9fc69a1b --- /dev/null +++ b/.changeset/tiny-carrots-wave.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +fix(dashboard): correct overflow in a few settings edit forms diff --git a/.changeset/tiny-icons-punch.md b/.changeset/tiny-icons-punch.md new file mode 100644 index 0000000000000..a1f2bcb2bf0a1 --- /dev/null +++ b/.changeset/tiny-icons-punch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +chore(dashboard): add missing US state diff --git a/.changeset/young-fishes-grab.md b/.changeset/young-fishes-grab.md new file mode 100644 index 0000000000000..b317f8ef5ba25 --- /dev/null +++ b/.changeset/young-fishes-grab.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +fix: medusa-service generated functions always return array of data even when only single object is passed diff --git a/.github/teams.yml b/.github/teams.yml index 3324ebde8e25e..ae9d6353facd7 100644 --- a/.github/teams.yml +++ b/.github/teams.yml @@ -12,3 +12,4 @@ - "@christiananese" - "@peterlgh7" - "@juanzgc" + - "@willbouch" diff --git a/.github/workflows/codegen-test.yml b/.github/workflows/codegen-test.yml deleted file mode 100644 index f03bea0e5dbf2..0000000000000 --- a/.github/workflows/codegen-test.yml +++ /dev/null @@ -1,40 +0,0 @@ -#name: OAS Codegen Build Check - BETA -#on: -# pull_request: -# paths-ignore: -# - "docs/**" -# - "www/**" -# -#jobs: -# codegen-test: -# runs-on: ubuntu-latest -# env: -# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} -# TURBO_TEAM: ${{ secrets.TURBO_TEAM }} -# steps: -# - name: Cancel Previous Runs -# uses: styfle/cancel-workflow-action@0.11.0 -# with: -# access_token: ${{ github.token }} -# -# - name: Checkout -# uses: actions/checkout@v3 -# with: -# fetch-depth: 0 -# -# - name: Setup Node.js environment -# uses: actions/setup-node@v4 -# with: -# node-version: "16.10.0" -# cache: "yarn" -# -# - name: Install dependencies -# uses: ./.github/actions/cache-deps -# with: -# extension: codegen -# -# - name: Build Packages - Force -# run: yarn build --force -# -# - name: Assert latest codegen build committed -# run: ./scripts/assert-codegen-build-committed-actions.sh diff --git a/CODEOWNERS b/CODEOWNERS index e25c2437dccca..78ebaf4c8d4bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,10 +1,10 @@ # All files not owned by other teams must be reviewed by the core team * @medusajs/core -package.json @medusajs/engineering -yarn.lock @medusajs/engineering -/.changeset/ @medusajs/engineering -/packages/ @medusajs/engineering -/integration-tests/ @medusajs/engineering +package.json @medusajs/core +yarn.lock @medusajs/os +/.changeset/ @medusajs/os +/packages/ @medusajs/os +/integration-tests/ @medusajs/os /docs-util/ @medusajs/docs /www/ @medusajs/docs /packages/design-system/ @medusajs/ui diff --git a/README.md b/README.md index 93603e474cf04..868ce05a037c1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Learn more about [Medusa’s architecture](https://docs.medusajs.com/learn/advan Follow the [Release Notes](https://github.com/medusajs/medusa/releases) to keep your Medusa project up-to-date. -Check out all [available Medusa integrations](https://docs.medusajs.com/resources/integrations). +Check out all [available Medusa integrations](https://medusajs.com/integrations/). ## Community & Contributions diff --git a/integration-tests/http/CHANGELOG.md b/integration-tests/http/CHANGELOG.md index 157d5c0f69727..3ed3be4e2b149 100644 --- a/integration-tests/http/CHANGELOG.md +++ b/integration-tests/http/CHANGELOG.md @@ -1,5 +1,33 @@ # integration-tests-http +## 1.0.20 + +### Patch Changes + +- Updated dependencies [[`a28226af80a8880afbdb926a5001f0cb0d89fdc9`](https://github.com/medusajs/medusa/commit/a28226af80a8880afbdb926a5001f0cb0d89fdc9), [`40625c82d6deb28ecb4e4c0f911c28fdd7356bf7`](https://github.com/medusajs/medusa/commit/40625c82d6deb28ecb4e4c0f911c28fdd7356bf7), [`822217fa366d4399d3f6a0407451c9649197d5d9`](https://github.com/medusajs/medusa/commit/822217fa366d4399d3f6a0407451c9649197d5d9), [`7b1debfe12fc096f7b4e20f13bdeb925c96085c1`](https://github.com/medusajs/medusa/commit/7b1debfe12fc096f7b4e20f13bdeb925c96085c1), [`eb83954f23077c0714125b6f2f19fd0ef0f288f9`](https://github.com/medusajs/medusa/commit/eb83954f23077c0714125b6f2f19fd0ef0f288f9), [`8c4228fc42e717f9ab72230040e708f606a585b7`](https://github.com/medusajs/medusa/commit/8c4228fc42e717f9ab72230040e708f606a585b7), [`439c7118450c5f9ee0b541de9014093a42b7d0ea`](https://github.com/medusajs/medusa/commit/439c7118450c5f9ee0b541de9014093a42b7d0ea), [`238e7d53c13a1c033886d7c33254919f8b5b22dc`](https://github.com/medusajs/medusa/commit/238e7d53c13a1c033886d7c33254919f8b5b22dc), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47), [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5), [`2c2528a08751945d6f0363473605f1a8ef1a8a2a`](https://github.com/medusajs/medusa/commit/2c2528a08751945d6f0363473605f1a8ef1a8a2a)]: + - @medusajs/core-flows@2.8.8 + - @medusajs/pricing@2.8.8 + - @medusajs/workflow-engine-inmemory@2.8.8 + - @medusajs/product@2.8.8 + - @medusajs/utils@2.8.8 + - @medusajs/test-utils@2.8.8 + - @medusajs/modules-sdk@2.8.8 + - @medusajs/medusa@2.8.8 + - @medusajs/api-key@2.8.8 + - @medusajs/auth@2.8.8 + - @medusajs/cache-inmemory@2.8.8 + - @medusajs/customer@2.8.8 + - @medusajs/event-bus-local@2.8.8 + - @medusajs/fulfillment@2.8.8 + - @medusajs/inventory@2.8.8 + - @medusajs/promotion@2.8.8 + - @medusajs/fulfillment-manual@2.8.8 + - @medusajs/region@2.8.8 + - @medusajs/stock-location@2.8.8 + - @medusajs/store@2.8.8 + - @medusajs/tax@2.8.8 + - @medusajs/user@2.8.8 + ## 1.0.19 ### Patch Changes diff --git a/integration-tests/http/__tests__/campaigns/admin/campaigns.spec.ts b/integration-tests/http/__tests__/campaigns/admin/campaigns.spec.ts index 1ed253914e0ec..3403efbbe86e2 100644 --- a/integration-tests/http/__tests__/campaigns/admin/campaigns.spec.ts +++ b/integration-tests/http/__tests__/campaigns/admin/campaigns.spec.ts @@ -17,7 +17,7 @@ export const campaignData = { budget: { type: CampaignBudgetType.SPEND, limit: 1000, - currency_code: "USD", + currency_code: "usd", }, } @@ -31,7 +31,7 @@ export const campaignsData = [ budget: { type: CampaignBudgetType.SPEND, limit: 1000, - currency_code: "USD", + currency_code: "usd", }, }, { @@ -56,7 +56,7 @@ const promotionData = { target_type: "items", type: "fixed", allocation: "each", - currency_code: "USD", + currency_code: "usd", value: 100, max_quantity: 100, target_rules: [ @@ -122,7 +122,7 @@ medusaIntegrationTestRunner({ value: 100, max_quantity: 100, target_rules: [], - currency_code: "USD", + currency_code: "usd", }, rules: [], } diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 5682059b14343..cc58729ebdf1d 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -419,22 +419,22 @@ medusaIntegrationTestRunner({ compare_at_unit_price: null, is_tax_inclusive: true, quantity: 2, - tax_lines: [ + tax_lines: expect.arrayContaining([ expect.objectContaining({ description: "CA Default Rate", code: "CADEFAULT", rate: 5, provider_id: "system", }), - ], - adjustments: [ - { + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ id: expect.any(String), code: "PROMOTION_APPLIED", promotion_id: promotion.id, amount: 100, - }, - ], + }), + ]), }), ]), }) @@ -456,14 +456,14 @@ medusaIntegrationTestRunner({ id: cart.id, items: expect.arrayContaining([ expect.objectContaining({ - adjustments: [ - { + adjustments: expect.arrayContaining([ + expect.objectContaining({ id: expect.any(String), code: "PROMOTION_APPLIED", promotion_id: promotion.id, amount: 100, - }, - ], + }), + ]), }), ]), }) @@ -823,22 +823,22 @@ medusaIntegrationTestRunner({ compare_at_unit_price: 1500, is_tax_inclusive: true, quantity: 2, - tax_lines: [ + tax_lines: expect.arrayContaining([ expect.objectContaining({ description: "CA Default Rate", code: "CADEFAULT", rate: 5, provider_id: "system", }), - ], - adjustments: [ - { + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ id: expect.any(String), code: "PROMOTION_APPLIED", promotion_id: promotion.id, amount: 100, - }, - ], + }), + ]), }), ]), }) @@ -1235,7 +1235,7 @@ medusaIntegrationTestRunner({ id: expect.any(String), currency_code: "usd", credit_line_total: 2395, - discount_total: 100, + discount_total: 105, credit_lines: [ expect.objectContaining({ amount: 2395, @@ -2071,6 +2071,56 @@ medusaIntegrationTestRunner({ ) }) + it("should not add a promotion that belongs to a different currency than the cart", async () => { + const newPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + // Set for EUR currency, different from USD Currency of the cart + currency_code: "eur", + value: 1000, + apply_to_quantity: 1, + }, + }, + adminHeaders + ) + ).data.promotion + + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [newPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + items: [ + expect.objectContaining({ + adjustments: [], + }), + ], + }) + ) + }) + it("should not generate tax lines if automatic taxes is false", async () => { let updated = await api.post( `/store/carts/${cart.id}`, @@ -2744,7 +2794,7 @@ medusaIntegrationTestRunner({ is_discountable: true, unit_price: 1500, total: 1395, - discount_total: 100, + discount_total: 105, adjustments: [ expect.objectContaining({ promotion_id: promotion.id, @@ -2765,6 +2815,24 @@ medusaIntegrationTestRunner({ ) }) + it("should throw an error when adding a promotion that does not exist", async () => { + const invalidPromoCode = "SOME_INVALID_PROMO_CODE" + + const { response } = await api + .post( + `/store/carts/${cart.id}/promotions`, + { promo_codes: [invalidPromoCode] }, + storeHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data.type).toEqual("invalid_data") + expect(response.data.message).toEqual( + `The promotion code ${invalidPromoCode} is invalid` + ) + }) + it("should remove promotion adjustments when promotion is deleted", async () => { let cartBeforeRemovingPromotion = ( await api.get(`/store/carts/${cart.id}`, storeHeaders) @@ -2775,14 +2843,14 @@ medusaIntegrationTestRunner({ id: cart.id, items: expect.arrayContaining([ expect.objectContaining({ - adjustments: [ - { + adjustments: expect.arrayContaining([ + expect.objectContaining({ id: expect.any(String), code: "PROMOTION_APPLIED", promotion_id: promotion.id, amount: 100, - }, - ], + }), + ]), }), ]), }) @@ -2810,6 +2878,861 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should add a 100 USD tax exclusive promotion for a 105 USD tax inclusive item and logically result in a 0 total with tax 5%", async () => { + const taxExclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_EXCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: false, //Here we apply a tax exclusive promotion to a tax inclusive item in a way that the total SHOULD be 0 + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 100, + apply_to_quantity: 1, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 105, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [taxExclPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 105, + discount_subtotal: 100, + discount_tax_total: 5, + original_total: 105, + total: 0, // 105 - 100 tax excl promotion + 5 promotion tax + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxExclPromotion.code, + amount: 100, + }), + ]), + }), + ]), + promotions: expect.arrayContaining([ + expect.objectContaining({ + code: "PROMOTION_TAX_EXCLUSIVE", + application_method: expect.objectContaining({ + value: 100, + }), + }), + ]), + }) + ) + }) + + it("should add a 105 USD tax inclusive promotion (fixed, across, apply_to_quantity=1) for a 105 USD tax inclusive item and logically result in a 0 total with tax 5%", async () => { + const taxInclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0 + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 105, + apply_to_quantity: 1, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 105, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [taxInclPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 105, + discount_subtotal: 100, + discount_tax_total: 5, + original_total: 105, + total: 0, // 105 - 100 tax excl promotion + 5 promotion tax + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 105, + is_tax_inclusive: true, + }), + ]), + }), + ]), + promotions: expect.arrayContaining([ + expect.objectContaining({ + code: "PROMOTION_TAX_INCLUSIVE", + is_tax_inclusive: true, + application_method: expect.objectContaining({ + value: 105, + }), + }), + ]), + }) + ) + }) + + it("should add a 105 USD tax inclusive promotion (fixed, across, apply_to_quantity=1) for two 105 USD tax inclusive items and logically result in a 105 total with tax 5%", async () => { + const taxInclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0 + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 105, + apply_to_quantity: 1, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 105, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 2, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [taxInclPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 105, + discount_subtotal: 100, + discount_tax_total: 5, + original_total: 210, + total: 105, // 210 - 100 tax excl promotion + 5 promotion tax + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 105, + is_tax_inclusive: true, + }), + ]), + }), + ]), + promotions: expect.arrayContaining([ + expect.objectContaining({ + code: "PROMOTION_TAX_INCLUSIVE", + is_tax_inclusive: true, + application_method: expect.objectContaining({ + value: 105, + }), + }), + ]), + }) + ) + }) + + it("should add a 105 USD tax inclusive promotion (fixed, each, max_quantity=2) for two 105 USD tax inclusive items and logically result in a 0 total with tax 5%", async () => { + const taxInclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0 + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + currency_code: "usd", + value: 105, + max_quantity: 2, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 105, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 2, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [taxInclPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 105, + discount_subtotal: 100, + discount_tax_total: 5, + original_total: 210, + total: 105, // 210 - 100 tax excl promotion + 5 promotion tax + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 105, + is_tax_inclusive: true, + }), + ]), + }), + ]), + promotions: expect.arrayContaining([ + expect.objectContaining({ + code: "PROMOTION_TAX_INCLUSIVE", + is_tax_inclusive: true, + application_method: expect.objectContaining({ + value: 105, + }), + }), + ]), + }) + ) + }) + + it("should add two tax inclusive promotions (50,100) (fixed, across) for two 105 USD tax inclusive items", async () => { + const taxInclPromotion50 = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE_50", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 50, + }, + }, + adminHeaders + ) + ).data.promotion + + const taxInclPromotion100 = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE_100", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 100, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 105, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 2, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { + promo_codes: [taxInclPromotion50.code, taxInclPromotion100.code], + }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 150, + original_total: 210, + total: 60, // 210 - (100 + 50 tax incl promotion) + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion50.code, + amount: 50, + is_tax_inclusive: true, + }), + expect.objectContaining({ + code: taxInclPromotion100.code, + amount: 100, + is_tax_inclusive: true, + }), + ]), + }), + ]), + }) + ) + }) + + it("should verify that reapplying the same promotion code after the cart total has been reduced to zero does not incorrectly remove existing adjustments", async () => { + const taxInclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 50, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 50, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { + promo_codes: [taxInclPromotion.code], + }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 50, + original_total: 50, + total: 0, + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 50, + is_tax_inclusive: true, + }), + ]), + }), + ]), + }) + ) + + let updatedAgain = await api.post( + `/store/carts/${cart.id}`, + { + promo_codes: [taxInclPromotion.code], + }, + storeHeaders + ) + + expect(updatedAgain.status).toEqual(200) + expect(updatedAgain.data.cart).toEqual( + expect.objectContaining({ + discount_total: 50, + original_total: 50, + total: 0, + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 50, + is_tax_inclusive: true, + }), + ]), + }), + ]), + }) + ) + }) + + it("should add a 1500 USD tax inclusive promotion (fixed, across) for 50x 29,95 USD tax inclusive items and logically result in a 0 total with tax 5%", async () => { + const taxInclPromotion = ( + await api.post( + `/admin/promotions`, + { + code: "PROMOTION_TAX_INCLUSIVE", + type: PromotionType.STANDARD, + status: PromotionStatus.ACTIVE, + is_tax_inclusive: true, //Here we apply a tax inclusive promotion to a tax inclusive item in a way that the total SHOULD be 0 + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + currency_code: "usd", + value: 1500, + }, + }, + adminHeaders + ) + ).data.promotion + + const product = ( + await api.post( + `/admin/products`, + { + title: "Product for free", + description: "test", + options: [ + { + title: "Size", + values: ["S", "M", "L", "XL"], + }, + ], + variants: [ + { + title: "S / Black", + sku: "special-shirt", + options: { + Size: "S", + }, + manage_inventory: false, + prices: [ + { + amount: 29.95, + currency_code: "usd", + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + }, + storeHeadersWithCustomer + ) + ).data.cart + + cart = ( + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 50, + }, + storeHeaders + ) + ).data.cart + + let updated = await api.post( + `/store/carts/${cart.id}`, + { promo_codes: [taxInclPromotion.code] }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + discount_total: 1497.5, + original_total: 1497.5, + total: 0, + items: expect.arrayContaining([ + expect.objectContaining({ + is_tax_inclusive: true, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + code: taxInclPromotion.code, + amount: 1497.5, + is_tax_inclusive: true, + }), + ]), + }), + ]), + promotions: expect.arrayContaining([ + expect.objectContaining({ + code: "PROMOTION_TAX_INCLUSIVE", + is_tax_inclusive: true, + application_method: expect.objectContaining({ + value: 1500, + }), + }), + ]), + }) + ) + }) }) describe("POST /store/carts/:id/customer", () => { diff --git a/integration-tests/http/__tests__/currency/admin/currency.spec.ts b/integration-tests/http/__tests__/currency/admin/currency.spec.ts index bb5d8a9c1baf1..b8db051219b70 100644 --- a/integration-tests/http/__tests__/currency/admin/currency.spec.ts +++ b/integration-tests/http/__tests__/currency/admin/currency.spec.ts @@ -22,7 +22,7 @@ medusaIntegrationTestRunner({ ) expect(response.status).toEqual(200) - expect(response.data.currencies).toHaveLength(120) + expect(response.data.currencies).toHaveLength(122) expect(response.data.currencies).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts index 5bfc0552fab83..c83a2c5ff4b15 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts @@ -16,6 +16,7 @@ medusaIntegrationTestRunner({ let stockLocation: HttpTypes.AdminStockLocation let testDraftOrder: HttpTypes.AdminDraftOrder let shippingOption: HttpTypes.AdminShippingOption + let shippingOptionHeavy: HttpTypes.AdminShippingOption beforeEach(async () => { const container = getContainer() @@ -55,6 +56,14 @@ medusaIntegrationTestRunner({ ) ).data.shipping_profile + const shippingProfileHeavy = ( + await api.post( + `/admin/shipping-profiles`, + { name: "test shipping profile heavy", type: "heavy" }, + adminHeaders + ) + ).data.shipping_profile + const fulfillmentSets = ( await api.post( `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, @@ -103,7 +112,28 @@ medusaIntegrationTestRunner({ description: "Test description", code: "test-code", }, - prices: [{ currency_code: "usd", amount: 1000 }], + prices: [{ currency_code: "usd", amount: 5 }], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + + shippingOptionHeavy = ( + await api.post( + `/admin/shipping-options`, + { + name: `Test shipping option ${fulfillmentSet.id}`, + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfileHeavy.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [{ currency_code: "usd", amount: 10 }], rules: [], }, adminHeaders @@ -117,6 +147,13 @@ medusaIntegrationTestRunner({ email: "test@test.com", region_id: region.id, sales_channel_id: salesChannel.id, + shipping_address: { + address_1: "123 Main St", + city: "Anytown", + country_code: "US", + postal_code: "12345", + first_name: "John", + }, }, adminHeaders ) @@ -610,6 +647,152 @@ medusaIntegrationTestRunner({ expect(order.shipping_methods.length).toBe(0) }) + + it("should ensure that the shipping method is removed from the order and tax lines are updated with multiple shipping methods", async () => { + /** + * Add Heavy SO + */ + + edit = ( + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit`, + {}, + adminHeaders + ) + ).data.draft_order_preview + + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/shipping-methods`, + { + shipping_option_id: shippingOptionHeavy.id, + }, + adminHeaders + ) + + edit = ( + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + ).data.draft_order_preview + + /** + * Tax rate -> 2% + * + * One product -> 10$ + * Shipping method 1 -> 5$ + * Shipping method 2 -> 10$ + */ + + expect(edit).toEqual( + expect.objectContaining({ + total: 25.5, + subtotal: 25, + tax_total: 0.5, + + items: [ + expect.objectContaining({ + subtotal: 10, + total: 10.2, + tax_total: 0.2, + tax_lines: [ + expect.objectContaining({ + rate: 2, + }), + ], + }), + ], + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 5, + subtotal: 5, + total: 5.1, + tax_total: 0.1, + }), + expect.objectContaining({ + shipping_option_id: shippingOptionHeavy.id, + amount: 10, + subtotal: 10, + total: 10.2, + tax_total: 0.2, + }), + ]), + }) + ) + + /** + * Remove Heavy shipping method + */ + + edit = ( + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit`, + {}, + adminHeaders + ) + ).data.draft_order_preview + + const response = await api.delete( + `/admin/draft-orders/${ + testDraftOrder.id + }/edit/shipping-methods/method/${ + edit.shipping_methods.find( + (sm) => sm.shipping_option_id === shippingOptionHeavy.id + ).id + }`, + adminHeaders + ) + + expect(response.status).toBe(200) + expect(response.data.draft_order_preview.shipping_methods.length).toBe( + 1 + ) + + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + + const order = ( + await api.get( + `/admin/draft-orders/${testDraftOrder.id}?fields=+total,+subtotal,+tax_total,+items.subtotal,+items.total,+items.tax_total,+shipping_methods.amount,+shipping_methods.subtotal,+shipping_methods.total,+shipping_methods.tax_total`, + adminHeaders + ) + ).data.draft_order + + expect(order).toEqual( + expect.objectContaining({ + total: 15.3, + subtotal: 15, + tax_total: 0.3, + + items: [ + expect.objectContaining({ + subtotal: 10, + total: 10.2, + tax_total: 0.2, + tax_lines: [ + expect.objectContaining({ + rate: 2, + }), + ], + }), + ], + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 5, + subtotal: 5, + total: 5.1, + tax_total: 0.1, + }), + ]), + }) + ) + }) }) }, }) diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index 11322c4a9f0d9..28e2e4558e014 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -1813,6 +1813,110 @@ medusaIntegrationTestRunner({ ) }) + it("updates products relations and attributes", async () => { + const shortsCategory = ( + await api.post( + "/admin/product-categories", + { name: "Shorts", is_internal: false, is_active: true }, + adminHeaders + ) + ).data.product_category + + const pantsCategory = ( + await api.post( + "/admin/product-categories", + { name: "Pants", is_internal: false, is_active: true }, + adminHeaders + ) + ).data.product_category + + const payload = { + title: "Test an update", + weight: 100, + length: 100, + width: 100, + height: 100, + options: [{ title: "size", values: ["large", "small"] }], + variants: [ + { + options: { size: "large" }, + title: "New variant", + prices: [ + { + currency_code: "usd", + amount: 200, + }, + ], + }, + ], + } + + const createdProduct = ( + await api.post("/admin/products", payload, adminHeaders) + ).data.product + + let updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}`, + { weight: 20, length: null }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: "20", + length: null, + width: "100", + height: "100", + }) + ) + + updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}?fields=+categories.id`, + { categories: [{ id: pantsCategory.id }] }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: "20", + length: null, + width: "100", + height: "100", + categories: expect.arrayContaining([ + expect.objectContaining({ + id: pantsCategory.id, + }), + ]), + }) + ) + + updatedProduct = ( + await api.post( + `/admin/products/${createdProduct.id}?fields=+categories.id`, + { weight: null, length: 20, width: 50 }, + adminHeaders + ) + ).data.product + + expect(updatedProduct).toEqual( + expect.objectContaining({ + weight: null, + length: "20", + width: "50", + height: "100", + categories: expect.arrayContaining([ + expect.objectContaining({ + id: pantsCategory.id, + }), + ]), + }) + ) + }) + it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => { const payload = { collection_id: null, @@ -2159,6 +2263,99 @@ medusaIntegrationTestRunner({ }) }) + it("should recreate a variant with the same sku after deletion", async () => { + const payload = { + title: "Test sku", + shipping_profile_id: shippingProfile.id, + variants: [ + { + sku: "test-sku-should-persist", + manage_inventory: true, + title: "Recreate variant", + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + }, + ], + } + + const created = ( + await api.post( + "/admin/products?fields=*variants.inventory_items.inventory", + getProductFixture(payload), + adminHeaders + ) + ).data.product + + const createdVariant = created.variants.find( + (v) => v.sku === "test-sku-should-persist" + ) + + const inventoryItem = createdVariant.inventory_items[0] + + expect(created).toEqual( + expect.objectContaining({ + variants: expect.arrayContaining([ + expect.objectContaining({ + sku: "test-sku-should-persist", + inventory_items: expect.arrayContaining([ + expect.objectContaining({ + inventory: expect.objectContaining({ + sku: "test-sku-should-persist", + }), + }), + ]), + title: "Recreate variant", + }), + ]), + }) + ) + + await api.delete( + `/admin/products/${created.id}/variants/${createdVariant.id}`, + adminHeaders + ) + + const recreated = ( + await api.post( + `/admin/products/${created.id}/variants?fields=+variants.inventory_items.inventory.sku`, + { + sku: "test-sku-should-persist", + manage_inventory: true, + title: "Recreate variant", + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + }, + adminHeaders + ) + ).data.product + + expect(recreated).toEqual( + expect.objectContaining({ + variants: expect.arrayContaining([ + expect.objectContaining({ + sku: "test-sku-should-persist", + inventory_items: expect.arrayContaining([ + expect.objectContaining({ + inventory: expect.objectContaining({ + sku: "test-sku-should-persist", + }), + }), + ]), + title: "Recreate variant", + }), + ]), + }) + ) + }) + it("updates products sales channels", async () => { const salesChannel1 = ( await api.post( diff --git a/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts b/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts index 3c1d753669395..1160471025ce0 100644 --- a/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts +++ b/integration-tests/http/__tests__/promotions/admin/promotions.spec.ts @@ -31,7 +31,7 @@ const standardPromotionPayload = { target_type: "items", type: "fixed", allocation: "each", - currency_code: "USD", + currency_code: "usd", value: 100, max_quantity: 100, target_rules: [ @@ -87,7 +87,7 @@ medusaIntegrationTestRunner({ target_type: "items", value: 100, target_rules: [promotionRule], - currency_code: "USD", + currency_code: "usd", }, rules: [promotionRule], }, @@ -180,7 +180,7 @@ medusaIntegrationTestRunner({ type: "fixed", target_type: "order", value: 100, - currency_code: "USD", + currency_code: "usd", }, }, adminHeaders @@ -322,7 +322,7 @@ medusaIntegrationTestRunner({ allocation: "each", value: 100, max_quantity: 100, - currency_code: "USD", + currency_code: "usd", target_rules: [ { attribute: "test.test", @@ -364,7 +364,7 @@ medusaIntegrationTestRunner({ allocation: "each", value: 100, max_quantity: 100, - currency_code: "USD", + currency_code: "usd", buy_rules: [ { attribute: "test.test", @@ -415,7 +415,7 @@ medusaIntegrationTestRunner({ max_quantity: 100, apply_to_quantity: 1, buy_rules_min_quantity: 1, - currency_code: "USD", + currency_code: "usd", target_rules: [ { attribute: "test.test", @@ -557,7 +557,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - currency_code: "USD", + currency_code: "usd", value: 100, max_quantity: 100, }, @@ -683,7 +683,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "across", - currency_code: "DKK", + currency_code: "dkk", value: 100, }, }, @@ -727,11 +727,11 @@ medusaIntegrationTestRunner({ ).data.cart /** - * Orignal total -> 1300 DKK (tax incl.) + * Orignal total -> 1300 dkk (tax incl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax incl.) + * Promotion -> FIXED 100 dkk (tax incl.) * - * We want total to be 1300 DKK - 100 DKK = 1200 DKK + * We want total to be 1300 dkk - 100 dkk = 1200 dkk */ expect(cart).toEqual( expect.objectContaining({ @@ -812,11 +812,11 @@ medusaIntegrationTestRunner({ ).data.order /** - * Orignal total -> 1300 DKK (tax incl.) + * Orignal total -> 1300 dkk (tax incl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax incl.) + * Promotion -> FIXED 100 dkk (tax incl.) * - * We want total to be 1300 DKK - 100 DKK = 1200 DKK + * We want total to be 1300 dkk - 100 dkk = 1200 dkk */ expect(order).toEqual( expect.objectContaining({ @@ -972,7 +972,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - currency_code: "DKK", + currency_code: "dkk", value: 100, max_quantity: 2, }, @@ -1022,11 +1022,11 @@ medusaIntegrationTestRunner({ ).data.cart /** - * Orignal total -> 1500 DKK (tax incl.) - * Promotion -> FIXED 100 DKK per item (tax incl.) + * Orignal total -> 1500 dkk (tax incl.) + * Promotion -> FIXED 100 dkk per item (tax incl.) * Tax rate -> 25% * - * We want total to be 1500 DKK - 100 DKK - 100 DKK = 1300 DKK + * We want total to be 1500 dkk - 100 dkk - 100 dkk = 1300 dkk */ expect(cart).toEqual( expect.objectContaining({ @@ -1036,7 +1036,7 @@ medusaIntegrationTestRunner({ subtotal: 1200, // taxable base (item subtotal - discount subtotal) = 1200 - 200 = 1000 tax_total: 260, - discount_total: 200, // 2 * 100 DKK fixed tax inclusive + discount_total: 200, // 2 * 100 dkk fixed tax inclusive discount_subtotal: 160, discount_tax_total: 40, @@ -1127,11 +1127,11 @@ medusaIntegrationTestRunner({ ).data.order /** - * Orignal total -> 1500 DKK (tax incl.) - * Promotion -> FIXED 100 DKK per item (tax incl.) + * Orignal total -> 1500 dkk (tax incl.) + * Promotion -> FIXED 100 dkk per item (tax incl.) * Tax rate -> 25% * - * We want total to be 1500 DKK - 100 DKK - 100 DKK = 1300 DKK + * We want total to be 1500 dkk - 100 dkk - 100 dkk = 1300 dkk */ expect(order).toEqual( expect.objectContaining({ @@ -1141,7 +1141,7 @@ medusaIntegrationTestRunner({ subtotal: 1200, // taxable base (item subtotal - discount subtotal) = 1200 - 200 = 1000 tax_total: 260, - discount_total: 200, // 2 * 100 DKK fixed tax inclusive + discount_total: 200, // 2 * 100 dkk fixed tax inclusive discount_subtotal: 160, discount_tax_total: 40, @@ -1263,7 +1263,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "across", - currency_code: "DKK", + currency_code: "dkk", value: 100, }, }, @@ -1307,9 +1307,9 @@ medusaIntegrationTestRunner({ ).data.cart /** - * Orignal total -> 1300 DKK (tax incl.) + * Orignal total -> 1300 dkk (tax incl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax exclusive !) + * Promotion -> FIXED 100 dkk (tax exclusive !) */ expect(cart).toEqual( expect.objectContaining({ @@ -1322,9 +1322,9 @@ medusaIntegrationTestRunner({ original_total: 1300, original_tax_total: 260, - discount_total: 100, + discount_total: 125, discount_subtotal: 100, - discount_tax_total: 20, + discount_tax_total: 25, item_total: 1175, item_subtotal: 1040, @@ -1354,14 +1354,13 @@ medusaIntegrationTestRunner({ original_total: 1300, original_tax_total: 260, - discount_total: 100, + discount_total: 125, discount_subtotal: 100, - discount_tax_total: 20, + discount_tax_total: 25, adjustments: expect.arrayContaining([ expect.objectContaining({ amount: 100, - is_tax_inclusive: false, }), ]), }), @@ -1388,9 +1387,9 @@ medusaIntegrationTestRunner({ ).data.order /** - * Orignal total -> 1300 DKK (tax incl.) + * Orignal total -> 1300 dkk (tax incl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax exclusive !) + * Promotion -> FIXED 100 dkk (tax exclusive !) */ expect(order).toEqual( expect.objectContaining({ @@ -1403,9 +1402,9 @@ medusaIntegrationTestRunner({ original_total: 1300, original_tax_total: 260, - discount_total: 100, + discount_total: 125, discount_subtotal: 100, - discount_tax_total: 20, + discount_tax_total: 25, item_total: 1175, item_subtotal: 1040, @@ -1435,14 +1434,13 @@ medusaIntegrationTestRunner({ original_total: 1300, original_tax_total: 260, - discount_total: 100, + discount_total: 125, discount_subtotal: 100, - discount_tax_total: 20, + discount_tax_total: 25, adjustments: expect.arrayContaining([ expect.objectContaining({ amount: 100, - is_tax_inclusive: false, }), ]), }), @@ -1500,7 +1498,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "across", - currency_code: "DKK", + currency_code: "dkk", value: 100, }, }, @@ -1544,9 +1542,9 @@ medusaIntegrationTestRunner({ ).data.cart /** - * Orignal total -> 1300 DKK (tax excl.) + * Orignal total -> 1300 dkk (tax excl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax exclusive !) + * Promotion -> FIXED 100 dkk (tax exclusive !) */ expect(cart).toEqual( expect.objectContaining({ @@ -1598,7 +1596,6 @@ medusaIntegrationTestRunner({ adjustments: expect.arrayContaining([ expect.objectContaining({ amount: 100, - is_tax_inclusive: false, }), ]), }), @@ -1625,9 +1622,9 @@ medusaIntegrationTestRunner({ ).data.order /** - * Orignal total -> 1300 DKK (tax excl.) + * Orignal total -> 1300 dkk (tax excl.) * Tax rate -> 25% - * Promotion -> FIXED 100 DKK (tax exclusive !) + * Promotion -> FIXED 100 dkk (tax exclusive !) */ expect(order).toEqual( expect.objectContaining({ @@ -1679,7 +1676,6 @@ medusaIntegrationTestRunner({ adjustments: expect.arrayContaining([ expect.objectContaining({ amount: 100, - is_tax_inclusive: false, }), ]), }), @@ -2053,7 +2049,7 @@ medusaIntegrationTestRunner({ buy_rules_min_quantity: 1, buy_rules: [promotionRule], target_rules: [promotionRule], - currency_code: "USD", + currency_code: "usd", }, rules: [promotionRule], }, @@ -2216,7 +2212,7 @@ medusaIntegrationTestRunner({ type: PromotionType.BUYGET, application_method: { type: "fixed", - currency_code: "USD", + currency_code: "usd", target_type: "items", allocation: "across", value: 100, diff --git a/integration-tests/http/package.json b/integration-tests/http/package.json index 4be92afa27974..7ad8f2d791328 100644 --- a/integration-tests/http/package.json +++ b/integration-tests/http/package.json @@ -1,6 +1,6 @@ { "name": "integration-tests-http", - "version": "1.0.19", + "version": "1.0.20", "main": "index.js", "license": "MIT", "private": true, diff --git a/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts b/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts index 33d5f482ca87f..b8be186cf78a5 100644 --- a/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/add-promotions-to-cart.spec.ts @@ -240,11 +240,6 @@ medusaIntegrationTestRunner({ operator: "in", values: ["cus_test"], }, - { - attribute: "currency_code", - operator: "in", - values: ["eur"], - }, ], application_method: { type: "fixed", @@ -275,11 +270,6 @@ medusaIntegrationTestRunner({ operator: "in", values: ["cus_test"], }, - { - attribute: "currency_code", - operator: "in", - values: ["eur"], - }, ], application_method: { type: "fixed", @@ -300,7 +290,7 @@ medusaIntegrationTestRunner({ ]) const cart = await cartModuleService.createCarts({ - currency_code: "eur", + currency_code: "usd", customer_id: "cus_test", items: [ { diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index fa00cbe0fb0e9..2fd4aa6b2c63e 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -1730,6 +1730,157 @@ medusaIntegrationTestRunner({ }) }) + it("should update the cart email from one guest account to another", async () => { + const guestsMainEmail = "guest.main@acme.com" + const guestsSecondaryEmail = "guest.secondary@acme.com" + + const [guestMain, guestSecondary] = + await customerModule.createCustomers([ + { + email: guestsMainEmail, + has_account: false, + }, + { + email: guestsSecondaryEmail, + has_account: false, + }, + ]) + + const cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + email: guestsSecondaryEmail, + shipping_address: { + address_1: "test address 1", + address_2: "test address 2", + city: "ny", + country_code: "us", + province: "ny", + postal_code: "94016", + }, + sales_channel_id: salesChannel.id, + }, + storeHeaders + ) + ).data.cart + + expect(cart.customer.id).toBe(guestSecondary.id) + + // update the cart without providing an email + await api.post( + `/store/carts/${cart.id}`, + { + metadata: { + test: "test updated", + }, + }, + storeHeaders + ) + + let currentCart = await api.get( + `/store/carts/${cart.id}`, + storeHeaders + ) + let currentCartCustomer = currentCart.data.cart.customer + + expect(currentCartCustomer.id).toEqual(guestSecondary.id) + expect(currentCartCustomer.email).toEqual(guestSecondary.email) + expect(currentCart.data.cart.metadata).toEqual({ + test: "test updated", + }) + + // update the cart providing an email + await api.post( + `/store/carts/${cart.id}`, + { + email: guestsMainEmail, + metadata: { + test: "test updated 2, new customer", + }, + }, + storeHeaders + ) + + currentCart = await api.get(`/store/carts/${cart.id}`, storeHeaders) + + expect(currentCart.data.cart.customer.id).toEqual(guestMain.id) + expect(currentCart.data.cart.email).toEqual(guestMain.email) + expect(currentCart.data.cart.metadata).toEqual({ + test: "test updated 2, new customer", + }) + }) + + it("should persist customer on cart if updated with the same email", async () => { + const guestsMainEmail = "guest.main@acme.com" + + const cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + email: guestsMainEmail, + shipping_address: { + address_1: "test address 1", + address_2: "test address 2", + city: "ny", + country_code: "us", + province: "ny", + postal_code: "94016", + }, + sales_channel_id: salesChannel.id, + }, + storeHeaders + ) + ).data.cart + + const guestMain = cart.customer + + // update the cart providing an email + await api.post( + `/store/carts/${cart.id}`, + { + email: guestsMainEmail, // update with the same mail + metadata: { + test: "test updated 2, same customer", + }, + }, + storeHeaders + ) + + let currentCart = await api.get( + `/store/carts/${cart.id}`, + storeHeaders + ) + + expect(currentCart.data.cart.customer.id).toEqual(guestMain.id) + expect(currentCart.data.cart.email).toEqual(guestMain.email) + expect(currentCart.data.cart.metadata).toEqual({ + test: "test updated 2, same customer", + }) + + // update the cart providing an email + await api.post( + `/store/carts/${cart.id}`, + { + email: guestsMainEmail, // update with the same mail + metadata: { + test: "test updated 3, same customer", + }, + }, + storeHeaders + ) + + currentCart = await api.get(`/store/carts/${cart.id}`, storeHeaders) + + expect(currentCart.data.cart.customer.id).toEqual(guestMain.id) + expect(currentCart.data.cart.email).toEqual(guestMain.email) + expect(currentCart.data.cart.metadata).toEqual({ + test: "test updated 3, same customer", + }) + }) + it("should keep the same customer when updating the customer cart and update Cart's email if provided", async () => { // create a customer const mainEmail = "jhon.doe@acme.com" diff --git a/integration-tests/modules/__tests__/cart/store/remove-promotions-from-cart.spec.ts b/integration-tests/modules/__tests__/cart/store/remove-promotions-from-cart.spec.ts index b2e9f9e14dd04..7d64b7aefd7c8 100644 --- a/integration-tests/modules/__tests__/cart/store/remove-promotions-from-cart.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/remove-promotions-from-cart.spec.ts @@ -191,11 +191,6 @@ medusaIntegrationTestRunner({ operator: "in", values: ["cus_test"], }, - { - attribute: "currency_code", - operator: "in", - values: ["eur"], - }, ], application_method: { type: "fixed", @@ -225,11 +220,6 @@ medusaIntegrationTestRunner({ operator: "in", values: ["cus_test"], }, - { - attribute: "currency_code", - operator: "in", - values: ["eur"], - }, ], application_method: { type: "fixed", @@ -249,7 +239,7 @@ medusaIntegrationTestRunner({ }) const cart = await cartModuleService.createCarts({ - currency_code: "eur", + currency_code: "usd", customer_id: "cus_test", items: [ { diff --git a/packages/admin/admin-bundler/CHANGELOG.md b/packages/admin/admin-bundler/CHANGELOG.md index 7ed01f6e25fca..88b1409a6f57e 100644 --- a/packages/admin/admin-bundler/CHANGELOG.md +++ b/packages/admin/admin-bundler/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/admin-bundler +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`3fa1db9dea27d99d7b5796281f118210f35a2880`](https://github.com/medusajs/medusa/commit/3fa1db9dea27d99d7b5796281f118210f35a2880), [`439c7118450c5f9ee0b541de9014093a42b7d0ea`](https://github.com/medusajs/medusa/commit/439c7118450c5f9ee0b541de9014093a42b7d0ea), [`491b08e0448e3e7d69c09b9516c39f50e2f691a0`](https://github.com/medusajs/medusa/commit/491b08e0448e3e7d69c09b9516c39f50e2f691a0)]: + - @medusajs/dashboard@2.8.8 + - @medusajs/admin-shared@2.8.8 + - @medusajs/admin-vite-plugin@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/admin/admin-bundler/package.json b/packages/admin/admin-bundler/package.json index 4ff65734b2a5e..dab859a9ed915 100644 --- a/packages/admin/admin-bundler/package.json +++ b/packages/admin/admin-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/admin-bundler", - "version": "2.8.7", + "version": "2.8.8", "description": "Bundler for the Medusa admin dashboard.", "author": "Kasper Kristensen ", "scripts": { @@ -18,16 +18,16 @@ "package.json" ], "devDependencies": { - "@medusajs/types": "2.8.7", + "@medusajs/types": "2.8.8", "@types/compression": "^1.7.5", "express": "^4.21.0", "tsup": "^8.0.1", "typescript": "^5.3.3" }, "dependencies": { - "@medusajs/admin-shared": "2.8.7", - "@medusajs/admin-vite-plugin": "2.8.7", - "@medusajs/dashboard": "2.8.7", + "@medusajs/admin-shared": "2.8.8", + "@medusajs/admin-vite-plugin": "2.8.8", + "@medusajs/dashboard": "2.8.8", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", "compression": "^1.7.4", diff --git a/packages/admin/admin-sdk/CHANGELOG.md b/packages/admin/admin-sdk/CHANGELOG.md index 2e7559577a151..ca279c4f214d9 100644 --- a/packages/admin/admin-sdk/CHANGELOG.md +++ b/packages/admin/admin-sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/admin-sdk +## 2.8.8 + +### Patch Changes + +- Updated dependencies []: + - @medusajs/admin-shared@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/admin/admin-sdk/package.json b/packages/admin/admin-sdk/package.json index 3d525345066dc..aafa4c645107a 100644 --- a/packages/admin/admin-sdk/package.json +++ b/packages/admin/admin-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@medusajs/admin-sdk", "description": "SDK for building extension for the Medusa admin dashboard.", - "version": "2.8.7", + "version": "2.8.8", "author": "Kasper Kristensen ", "types": "dist/index.d.ts", "main": "dist/index.js", @@ -24,8 +24,8 @@ "typescript": "^5.3.3" }, "dependencies": { - "@medusajs/admin-shared": "2.8.7", - "zod": "3.22.4" + "@medusajs/admin-shared": "2.8.8", + "zod": "3.25.76" }, "packageManager": "yarn@3.2.1" } diff --git a/packages/admin/admin-shared/CHANGELOG.md b/packages/admin/admin-shared/CHANGELOG.md index 9eff3deb66d8e..f58c7d26141b5 100644 --- a/packages/admin/admin-shared/CHANGELOG.md +++ b/packages/admin/admin-shared/CHANGELOG.md @@ -1,5 +1,7 @@ # @medusajs/admin-shared +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/admin/admin-shared/package.json b/packages/admin/admin-shared/package.json index 0add286c26390..a0dd6830a1d6a 100644 --- a/packages/admin/admin-shared/package.json +++ b/packages/admin/admin-shared/package.json @@ -1,7 +1,7 @@ { "name": "@medusajs/admin-shared", "description": "Shared code for Medusa admin packages.", - "version": "2.8.7", + "version": "2.8.8", "author": "Kasper Kristensen ", "types": "dist/index.d.ts", "main": "dist/index.js", diff --git a/packages/admin/admin-vite-plugin/CHANGELOG.md b/packages/admin/admin-vite-plugin/CHANGELOG.md index 5ba50fceaf5f1..fed7d906a7551 100644 --- a/packages/admin/admin-vite-plugin/CHANGELOG.md +++ b/packages/admin/admin-vite-plugin/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/admin-vite-plugin +## 2.8.8 + +### Patch Changes + +- Updated dependencies []: + - @medusajs/admin-shared@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/admin/admin-vite-plugin/package.json b/packages/admin/admin-vite-plugin/package.json index 40d4b50c860a0..8f432f46916fb 100644 --- a/packages/admin/admin-vite-plugin/package.json +++ b/packages/admin/admin-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/admin-vite-plugin", - "version": "2.8.7", + "version": "2.8.8", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", @@ -40,7 +40,7 @@ "@babel/parser": "7.25.6", "@babel/traverse": "7.25.6", "@babel/types": "7.25.6", - "@medusajs/admin-shared": "2.8.7", + "@medusajs/admin-shared": "2.8.8", "chokidar": "3.5.3", "fdir": "6.1.1", "magic-string": "0.30.5", diff --git a/packages/admin/dashboard/CHANGELOG.md b/packages/admin/dashboard/CHANGELOG.md index 0ac8ea7a97e68..730046c6e1c07 100644 --- a/packages/admin/dashboard/CHANGELOG.md +++ b/packages/admin/dashboard/CHANGELOG.md @@ -1,5 +1,21 @@ # @medusajs/dashboard +## 2.8.8 + +### Patch Changes + +- [#12989](https://github.com/medusajs/medusa/pull/12989) [`3fa1db9dea27d99d7b5796281f118210f35a2880`](https://github.com/medusajs/medusa/commit/3fa1db9dea27d99d7b5796281f118210f35a2880) Thanks [@fPolic](https://github.com/fPolic)! - fix(dashboard): allocation UI for orders with more than 20 reservation items + +- [#13019](https://github.com/medusajs/medusa/pull/13019) [`439c7118450c5f9ee0b541de9014093a42b7d0ea`](https://github.com/medusajs/medusa/commit/439c7118450c5f9ee0b541de9014093a42b7d0ea) Thanks [@fPolic](https://github.com/fPolic)! - fix(dashboard, product): update product attributes + +- [#12939](https://github.com/medusajs/medusa/pull/12939) [`491b08e0448e3e7d69c09b9516c39f50e2f691a0`](https://github.com/medusajs/medusa/commit/491b08e0448e3e7d69c09b9516c39f50e2f691a0) Thanks [@fPolic](https://github.com/fPolic)! - fix(dashboard): clearing multiitem combobox + +- Updated dependencies [[`0db5bf6f8cfb47c67435f92733879e990b500d83`](https://github.com/medusajs/medusa/commit/0db5bf6f8cfb47c67435f92733879e990b500d83)]: + - @medusajs/js-sdk@2.8.8 + - @medusajs/admin-shared@2.8.8 + - @medusajs/icons@2.8.8 + - @medusajs/ui@4.0.18 + ## 2.8.7 ### Patch Changes diff --git a/packages/admin/dashboard/package.json b/packages/admin/dashboard/package.json index 1a098f73c1f32..aa2da935fa232 100644 --- a/packages/admin/dashboard/package.json +++ b/packages/admin/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/dashboard", - "version": "2.8.7", + "version": "2.8.8", "scripts": { "generate:static": "node ./scripts/generate-currencies.js && prettier --write ./src/lib/currencies.ts", "dev": "vite", @@ -45,10 +45,10 @@ "@dnd-kit/utilities": "^3.2.2", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "3.4.2", - "@medusajs/admin-shared": "2.8.7", - "@medusajs/icons": "2.8.7", - "@medusajs/js-sdk": "2.8.7", - "@medusajs/ui": "4.0.17", + "@medusajs/admin-shared": "2.8.8", + "@medusajs/icons": "2.8.8", + "@medusajs/js-sdk": "2.8.8", + "@medusajs/ui": "4.0.18", "@tanstack/react-query": "5.64.2", "@tanstack/react-table": "8.20.5", "@tanstack/react-virtual": "^3.8.3", @@ -73,13 +73,13 @@ "react-i18next": "13.5.0", "react-jwt": "^1.2.0", "react-router-dom": "6.20.1", - "zod": "3.22.4" + "zod": "3.25.76" }, "devDependencies": { - "@medusajs/admin-shared": "2.8.7", - "@medusajs/admin-vite-plugin": "2.8.7", - "@medusajs/types": "2.8.7", - "@medusajs/ui-preset": "2.8.7", + "@medusajs/admin-shared": "2.8.8", + "@medusajs/admin-vite-plugin": "2.8.8", + "@medusajs/types": "2.8.8", + "@medusajs/ui-preset": "2.8.8", "@types/node": "^20.11.15", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", diff --git a/packages/admin/dashboard/src/components/inputs/combobox/combobox.tsx b/packages/admin/dashboard/src/components/inputs/combobox/combobox.tsx index f1747de7d1707..c06cfcc27f8c5 100644 --- a/packages/admin/dashboard/src/components/inputs/combobox/combobox.tsx +++ b/packages/admin/dashboard/src/components/inputs/combobox/combobox.tsx @@ -249,7 +249,7 @@ const ComboboxImpl = ( type="button" onClick={(e) => { e.preventDefault() - handleValueChange(undefined) + handleValueChange(isArrayValue ? ([] as unknown as T) : undefined) }} className="bg-ui-bg-base hover:bg-ui-bg-base-hover txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive transition-fg absolute left-0.5 top-0.5 z-[1] flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1 outline-none" > diff --git a/packages/admin/dashboard/src/i18n/translations/vi.json b/packages/admin/dashboard/src/i18n/translations/vi.json index 2415b0e21c603..a26173885bf59 100644 --- a/packages/admin/dashboard/src/i18n/translations/vi.json +++ b/packages/admin/dashboard/src/i18n/translations/vi.json @@ -539,18 +539,22 @@ "fields": { "title": { "label": "Tiêu đề", - "hint": "Đặt cho sản phẩm một tiêu đề ngắn gọn và rõ ràng.<0/>Độ dài khuyến nghị là 50-60 ký tự để tối ưu hóa cho công cụ tìm kiếm." + "hint": "Đặt cho sản phẩm một tiêu đề ngắn gọn và rõ ràng.<0/>Độ dài khuyến nghị là 50-60 ký tự để tối ưu hóa cho công cụ tìm kiếm.", + "placeholder": "Áo khoác cho mùa đông" }, "subtitle": { - "label": "Phụ đề" + "label": "Phụ đề", + "placeholder": "Ấm và mềm mại" }, "handle": { "label": "Định danh", - "tooltip": "Định danh được sử dụng để tham chiếu sản phẩm trên giao diện cửa hàng của bạn. Nếu không chỉ định, định danh sẽ được tạo từ tiêu đề sản phẩm." + "tooltip": "Định danh được sử dụng để tham chiếu sản phẩm trên giao diện cửa hàng của bạn. Nếu không chỉ định, định danh sẽ được tạo từ tiêu đề sản phẩm.", + "placeholder": "ao-khoac-mua-dong" }, "description": { "label": "Mô tả", - "hint": "Đặt cho sản phẩm một mô tả ngắn gọn và rõ ràng.<0/>Độ dài khuyến nghị là 120-160 ký tự để tối ưu hóa cho công cụ tìm kiếm." + "hint": "Đặt cho sản phẩm một mô tả ngắn gọn và rõ ràng.<0/>Độ dài khuyến nghị là 120-160 ký tự để tối ưu hóa cho công cụ tìm kiếm.", + "placeholder": "Áo khoác ấm và mềm mại" }, "discountable": { "label": "Có thể giảm giá", @@ -811,8 +815,10 @@ "locationLevels": "Vị trí", "associatedVariants": "Biến thể liên quan", "manageLocations": "Quản lý vị trí", + "manageLocationQuantity": "Quản lý số vị trí", "deleteWarning": "Bạn sắp xóa một mục kho hàng. Hành động này không thể hoàn tác.", "editItemDetails": "Chỉnh sửa chi tiết mục", + "quantityAcrossLocations": "{{quantity}} trên {{locations}} địa điểm", "create": { "title": "Tạo mục kho hàng", "details": "Chi tiết", @@ -1004,6 +1010,31 @@ } }, "orders": { + "giftCardsStoreCreditLines": "Thẻ quà tặng & hạn mức tín dụng", + "creditLines": { + "title": "Hạn mức tín dụng", + "total": "Tổng tất cả các hạn mức tín dụng", + "creditOrDebit": "Tín dụng / Ghi nợ", + "createCreditLine": "Tạo hạn mức tín dụng", + "createCreditLineSuccess": "Hạn mức tín dụng được tạo thành công", + "createCreditLineError": "Lỗi khi tạo hạn mức tín dụng", + "createCreditLineDescription": "Tạo một hạn mức tín dụng với số tiền {{amount}}", + "operation": "Thao tác", + "credit": "Tín dụng", + "creditDescription": "Thêm một khoản dương vào đơn hàng", + "debit": "Ghi nợ", + "debitDescription": "Trừ một khoản âm khỏi đơn hàng" + }, + "balanceSettlement": { + "title": "Thanh toán số dư", + "settlementType": "Loại thanh toán", + "settlementTypes": { + "paymentMethod": "Phương thức thanh toán", + "paymentMethodDescription": "Hoàn tiền vào phương thức thanh toán", + "creditLine": "Tín dụng cửa hàng", + "creditLineDescription": "Hoàn tiền dưới dạng tín dụng cửa hàng" + } + }, "domain": "Đơn hàng", "claim": "Khiếu nại", "exchange": "Đổi hàng", @@ -1050,6 +1081,7 @@ "title": "Thanh toán", "isReadyToBeCaptured": "Thanh toán <0/> đã sẵn sàng để ghi nhận.", "totalPaidByCustomer": "Tổng số tiền khách hàng đã thanh toán", + "totalStoreCreditRefunds": "Tổng số tiền hoàn lại cho cửa hàng", "capture": "Ghi nhận thanh toán", "capture_short": "Ghi nhận", "refund": "Hoàn tiền", @@ -1133,7 +1165,7 @@ "sendNotificationHint": "Thông báo cho khách hàng về việc trả hàng.", "returnTotal": "Tổng trả hàng", "inboundTotal": "Tổng hàng trả về", - "refundAmount": "Số tiền hoàn lại", + "estDifference": "Chênh lệch ước tính", "outstandingAmount": "Số tiền còn lại", "reason": "Lý do", "reasonHint": "Chọn lý do khách hàng muốn trả hàng.", @@ -1712,11 +1744,16 @@ "header": "Tạo khu vực thuế", "hint": "Tạo một khu vực thuế mới để xác định thuế suất cho một quốc gia cụ thể.", "errors": { - "rateIsRequired": "Thuế suất là bắt buộc khi tạo thuế suất mặc định.", - "nameIsRequired": "Tên là bắt buộc khi tạo thuế suất mặc định." + "missingProvider": "Nhà cung cấp là bắt buộc khi tạo khu vực thuế.", + "missingCountry": "Quốc gia là bắt buộc khi tạo khu vực thuế." }, "successToast": "Khu vực thuế đã được tạo thành công." }, + "edit": { + "header": "Chỉnh sửa khu vực thuế", + "hint": "Chỉnh sửa thông tin chi tiết về khu vực thuế.", + "successToast": "Khu vực thuế đã được cập nhật thành công." + }, "province": { "header": "Tỉnh/Thành", "create": { @@ -1724,6 +1761,9 @@ "hint": "Tạo một khu vực thuế mới để xác định thuế suất cho một tỉnh/thành cụ thể." } }, + "provider": { + "header": "Nhà cung cấp thuế" + }, "state": { "header": "Bang", "create": { @@ -1849,6 +1889,7 @@ }, "taxRate": "Thuế suất", "taxCode": "Mã thuế", + "taxProvider": "Nhà cung cấp thuế", "targets": { "label": "Đối tượng áp dụng", "hint": "Chọn các đối tượng mà thuế suất này sẽ áp dụng.", @@ -1954,6 +1995,7 @@ "allocation": "Phân bổ", "addCondition": "Thêm điều kiện", "clearAll": "Xóa tất cả", + "taxInclusive": "Bao gồm thuế", "amount": { "tooltip": "Chọn mã tiền tệ để kích hoạt cài đặt số tiền" }, @@ -2030,6 +2072,10 @@ "description": "Tiếp tục mà không liên kết khuyến mãi với chiến dịch" } }, + "taxInclusive": { + "title": "Khuyến mãi có bao gồm thuế?", + "description": "Bật để áp dụng khuyến mãi sau thuế" + }, "status": { "label": "Trạng thái", "draft": { @@ -2465,7 +2511,6 @@ "createTaxRate": "Tạo thuế suất", "createTaxRateHint": "Tạo một thuế suất mới cho khu vực.", "deleteRateDescription": "Bạn sắp xóa thuế suất {{name}}. Hành động này không thể hoàn tác.", - "editTaxRate": "Chỉnh sửa thuế suất", "editRateAction": "Chỉnh sửa thuế suất", "editOverridesAction": "Chỉnh sửa ghi đè", "editOverridesTitle": "Chỉnh sửa ghi đè thuế suất", @@ -2828,6 +2873,8 @@ }, "fields": { "amount": "Số tiền", + "reference": "Tham chiếu", + "reference_id": "Mã tham chiếu", "refundAmount": "Số tiền hoàn lại", "name": "Tên", "default": "Mặc định", @@ -2914,6 +2961,7 @@ "account": "Tài khoản", "total": "Tổng đơn hàng", "paidTotal": "Tổng đã ghi nhận", + "creditTotal": "Tổng hạn mức tín dụng", "totalExclTax": "Tổng chưa gồm thuế", "subtotal": "Tạm tính", "shipping": "Vận chuyển", diff --git a/packages/admin/dashboard/src/lib/data/country-states.ts b/packages/admin/dashboard/src/lib/data/country-states.ts index cbd5c0b73476d..d0417b30eaa22 100644 --- a/packages/admin/dashboard/src/lib/data/country-states.ts +++ b/packages/admin/dashboard/src/lib/data/country-states.ts @@ -1227,6 +1227,7 @@ const countryProvinceMap: Record = { "US-CA": "California", "US-CO": "Colorado", "US-CT": "Connecticut", + "US-DE": "Delaware", "US-FL": "Florida", "US-GA": "Georgia", "US-HI": "Hawaii", @@ -1244,8 +1245,10 @@ const countryProvinceMap: Record = { "US-MN": "Minnesota", "US-MS": "Mississippi", "US-MO": "Missouri", + "US-MT": "Montana", "US-NE": "Nebraska", "US-NV": "Nevada", + "US-NH": "New Hampshire", "US-NJ": "New Jersey", "US-NM": "New Mexico", "US-NY": "New York", @@ -1253,6 +1256,7 @@ const countryProvinceMap: Record = { "US-ND": "North Dakota", "US-OH": "Ohio", "US-OK": "Oklahoma", + "US-OR": "Oregon", "US-PA": "Pennsylvania", "US-PR": "Puerto Rico", "US-RI": "Rhode Island", @@ -1268,6 +1272,11 @@ const countryProvinceMap: Record = { "US-WV": "West Virginia", "US-WI": "Wisconsin", "US-WY": "Wyoming", + "US-AS": "American Samoa", + "US-GU": "Guam", + "US-MP": "Northern Mariana Islands", + "US-VI": "U.S. Virgin Islands", + "US-UM": "United States Minor Outlying Islands", }, }, VE: { diff --git a/packages/admin/dashboard/src/lib/orders.ts b/packages/admin/dashboard/src/lib/orders.ts index 39ef3813534bc..2bf7c42cb2aa3 100644 --- a/packages/admin/dashboard/src/lib/orders.ts +++ b/packages/admin/dashboard/src/lib/orders.ts @@ -1,4 +1,4 @@ -import { HttpTypes } from "@medusajs/types" +import { AdminOrder, AdminOrderLineItem, HttpTypes } from "@medusajs/types" export const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => { return order.payment_collections @@ -6,3 +6,18 @@ export const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => { .flat(1) .filter(Boolean) as HttpTypes.AdminPayment[] } + +/** + * Returns a limit for number of reservations that order can have. + */ +export function getReservationsLimitCount(order: AdminOrder) { + if (!order?.items?.length) { + return 0 + } + + return order.items.reduce( + (acc: number, item: AdminOrderLineItem) => + acc + (item.variant?.inventory_items?.length || 1), + 0 + ) +} diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx index 4be44b2b93d1c..2fef4a41e85b9 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx @@ -111,8 +111,8 @@ export const EditShippingOptionForm = ({ return ( - - + +
{!isPickup && ( diff --git a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx index 91b14ec75d105..0df6dc9d4a6ca 100644 --- a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx @@ -23,6 +23,7 @@ import { useReservationItems, useShippingOptions, } from "../../../../../hooks/api" +import { getReservationsLimitCount } from "../../../../../lib/orders" type OrderCreateFulfillmentFormProps = { order: AdminOrder @@ -41,6 +42,7 @@ export function OrderCreateFulfillmentForm({ const { reservations } = useReservationItems({ line_item_id: order.items.map((i) => i.id), + limit: getReservationsLimitCount(order), }) const itemReservedQuantitiesMap = useMemo( diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 84cc7a3dd5bcc..90d0c7d01f3ef 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -51,6 +51,7 @@ import { useReturns } from "../../../../../hooks/api/returns" import { useDate } from "../../../../../hooks/use-date" import { getTotalCreditLines } from "../../../../../lib/credit-line" import { formatCurrency } from "../../../../../lib/format-currency" +import { getReservationsLimitCount } from "../../../../../lib/orders" import { getLocaleAmount, getStylizedAmount, @@ -78,6 +79,7 @@ export const OrderSummarySection = ({ const { reservations } = useReservationItems( { line_item_id: order?.items?.map((i) => i.id), + limit: getReservationsLimitCount(order), }, { enabled: Array.isArray(order?.items) } ) diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts index bd4668626c065..1162a331813a2 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts +++ b/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts @@ -36,7 +36,7 @@ const DEFAULT_RELATIONS = [ "*shipping_address", "*billing_address", "*sales_channel", - "*promotion", + "*promotions", "*shipping_methods", "*credit_lines", "*fulfillments", diff --git a/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx b/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx index 87a4d4b36c430..e2857f9b27cf3 100644 --- a/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx +++ b/packages/admin/dashboard/src/routes/products/product-attributes/components/product-attributes-form/product-attributes-form.tsx @@ -68,10 +68,10 @@ export const ProductAttributesForm = ({ const handleSubmit = form.handleSubmit(async (data) => { await mutateAsync( { - weight: data.weight ? data.weight : undefined, - length: data.length ? data.length : undefined, - width: data.width ? data.width : undefined, - height: data.height ? data.height : undefined, + weight: data.weight ? data.weight : null, + length: data.length ? data.length : null, + width: data.width ? data.width : null, + height: data.height ? data.height : null, mid_code: data.mid_code, hs_code: data.hs_code, origin_country: data.origin_country, diff --git a/packages/admin/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx b/packages/admin/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx index 12039625e3608..d0797fc68964a 100644 --- a/packages/admin/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx +++ b/packages/admin/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx @@ -79,8 +79,8 @@ export const EditRegionForm = ({ return ( - - + +
{ return ( - - + +
{ + // We don't allow projects to have a dot in the name, as this causes issues for + // for MikroORM path resolutions. + if (input.includes(".")) { + return `Project names cannot contain a dot (.) character. Please enter a different ${ + isPlugin ? "plugin" : "project" + } name.` + } + if (!input.length) { return `Please enter a ${isPlugin ? "plugin" : "project"} name` } diff --git a/packages/cli/medusa-cli/CHANGELOG.md b/packages/cli/medusa-cli/CHANGELOG.md index d43213e9cebea..54892da8d3f21 100644 --- a/packages/cli/medusa-cli/CHANGELOG.md +++ b/packages/cli/medusa-cli/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47)]: + - @medusajs/utils@2.8.8 + - @medusajs/telemetry@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/cli/medusa-cli/package.json b/packages/cli/medusa-cli/package.json index 0c9cfdc6e58d0..f783b0e86e32a 100644 --- a/packages/cli/medusa-cli/package.json +++ b/packages/cli/medusa-cli/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/cli", - "version": "2.8.7", + "version": "2.8.8", "description": "Command Line interface for Medusa Commerce", "main": "dist/index.js", "bin": { @@ -46,8 +46,8 @@ "typescript": "^5.6.2" }, "dependencies": { - "@medusajs/telemetry": "2.8.7", - "@medusajs/utils": "2.8.7", + "@medusajs/telemetry": "2.8.8", + "@medusajs/utils": "2.8.8", "@types/express": "^4.17.17", "chalk": "^4.0.0", "configstore": "5.0.1", diff --git a/packages/cli/medusa-dev-cli/CHANGELOG.md b/packages/cli/medusa-dev-cli/CHANGELOG.md index 277dc3a6b4244..6ea8e931d9f5f 100644 --- a/packages/cli/medusa-dev-cli/CHANGELOG.md +++ b/packages/cli/medusa-dev-cli/CHANGELOG.md @@ -1,5 +1,7 @@ # Change Log +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/cli/medusa-dev-cli/package.json b/packages/cli/medusa-dev-cli/package.json index 90a5a5f3cac39..7061704407652 100644 --- a/packages/cli/medusa-dev-cli/package.json +++ b/packages/cli/medusa-dev-cli/package.json @@ -1,7 +1,7 @@ { "name": "medusa-dev-cli", "description": "CLI helpers for contributors working on Medusa", - "version": "2.8.7", + "version": "2.8.8", "author": "Sebastian Rindom ", "bin": { "medusa-dev": "./dist/index.js" diff --git a/packages/cli/oas/medusa-oas-cli/CHANGELOG.md b/packages/cli/oas/medusa-oas-cli/CHANGELOG.md index 23c382de43a16..670eaa2a967e2 100644 --- a/packages/cli/oas/medusa-oas-cli/CHANGELOG.md +++ b/packages/cli/oas/medusa-oas-cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @medusajs/oas-cli +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47)]: + - @medusajs/utils@2.8.8 + - @medusajs/medusa@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/cli/oas/medusa-oas-cli/package.json b/packages/cli/oas/medusa-oas-cli/package.json index a1d888f28c659..723eedc55f79d 100644 --- a/packages/cli/oas/medusa-oas-cli/package.json +++ b/packages/cli/oas/medusa-oas-cli/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa-oas-cli", - "version": "2.8.7", + "version": "2.8.8", "description": "OAS CLI", "main": "dist/index.js", "bin": { @@ -21,7 +21,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/types": "2.8.7", + "@medusajs/types": "2.8.8", "@types/js-yaml": "^4.0.9", "jest": "^29.1.0", "js-yaml": "^4.1.0", @@ -35,8 +35,8 @@ "medusa-oas": "ts-node src/index.ts" }, "dependencies": { - "@medusajs/medusa": "2.8.7", - "@medusajs/utils": "2.8.7", + "@medusajs/medusa": "2.8.8", + "@medusajs/utils": "2.8.8", "@readme/json-schema-ref-parser": "^1.2.0", "@readme/openapi-parser": "^2.4.0", "@redocly/cli": "^1.7.0", diff --git a/packages/cli/oas/oas-github-ci/CHANGELOG.md b/packages/cli/oas/oas-github-ci/CHANGELOG.md index 8786f33760a1f..34d57b00ac0cd 100644 --- a/packages/cli/oas/oas-github-ci/CHANGELOG.md +++ b/packages/cli/oas/oas-github-ci/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/oas-github-ci +## 2.8.8 + +### Patch Changes + +- Updated dependencies []: + - @medusajs/medusa-oas-cli@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/cli/oas/oas-github-ci/package.json b/packages/cli/oas/oas-github-ci/package.json index efb0f1345b251..00e8a2082bf66 100644 --- a/packages/cli/oas/oas-github-ci/package.json +++ b/packages/cli/oas/oas-github-ci/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/oas-github-ci", - "version": "2.8.7", + "version": "2.8.8", "description": "OAS Github CI", "main": "scripts/build-openapi.js", "files": [ @@ -21,7 +21,7 @@ "test": "jest --passWithNoTests" }, "dependencies": { - "@medusajs/medusa-oas-cli": "2.8.7", + "@medusajs/medusa-oas-cli": "2.8.8", "execa": "^5.1.1" } } diff --git a/packages/core/core-flows/CHANGELOG.md b/packages/core/core-flows/CHANGELOG.md index 9c942b7ae5b86..caa2b00ef5041 100644 --- a/packages/core/core-flows/CHANGELOG.md +++ b/packages/core/core-flows/CHANGELOG.md @@ -1,5 +1,22 @@ # @medusajs/core-flows +## 2.8.8 + +### Patch Changes + +- [#12919](https://github.com/medusajs/medusa/pull/12919) [`a28226af80a8880afbdb926a5001f0cb0d89fdc9`](https://github.com/medusajs/medusa/commit/a28226af80a8880afbdb926a5001f0cb0d89fdc9) Thanks [@fPolic](https://github.com/fPolic)! - fix(core-flows): updating tax lines when draft order shipping is removed + +- [#12958](https://github.com/medusajs/medusa/pull/12958) [`40625c82d6deb28ecb4e4c0f911c28fdd7356bf7`](https://github.com/medusajs/medusa/commit/40625c82d6deb28ecb4e4c0f911c28fdd7356bf7) Thanks [@fPolic](https://github.com/fPolic)! - fix(core-flows): correctly delete related inventory item when deleting variant + +- [#12920](https://github.com/medusajs/medusa/pull/12920) [`8c4228fc42e717f9ab72230040e708f606a585b7`](https://github.com/medusajs/medusa/commit/8c4228fc42e717f9ab72230040e708f606a585b7) Thanks [@riqwan](https://github.com/riqwan)! - fix(link-modules,core-flows): Carry over cart promotions to order promotions + +- [#12969](https://github.com/medusajs/medusa/pull/12969) [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47) Thanks [@juanzgc](https://github.com/juanzgc)! - fix: accepted values in import with template + +- [#12962](https://github.com/medusajs/medusa/pull/12962) [`2c2528a08751945d6f0363473605f1a8ef1a8a2a`](https://github.com/medusajs/medusa/commit/2c2528a08751945d6f0363473605f1a8ef1a8a2a) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(core-flows): useQueryGraph util return type + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/core-flows/package.json b/packages/core/core-flows/package.json index 0933149681eeb..fa78fcea8d991 100644 --- a/packages/core/core-flows/package.json +++ b/packages/core/core-flows/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/core-flows", - "version": "2.8.7", + "version": "2.8.8", "description": "Set of workflow definitions for Medusa", "main": "dist/index.js", "exports": { @@ -26,7 +26,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -45,7 +45,7 @@ "json-2-csv": "^5.5.4" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" }, "scripts": { diff --git a/packages/core/core-flows/src/cart/steps/find-or-create-customer.ts b/packages/core/core-flows/src/cart/steps/find-or-create-customer.ts index b6ed2100ec3d1..cc075d89e7cc1 100644 --- a/packages/core/core-flows/src/cart/steps/find-or-create-customer.ts +++ b/packages/core/core-flows/src/cart/steps/find-or-create-customer.ts @@ -39,12 +39,12 @@ interface StepCompensateInput { export const findOrCreateCustomerStepId = "find-or-create-customer" /** * This step finds or creates a customer based on the provided ID or email. It prioritizes finding the customer by ID, then by email. - * + * * The step creates a new customer either if: - * + * * - No customer is found with the provided ID and email; * - Or if it found the customer by ID but their email does not match the email in the input. - * + * * The step returns the details of the customer found or created, along with their email. */ export const findOrCreateCustomerStep = createStep( @@ -96,10 +96,14 @@ export const findOrCreateCustomerStep = createStep( }) } - if ( - !customer || - (isDefined(data.email) && customer.email !== validatedEmail) - ) { + if (customer && customer.email !== validatedEmail) { + ;[customer] = await service.listCustomers({ + email: validatedEmail, + has_account: false, + }) + } + + if (!customer) { customer = await service.createCustomers({ email: validatedEmail }) customerWasCreated = true } diff --git a/packages/core/core-flows/src/cart/steps/get-promotion-codes-to-apply.ts b/packages/core/core-flows/src/cart/steps/get-promotion-codes-to-apply.ts index 4de4ab53fb959..9b554e707d814 100644 --- a/packages/core/core-flows/src/cart/steps/get-promotion-codes-to-apply.ts +++ b/packages/core/core-flows/src/cart/steps/get-promotion-codes-to-apply.ts @@ -1,6 +1,10 @@ import { IPromotionModuleService } from "@medusajs/framework/types" -import { Modules, PromotionActions } from "@medusajs/framework/utils" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { + MedusaError, + Modules, + PromotionActions, +} from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" /** * The details of the promotion codes to apply on a cart. @@ -68,14 +72,12 @@ export const getPromotionCodesToApply = createStep( async (data: GetPromotionCodesToApplyStepInput, { container }) => { const { promo_codes = [], cart, action = PromotionActions.ADD } = data const { items = [], shipping_methods = [] } = cart - const adjustmentCodes: string[] = [] const promotionService = container.resolve( Modules.PROMOTION ) - const objects = items.concat(shipping_methods) - - objects.forEach((object) => { + const adjustmentCodes: string[] = [] + items.concat(shipping_methods).forEach((object) => { object.adjustments?.forEach((adjustment) => { if (adjustment.code && !adjustmentCodes.includes(adjustment.code)) { adjustmentCodes.push(adjustment.code) @@ -94,17 +96,38 @@ export const getPromotionCodesToApply = createStep( : [] ) - if (action === PromotionActions.ADD) { - promo_codes.forEach((code) => promotionCodesToApply.add(code)) - } - if (action === PromotionActions.REMOVE) { promo_codes.forEach((code) => promotionCodesToApply.delete(code)) } if (action === PromotionActions.REPLACE) { promotionCodesToApply.clear() - promo_codes.forEach((code) => promotionCodesToApply.add(code)) + } + + if ( + action === PromotionActions.ADD || + action === PromotionActions.REPLACE + ) { + const validPromoCodes: Set = new Set( + promo_codes.length + ? ( + await promotionService.listPromotions( + { code: promo_codes }, + { select: ["code"] } + ) + ).map((p) => p.code!) + : [] + ) + + promo_codes.forEach((code) => { + if (!validPromoCodes.has(code)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The promotion code ${code} is invalid` + ) + } + promotionCodesToApply.add(code) + }) } return new StepResponse( diff --git a/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts b/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts index 6eb15c0295495..e3c0d82c56b5e 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts @@ -120,7 +120,7 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow( return { selector: { id: cart.payment_collection.id }, update: { - amount: cart.total, + amount: cart.raw_total, currency_code: cart.currency_code, }, } diff --git a/packages/core/core-flows/src/common/steps/use-query-graph.ts b/packages/core/core-flows/src/common/steps/use-query-graph.ts index 6324d755b9bad..f9faf00bd4cc9 100644 --- a/packages/core/core-flows/src/common/steps/use-query-graph.ts +++ b/packages/core/core-flows/src/common/steps/use-query-graph.ts @@ -7,9 +7,10 @@ import { import { createStep, StepFunction, StepResponse } from "@medusajs/workflows-sdk" import { ContainerRegistrationKeys } from "@medusajs/utils" -export type UseQueryGraphStepInput = RemoteQueryInput & { - options?: RemoteJoinerOptions -} +export type UseQueryGraphStepInput = + RemoteQueryInput & { + options?: RemoteJoinerOptions + } const useQueryGraphStepId = "use-query-graph-step" @@ -30,7 +31,7 @@ const step = createStep( * This step fetches data across modules using the Query. * * Learn more in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query). - * + * * @example * To retrieve a list of records of a data model: * @@ -101,5 +102,7 @@ const step = createStep( */ export const useQueryGraphStep = ( input: UseQueryGraphStepInput -): ReturnType, GraphResultSet>> => - step(input as any) as unknown as ReturnType, GraphResultSet>> +): ReturnType>> => + step(input as any) as unknown as ReturnType< + StepFunction> + > diff --git a/packages/core/core-flows/src/customer-group/steps/create-customer-groups.ts b/packages/core/core-flows/src/customer-group/steps/create-customer-groups.ts index 1b0dd3ed92c6c..837aeb84ec2e5 100644 --- a/packages/core/core-flows/src/customer-group/steps/create-customer-groups.ts +++ b/packages/core/core-flows/src/customer-group/steps/create-customer-groups.ts @@ -35,6 +35,6 @@ export const createCustomerGroupsStep = createStep( const service = container.resolve(Modules.CUSTOMER) - await service.deleteCustomers(createdCustomerGroupIds) + await service.deleteCustomerGroups(createdCustomerGroupIds) } ) diff --git a/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-shipping-method.ts b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-shipping-method.ts index f0e848cb37431..62585d0293a48 100644 --- a/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-shipping-method.ts +++ b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-shipping-method.ts @@ -15,7 +15,6 @@ import { useRemoteQueryStep } from "../../common" import { createOrderChangeActionsWorkflow, previewOrderChangeStep, - updateOrderTaxLinesWorkflow, } from "../../order" import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" import { draftOrderFieldsForRefreshSteps } from "../utils/fields" @@ -87,12 +86,6 @@ export const removeDraftOrderShippingMethodWorkflow = createWorkflow( validateDraftOrderChangeStep({ order, orderChange }) - updateOrderTaxLinesWorkflow.runAsStep({ - input: { - order_id: order.id, - }, - }) - const appliedPromoCodes: string[] = transform( order, (order) => order.promotions?.map((promotion) => promotion.code) ?? [] diff --git a/packages/core/core-flows/src/order/utils/aggregate-status.ts b/packages/core/core-flows/src/order/utils/aggregate-status.ts index 94abf727ae29b..d9afd14be0482 100644 --- a/packages/core/core-flows/src/order/utils/aggregate-status.ts +++ b/packages/core/core-flows/src/order/utils/aggregate-status.ts @@ -26,7 +26,7 @@ export const getLastPaymentStatus = (order: OrderDetailDTO) => { (isDefined(paymentCollection.amount) && MathBN.eq(paymentCollection.amount, 0)) ) { - paymentStatus[PaymentStatus.CAPTURED] += MathBN.eq( + paymentStatus[PaymentStatus.CAPTURED] += MathBN.gte( paymentCollection.captured_amount as number, paymentCollection.amount ) @@ -35,7 +35,7 @@ export const getLastPaymentStatus = (order: OrderDetailDTO) => { } if (MathBN.gt(paymentCollection.refunded_amount ?? 0, 0)) { - paymentStatus[PaymentStatus.REFUNDED] += MathBN.eq( + paymentStatus[PaymentStatus.REFUNDED] += MathBN.gte( paymentCollection.refunded_amount as number, paymentCollection.amount ) diff --git a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts index d78e0760c5d66..1176eed94cdbb 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/order-edit-update-item-quantity.ts @@ -79,6 +79,8 @@ export const orderEditUpdateItemQuantityWorkflowId = * This workflow updates the quantity of an existing item in an order's edit. It's used by the * [Update Order Item Quantity Admin API Route](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsitemitem_id). * + * You can also use this workflow to remove an item from an order by setting its quantity to `0`. + * * You can use this workflow within your customizations or your own custom workflows, allowing you to update the quantity of an existing * item in an order's edit in your custom flow. * @@ -98,7 +100,7 @@ export const orderEditUpdateItemQuantityWorkflowId = * * @summary * - * Update the quantity of an existing order item in the order's edit. + * Update or remove an existing order item's quantity in the order's edit. */ export const orderEditUpdateItemQuantityWorkflow = createWorkflow( orderEditUpdateItemQuantityWorkflowId, diff --git a/packages/core/core-flows/src/order/workflows/return/cancel-return.ts b/packages/core/core-flows/src/order/workflows/return/cancel-return.ts index fbcff4e292dc7..7cf2d6e5954ee 100644 --- a/packages/core/core-flows/src/order/workflows/return/cancel-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/cancel-return.ts @@ -32,14 +32,14 @@ export type CancelReturnValidateOrderInput = { * This step validates that a return can be canceled. * If the return is canceled, its fulfillment aren't canceled, * or it has received items, the step will throw an error. - * + * * :::note - * + * * You can retrieve a return details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = cancelReturnValidateOrder({ * orderReturn: { @@ -53,9 +53,7 @@ export type CancelReturnValidateOrderInput = { */ export const cancelReturnValidateOrder = createStep( "validate-return", - ({ - orderReturn, - }: CancelReturnValidateOrderInput) => { + ({ orderReturn }: CancelReturnValidateOrderInput) => { const orderReturn_ = orderReturn as ReturnDTO & { payment_collections: PaymentCollectionDTO[] fulfillments: FulfillmentDTO[] @@ -92,12 +90,12 @@ export const cancelReturnValidateOrder = createStep( export const cancelReturnWorkflowId = "cancel-return" /** - * This workflow cancels a return. It's used by the + * This workflow cancels a return. It's used by the * [Cancel Return Admin API Route](https://docs.medusajs.com/api/admin#returns_postreturnsidcancel). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you * to cancel a return in your custom flow. - * + * * @example * const { result } = await cancelReturnWorkflow(container) * .run({ @@ -105,9 +103,9 @@ export const cancelReturnWorkflowId = "cancel-return" * return_id: "return_123", * } * }) - * + * * @summary - * + * * Cancel a return. */ export const cancelReturnWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/product/steps/generate-product-csv.ts b/packages/core/core-flows/src/product/steps/generate-product-csv.ts index f025c5ec45b27..dd1633a82ec92 100644 --- a/packages/core/core-flows/src/product/steps/generate-product-csv.ts +++ b/packages/core/core-flows/src/product/steps/generate-product-csv.ts @@ -12,13 +12,22 @@ const prodColumnPositions = new Map([ ["Product Id", 0], ["Product Handle", 1], ["Product Title", 2], - ["Product Status", 3], + ["Product Subtitle", 3], ["Product Description", 4], - ["Product Subtitle", 5], - ["Product External Id", 6], - ["Product Thumbnail", 7], - ["Product Collection Id", 8], - ["Product Type Id", 9], + ["Product Status", 5], + ["Product Thumbnail", 6], + ["Product Weight", 7], + ["Product Length", 8], + ["Product Width", 9], + ["Product Height", 10], + ["Product HS Code", 11], + ["Product Origin Country", 12], + ["Product MID Code", 13], + ["Product Material", 14], + ["Product Collection Id", 15], + ["Product Type Id", 16], + ["Product Discountable", 17], + ["Product External Id", 18], ]) const variantColumnPositions = new Map([ diff --git a/packages/core/core-flows/src/product/steps/update-products.ts b/packages/core/core-flows/src/product/steps/update-products.ts index 599625185f3ca..d498686f379ab 100644 --- a/packages/core/core-flows/src/product/steps/update-products.ts +++ b/packages/core/core-flows/src/product/steps/update-products.ts @@ -11,9 +11,9 @@ import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" */ export type UpdateProductsStepInput = | { - /** - * The filters to select the products to update. - */ + /** + * The filters to select the products to update. + */ selector: ProductTypes.FilterableProductProps /** * The data to update the products with. @@ -21,19 +21,19 @@ export type UpdateProductsStepInput = update: ProductTypes.UpdateProductDTO } | { - /** - * The data to create or update products. - */ + /** + * The data to create or update products. + */ products: ProductTypes.UpsertProductDTO[] } export const updateProductsStepId = "update-products" /** * This step updates one or more products. - * + * * @example * To update products by their ID: - * + * * ```ts * const data = updateProductsStep({ * products: [ @@ -44,9 +44,9 @@ export const updateProductsStepId = "update-products" * ] * }) * ``` - * + * * To update products matching a filter: - * + * * ```ts * const data = updateProductsStep({ * selector: { diff --git a/packages/core/core-flows/src/product/workflows/create-product-variants.ts b/packages/core/core-flows/src/product/workflows/create-product-variants.ts index e09d7491791d8..6086c537f066e 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-variants.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-variants.ts @@ -26,9 +26,9 @@ import { createProductVariantsStep } from "../steps/create-product-variants" import { createVariantPricingLinkStep } from "../steps/create-variant-pricing-link" /** - * + * * The data to create one or more product variants, along with custom data that's passed to the workflow's hooks. - * + * * @privateRemarks * TODO: Create separate typings for the workflow input */ @@ -51,9 +51,9 @@ export type CreateProductVariantsWorkflowInput = { */ inventory_item_id: string /** - * The number of units a single quantity is equivalent to. For example, if a customer orders one quantity of the variant, - * Medusa checks the availability of the quantity multiplied by the value set for `required_quantity`. - * When the customer orders the quantity, Medusa reserves the ordered quantity multiplied by the value + * The number of units a single quantity is equivalent to. For example, if a customer orders one quantity of the variant, + * Medusa checks the availability of the quantity multiplied by the value set for `required_quantity`. + * When the customer orders the quantity, Medusa reserves the ordered quantity multiplied by the value * set for `required_quantity`. */ required_quantity?: number @@ -201,19 +201,19 @@ const buildVariantItemCreateMap = (data: { export const createProductVariantsWorkflowId = "create-product-variants" /** * This workflow creates one or more product variants. It's used by the [Create Product Variant Admin API Route](https://docs.medusajs.com/api/admin#products_postproductsidvariants). - * - * This workflow has a hook that allows you to perform custom actions on the created product variants. For example, you can pass under `additional_data` custom data that + * + * This workflow has a hook that allows you to perform custom actions on the created product variants. For example, you can pass under `additional_data` custom data that * allows you to create custom data models linked to the product variants. - * + * * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product-variant creation. - * + * * :::note - * - * Learn more about adding rules to the product variant's prices in the Pricing Module's + * + * Learn more about adding rules to the product variant's prices in the Pricing Module's * [Price Rules](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules) documentation. - * + * * ::: - * + * * @example * const { result } = await createProductVariantsWorkflow(container) * .run({ @@ -239,11 +239,11 @@ export const createProductVariantsWorkflowId = "create-product-variants" * } * } * }) - * + * * @summary - * + * * Create one or more product variants. - * + * * @property hooks.productVariantsCreated - This hook is executed after the product variants are created. You can consume this hook to perform custom actions on the created product variants. */ export const createProductVariantsWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/product/workflows/create-products.ts b/packages/core/core-flows/src/product/workflows/create-products.ts index 5d3e94eccb98a..b472585853124 100644 --- a/packages/core/core-flows/src/product/workflows/create-products.ts +++ b/packages/core/core-flows/src/product/workflows/create-products.ts @@ -144,10 +144,10 @@ export const createProductsWorkflowId = "create-products" * ], * manage_inventory: true, * }, - * ] + * ], + * shipping_profile_id: "sp_123", * } * ], - * shipping_profile_id: "sp_123", * additional_data: { * erp_id: "123" * } diff --git a/packages/core/core-flows/src/product/workflows/delete-product-variants.ts b/packages/core/core-flows/src/product/workflows/delete-product-variants.ts index 2e0572b706b84..5e6d53c783fe3 100644 --- a/packages/core/core-flows/src/product/workflows/delete-product-variants.ts +++ b/packages/core/core-flows/src/product/workflows/delete-product-variants.ts @@ -20,7 +20,7 @@ import { deleteInventoryItemWorkflow } from "../../inventory" /** * The data to delete one or more product variants. */ -export type DeleteProductVariantsWorkflowInput = { +export type DeleteProductVariantsWorkflowInput = { /** * The IDs of the variants to delete. */ @@ -29,14 +29,14 @@ export type DeleteProductVariantsWorkflowInput = { export const deleteProductVariantsWorkflowId = "delete-product-variants" /** - * This workflow deletes one or more product variants. It's used by the + * This workflow deletes one or more product variants. It's used by the * [Delete Product Variants Admin API Route](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_id). - * - * This workflow has a hook that allows you to perform custom actions after the product variants are deleted. For example, + * + * This workflow has a hook that allows you to perform custom actions after the product variants are deleted. For example, * you can delete custom records linked to the product variants. - * + * * You can also use this workflow within your own custom workflows, allowing you to wrap custom logic around product-variant deletion. - * + * * @example * const { result } = await deleteProductVariantsWorkflow(container) * .run({ @@ -44,20 +44,16 @@ export const deleteProductVariantsWorkflowId = "delete-product-variants" * ids: ["variant_123"], * } * }) - * + * * @summary - * + * * Delete one or more product variants. - * + * * @property hooks.productVariantsDeleted - This hook is executed after the variants are deleted. You can consume this hook to perform custom actions on the deleted variants. */ export const deleteProductVariantsWorkflow = createWorkflow( deleteProductVariantsWorkflowId, (input: WorkflowData) => { - removeRemoteLinkStep({ - [Modules.PRODUCT]: { variant_id: input.ids }, - }).config({ name: "remove-variant-link-step" }) - const variantsWithInventoryStepResponse = useQueryGraphStep({ entity: "variants", fields: [ @@ -71,6 +67,10 @@ export const deleteProductVariantsWorkflow = createWorkflow( }, }) + removeRemoteLinkStep({ + [Modules.PRODUCT]: { variant_id: input.ids }, + }).config({ name: "remove-variant-link-step" }) + const toDeleteInventoryItemIds = transform( { variants: variantsWithInventoryStepResponse.data }, (data) => { diff --git a/packages/core/core-flows/src/product/workflows/update-products.ts b/packages/core/core-flows/src/product/workflows/update-products.ts index 94a37dea50fc0..2259eb9b2f95d 100644 --- a/packages/core/core-flows/src/product/workflows/update-products.ts +++ b/packages/core/core-flows/src/product/workflows/update-products.ts @@ -343,12 +343,12 @@ export const updateProductsWorkflowId = "update-products" * allows you to update custom data models linked to the products. * * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product update. - * + * * :::note - * - * Learn more about adding rules to the product variant's prices in the Pricing Module's + * + * Learn more about adding rules to the product variant's prices in the Pricing Module's * [Price Rules](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules) documentation. - * + * * ::: * * @example diff --git a/packages/core/framework/CHANGELOG.md b/packages/core/framework/CHANGELOG.md index 5dfd352ba757c..7b02434b212df 100644 --- a/packages/core/framework/CHANGELOG.md +++ b/packages/core/framework/CHANGELOG.md @@ -1,5 +1,20 @@ # @medusajs/framework +## 2.8.8 + +### Patch Changes + +- [#13007](https://github.com/medusajs/medusa/pull/13007) [`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7) Thanks [@peterlgh7](https://github.com/peterlgh7)! - fix(framework): fix script migrations order + +- Updated dependencies [[`e74044af4de213ba6326a8fb2b5d02e5875b8a4a`](https://github.com/medusajs/medusa/commit/e74044af4de213ba6326a8fb2b5d02e5875b8a4a), [`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5), [`1bd455bc7bd32b0fda8c808b203ad341253f095d`](https://github.com/medusajs/medusa/commit/1bd455bc7bd32b0fda8c808b203ad341253f095d), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47), [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5)]: + - @medusajs/orchestration@2.8.8 + - @medusajs/types@2.8.8 + - @medusajs/workflows-sdk@2.8.8 + - @medusajs/utils@2.8.8 + - @medusajs/modules-sdk@2.8.8 + - @medusajs/cli@2.8.8 + - @medusajs/telemetry@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/framework/package.json b/packages/core/framework/package.json index 65bfa41786ebb..f3e9f3cfc825a 100644 --- a/packages/core/framework/package.json +++ b/packages/core/framework/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/framework", - "version": "2.8.7", + "version": "2.8.8", "description": "Framework", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -55,7 +55,7 @@ }, "devDependencies": { "@aws-sdk/client-dynamodb": "^3.218.0", - "@medusajs/cli": "2.8.7", + "@medusajs/cli": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -76,12 +76,12 @@ }, "dependencies": { "@jercle/yargonaut": "^1.1.5", - "@medusajs/modules-sdk": "2.8.7", - "@medusajs/orchestration": "2.8.7", - "@medusajs/telemetry": "2.8.7", - "@medusajs/types": "2.8.7", - "@medusajs/utils": "2.8.7", - "@medusajs/workflows-sdk": "2.8.7", + "@medusajs/modules-sdk": "2.8.8", + "@medusajs/orchestration": "2.8.8", + "@medusajs/telemetry": "2.8.8", + "@medusajs/types": "2.8.8", + "@medusajs/utils": "2.8.8", + "@medusajs/workflows-sdk": "2.8.8", "@opentelemetry/api": "^1.9.0", "@types/express": "^4.17.17", "chokidar": "^3.4.2", @@ -97,12 +97,12 @@ "morgan": "^1.9.1", "path-to-regexp": "^0.1.10", "tsconfig-paths": "^4.2.0", - "zod": "3.22.4", - "zod-validation-error": "^3.4.1" + "zod": "3.25.76", + "zod-validation-error": "3.5.1" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.218.0", - "@medusajs/cli": "2.8.7", + "@medusajs/cli": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", diff --git a/packages/core/framework/src/http/__fixtures__/routers-middleware/middlewares.ts b/packages/core/framework/src/http/__fixtures__/routers-middleware/middlewares.ts index c79a43099f1df..3c243d7526000 100644 --- a/packages/core/framework/src/http/__fixtures__/routers-middleware/middlewares.ts +++ b/packages/core/framework/src/http/__fixtures__/routers-middleware/middlewares.ts @@ -1,13 +1,13 @@ import { raw } from "express" -import { MedusaRequest, MedusaResponse, MedusaNextFunction } from "../../types" +import { z } from "zod" +import { MedusaNextFunction, MedusaRequest, MedusaResponse } from "../../types" +import { defineMiddlewares } from "../../utils/define-middlewares" import { customersCreateMiddlewareMock, - customersGlobalMiddlewareMock, customersCreateMiddlewareValidatorMock, + customersGlobalMiddlewareMock, storeGlobalMiddlewareMock, } from "../mocks" -import z from "zod" -import { defineMiddlewares } from "../../utils/define-middlewares" const customersGlobalMiddleware = ( req: MedusaRequest, diff --git a/packages/core/framework/src/http/__fixtures__/server/index.ts b/packages/core/framework/src/http/__fixtures__/server/index.ts index 153bac8fe9d85..368151468fec7 100644 --- a/packages/core/framework/src/http/__fixtures__/server/index.ts +++ b/packages/core/framework/src/http/__fixtures__/server/index.ts @@ -9,14 +9,14 @@ import express from "express" import querystring from "querystring" import supertest from "supertest" -import { config } from "../mocks" import { MedusaContainer } from "@medusajs/types" import { configManager } from "../../../config" import { container } from "../../../container" import { featureFlagsLoader } from "../../../feature-flags" import { logger } from "../../../logger" -import { MedusaRequest } from "../../types" import { ApiLoader } from "../../router" +import { MedusaRequest } from "../../types" +import { config } from "../mocks" function asArray(resolvers) { return { @@ -69,6 +69,7 @@ export const createServer = async (rootDir) => { container.register({ logger: asValue({ error: () => {}, + info: () => {}, }), manager: asValue({}), }) diff --git a/packages/core/framework/src/http/__tests__/validate-body.spec.ts b/packages/core/framework/src/http/__tests__/validate-body.spec.ts index cb4418cfd6c44..d84ff4f4b1bdb 100644 --- a/packages/core/framework/src/http/__tests__/validate-body.spec.ts +++ b/packages/core/framework/src/http/__tests__/validate-body.spec.ts @@ -1,7 +1,7 @@ -import zod from "zod" import { MedusaError } from "@medusajs/utils" -import { validateAndTransformBody } from "../utils/validate-body" +import zod, { ZodNullable, ZodObject, ZodOptional } from "zod" import { MedusaRequest, MedusaResponse } from "../types" +import { validateAndTransformBody } from "../utils/validate-body" const createLinkBody = () => { return zod.object({ @@ -33,7 +33,7 @@ describe("validateAndTransformBody", () => { .nullish() const validatorFactory = ( - schema?: Zod.ZodOptional>> + schema?: ZodOptional>> ) => { return schema ? createLinkBody().extend({ @@ -69,7 +69,7 @@ describe("validateAndTransformBody", () => { .nullish() const validatorFactory = ( - schema?: Zod.ZodOptional>> + schema?: ZodOptional>> ) => { return schema ? createLinkBody().extend({ @@ -102,7 +102,7 @@ describe("validateAndTransformBody", () => { .nullish() const validatorFactory = ( - schema?: Zod.ZodOptional>> + schema?: ZodOptional>> ) => { return schema ? createLinkBody().extend({ diff --git a/packages/core/framework/src/http/middlewares/error-handler.ts b/packages/core/framework/src/http/middlewares/error-handler.ts index 822d0c15829c6..f46d3d44dac19 100644 --- a/packages/core/framework/src/http/middlewares/error-handler.ts +++ b/packages/core/framework/src/http/middlewares/error-handler.ts @@ -1,9 +1,9 @@ +import { ErrorRequestHandler, NextFunction, Response } from "express" import { fromZodIssue } from "zod-validation-error" -import { NextFunction, ErrorRequestHandler, Response } from "express" import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" -import { formatException } from "./exception-formatter" import { MedusaRequest } from "../types" +import { formatException } from "./exception-formatter" const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError" const TRANSACTION_STARTED = "TransactionAlreadyStartedError" @@ -31,7 +31,6 @@ export function errorHandler() { } err = formatException(err) - logger.error(err) const errorType = err.type || err.name const errObj = { @@ -82,6 +81,12 @@ export function errorHandler() { break } + if (statusCode >= 500) { + logger.error(err) + } else { + logger.info(err.message) + } + if ("issues" in err && Array.isArray(err.issues)) { const messages = err.issues.map((issue) => fromZodIssue(issue).toString()) res.status(statusCode).json({ diff --git a/packages/core/framework/src/http/types.ts b/packages/core/framework/src/http/types.ts index 64be1afd671f2..6a0412f46967e 100644 --- a/packages/core/framework/src/http/types.ts +++ b/packages/core/framework/src/http/types.ts @@ -136,7 +136,7 @@ export interface MedusaRequest< /** * An object containing fields and variables to be used with the remoteQuery * - * @version 2.2.0 + * @since 2.2.0 */ queryConfig: { fields: string[] diff --git a/packages/core/framework/src/migrations/migrator.ts b/packages/core/framework/src/migrations/migrator.ts index 60bbeb8a79d64..9873a7db77fb1 100644 --- a/packages/core/framework/src/migrations/migrator.ts +++ b/packages/core/framework/src/migrations/migrator.ts @@ -139,6 +139,7 @@ export abstract class Migrator { cwd: basePath, ignore: ["**/index.{js,ts}", "**/*.d.ts"], }) + scriptFiles.sort((a, b) => a.localeCompare(b)) if (!scriptFiles?.length) { continue diff --git a/packages/core/framework/src/types/container.ts b/packages/core/framework/src/types/container.ts index 65e1c2a43a7ad..99ce147368282 100644 --- a/packages/core/framework/src/types/container.ts +++ b/packages/core/framework/src/types/container.ts @@ -41,7 +41,7 @@ declare module "@medusajs/types" { */ [ContainerRegistrationKeys.REMOTE_LINK]: Link /** - * @version 2.2.0 + * @since 2.2.0 */ [ContainerRegistrationKeys.LINK]: Link [ContainerRegistrationKeys.CONFIG_MODULE]: ConfigModule diff --git a/packages/core/js-sdk/CHANGELOG.md b/packages/core/js-sdk/CHANGELOG.md index 9021e73a79303..878e1aea97a2d 100644 --- a/packages/core/js-sdk/CHANGELOG.md +++ b/packages/core/js-sdk/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/js-sdk +## 2.8.8 + +### Patch Changes + +- [#12944](https://github.com/medusajs/medusa/pull/12944) [`0db5bf6f8cfb47c67435f92733879e990b500d83`](https://github.com/medusajs/medusa/commit/0db5bf6f8cfb47c67435f92733879e990b500d83) Thanks [@anteprimorac](https://github.com/anteprimorac)! - fix(js-sdk): add missing admin order archive and complete methods + +- Updated dependencies [[`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5)]: + - @medusajs/types@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/js-sdk/package.json b/packages/core/js-sdk/package.json index 9f701078aedbf..71330095b6842 100644 --- a/packages/core/js-sdk/package.json +++ b/packages/core/js-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/js-sdk", - "version": "2.8.7", + "version": "2.8.8", "description": "SDK for the Medusa API", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -35,7 +35,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@medusajs/types": "2.8.7", + "@medusajs/types": "2.8.8", "fetch-event-stream": "^0.1.5", "qs": "^6.12.1" }, diff --git a/packages/core/js-sdk/src/admin/order-edit.ts b/packages/core/js-sdk/src/admin/order-edit.ts index a04cb18fe5202..97808d40a27dc 100644 --- a/packages/core/js-sdk/src/admin/order-edit.ts +++ b/packages/core/js-sdk/src/admin/order-edit.ts @@ -188,6 +188,8 @@ export class OrderEdit { * [Update Item Quantity](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsitemitem_id) * API route. * + * You can also use this method to remove an item from an order by setting the `quantity` to `0`. + * * @param id - The order edit's ID. * @param itemId - The item's ID in the order. * @param body - The data to edit in the item. diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts index 0e1dbd794406e..8135f0acdb7b2 100644 --- a/packages/core/js-sdk/src/admin/order.ts +++ b/packages/core/js-sdk/src/admin/order.ts @@ -194,6 +194,37 @@ export class Order { ) } + /** + * This method archives an order. It sends a request to the + * [Archive Order](https://docs.medusajs.com/api/admin#orders_postordersidarchive) + * API route. + * + * @param id - The order's ID. + * @param queryParams - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request + * @returns The order's details. + * + * @example + * sdk.admin.order.archive("order_123") + * .then(({ order }) => { + * console.log(order) + * }) + */ + async archive( + id: string, + queryParams?: SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/orders/${id}/archive`, + { + method: "POST", + query: queryParams, + headers, + } + ) + } + /** * This method cancels an order. It sends a request to the * [Cancel Order](https://docs.medusajs.com/api/admin#orders_postordersidcancel) @@ -219,6 +250,38 @@ export class Order { ) } + /** + * This method completes an order. It sends a request to the + * [Complete Order](https://docs.medusajs.com/api/admin#orders_postordersidcomplete) + * API route. + * + * @param id - The order's ID. + * @param headers - Headers to pass in the request. + * @returns The order's details. + * + * @example + * sdk.admin.order.complete("order_123") + * .then(({ order }) => { + * console.log(order) + * }) + */ + async complete( + id: string, + body: HttpTypes.AdditionalData, + queryParams?: SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/orders/${id}/complete`, + { + method: "POST", + body, + query: queryParams, + headers, + } + ) + } + /** * This method requests an order transfer. It sends a request to the * [Request Order Transfer](https://docs.medusajs.com/api/admin#orders_postordersidrequesttransfer) @@ -504,13 +567,13 @@ export class Order { /** * This method creates a credit line for an order. It sends a request to the * [Create Credit Line](https://docs.medusajs.com/api/admin#orders_postordersidcredit-lines) API route. - * + * * @param orderId - The order's ID. * @param body - The credit line's details. * @param query - Configure the fields to retrieve in the order. * @param headers - Headers to pass in the request * @returns The order's details. - * + * * @example * sdk.admin.order.createCreditLine( * "order_123", diff --git a/packages/core/js-sdk/src/admin/product.ts b/packages/core/js-sdk/src/admin/product.ts index 47325e5ac2905..0f4863d04d56d 100644 --- a/packages/core/js-sdk/src/admin/product.ts +++ b/packages/core/js-sdk/src/admin/product.ts @@ -66,7 +66,7 @@ export class Product { * [Create Product Import](https://docs.medusajs.com/api/admin#products_postproductsimports) * API route. * - * @version 2.8.5 + * @since 2.8.5 * * @param body - The import's details. * @param query - Query parameters to pass to the request. @@ -172,7 +172,7 @@ export class Product { * [Confirm Product Import](https://docs.medusajs.com/api/admin#products_postproductsimporttransaction_idconfirm) * API route. * - * @version 2.8.5 + * @since 2.8.5 * * @param transactionId - The ID of the transaction of the created product import. This is returned * by the API route used to create the product import. diff --git a/packages/core/js-sdk/src/admin/tax-provider.ts b/packages/core/js-sdk/src/admin/tax-provider.ts index f0d3848ec0995..e2ef2d219be64 100644 --- a/packages/core/js-sdk/src/admin/tax-provider.ts +++ b/packages/core/js-sdk/src/admin/tax-provider.ts @@ -21,7 +21,7 @@ export class TaxProvider { * [List Tax Providers](https://docs.medusajs.com/api/admin#tax-providers_gettaxproviders) * API route. * - * @version 2.8.0 + * @since 2.8.0 * * @param query - Filters and pagination configurations. * @param headers - Headers to pass in the request. diff --git a/packages/core/js-sdk/src/admin/tax-region.ts b/packages/core/js-sdk/src/admin/tax-region.ts index 694fb224475a1..000a9b8b4f43c 100644 --- a/packages/core/js-sdk/src/admin/tax-region.ts +++ b/packages/core/js-sdk/src/admin/tax-region.ts @@ -67,7 +67,7 @@ export class TaxRegion { * [Update Tax Region](https://docs.medusajs.com/api/admin#tax-regions_posttaxregionsid) * API route. * - * @version 2.8.0 + * @since 2.8.0 * * @param id - The ID of the tax region to update. * @param body - The details of the tax region to update. diff --git a/packages/core/modules-sdk/CHANGELOG.md b/packages/core/modules-sdk/CHANGELOG.md index 6e5a4bd253d1d..17ae306f0fee4 100644 --- a/packages/core/modules-sdk/CHANGELOG.md +++ b/packages/core/modules-sdk/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/modules-sdk +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`e74044af4de213ba6326a8fb2b5d02e5875b8a4a`](https://github.com/medusajs/medusa/commit/e74044af4de213ba6326a8fb2b5d02e5875b8a4a), [`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47), [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5)]: + - @medusajs/orchestration@2.8.8 + - @medusajs/types@2.8.8 + - @medusajs/utils@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/modules-sdk/package.json b/packages/core/modules-sdk/package.json index 9b0dbbd5c959d..d1042fdf6b6e5 100644 --- a/packages/core/modules-sdk/package.json +++ b/packages/core/modules-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/modules-sdk", - "version": "2.8.7", + "version": "2.8.8", "description": "SDK for medusa modules", "main": "dist/index.js", "export": { @@ -45,9 +45,9 @@ "typescript": "^5.6.2" }, "dependencies": { - "@medusajs/orchestration": "2.8.7", - "@medusajs/types": "2.8.7", - "@medusajs/utils": "2.8.7" + "@medusajs/orchestration": "2.8.8", + "@medusajs/types": "2.8.8", + "@medusajs/utils": "2.8.8" }, "peerDependencies": { "@mikro-orm/core": "6.4.3", diff --git a/packages/core/orchestration/CHANGELOG.md b/packages/core/orchestration/CHANGELOG.md index 6ec84ae931d2e..a367a71e5d8d3 100644 --- a/packages/core/orchestration/CHANGELOG.md +++ b/packages/core/orchestration/CHANGELOG.md @@ -1,5 +1,17 @@ # @medusajs/orchestration +## 2.8.8 + +### Patch Changes + +- [#12951](https://github.com/medusajs/medusa/pull/12951) [`e74044af4de213ba6326a8fb2b5d02e5875b8a4a`](https://github.com/medusajs/medusa/commit/e74044af4de213ba6326a8fb2b5d02e5875b8a4a) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - chore(orchestration): improve transaction error handling + +- [#12903](https://github.com/medusajs/medusa/pull/12903) [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(orchestration): Prevent workf. cancellation to execute while rescheduling + +- Updated dependencies [[`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47)]: + - @medusajs/types@2.8.8 + - @medusajs/utils@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/orchestration/package.json b/packages/core/orchestration/package.json index a162e52051237..ad2fdb340dd83 100644 --- a/packages/core/orchestration/package.json +++ b/packages/core/orchestration/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/orchestration", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa utilities to orchestrate modules", "main": "dist/index.js", "exports": { @@ -39,8 +39,8 @@ "typescript": "^5.6.2" }, "dependencies": { - "@medusajs/types": "2.8.7", - "@medusajs/utils": "2.8.7", + "@medusajs/types": "2.8.8", + "@medusajs/utils": "2.8.8", "ulid": "^2.3.0" }, "peerDependencies": { diff --git a/packages/core/orchestration/src/__tests__/transaction/transaction-orchestrator.ts b/packages/core/orchestration/src/__tests__/transaction/transaction-orchestrator.ts index 48b1ca5911e98..2c1ace528b659 100644 --- a/packages/core/orchestration/src/__tests__/transaction/transaction-orchestrator.ts +++ b/packages/core/orchestration/src/__tests__/transaction/transaction-orchestrator.ts @@ -1,4 +1,8 @@ -import { TransactionStepState, TransactionStepStatus } from "@medusajs/utils" +import { + MedusaError, + TransactionStepState, + TransactionStepStatus, +} from "@medusajs/utils" import { setTimeout } from "timers/promises" import { DistributedTransaction, @@ -690,6 +694,136 @@ describe("Transaction Orchestrator", () => { expect(transaction.getState()).toBe(TransactionState.FAILED) }) + it("Should handle multiple types of errors", async () => { + const errorTypes = [ + new Error("Regular error object"), + new MedusaError(MedusaError.Types.NOT_FOUND, "Not found error"), + { message: "Custom error object" }, + "String error", + 123, + {}, + null, + [1, 2, "b"], + new Date(), + ] + async function handler( + actionId: string, + functionHandlerType: TransactionHandlerType, + payload: TransactionPayload + ) { + const command = { + [actionId]: { + [TransactionHandlerType.INVOKE]: () => { + throw errorTypes[+actionId.slice(-1) - 1] + }, + }, + } + + return command[actionId][functionHandlerType](payload) + } + + const flow: TransactionStepsDefinition = { + next: Array.from({ length: errorTypes.length }, (_, i) => ({ + action: `a${i + 1}`, + maxRetries: 0, + noCompensation: true, + })), + } + + const strategy = new TransactionOrchestrator({ + id: "transaction-name", + definition: flow, + }) + + const transaction = await strategy.beginTransaction({ + transactionId: "transaction_id_123", + handler, + }) + + await strategy.resume(transaction) + + expect(transaction.getErrors()).toEqual([ + { + action: "a1", + handlerType: "invoke", + error: { + message: "Regular error object", + name: "Error", + stack: expect.stringContaining("transaction-name -> a1 (invoke)"), + }, + }, + { + action: "a2", + handlerType: "invoke", + error: { + message: "Not found error", + name: "Error", + stack: expect.stringContaining("transaction-name -> a2 (invoke)"), + type: "not_found", + __isMedusaError: true, + code: undefined, + date: expect.any(Date), + }, + }, + { + action: "a3", + handlerType: "invoke", + error: { + message: "Custom error object", + stack: expect.stringContaining("transaction-name -> a3 (invoke)"), + }, + }, + { + action: "a4", + handlerType: "invoke", + error: { + message: '"String error"', + stack: expect.stringContaining("transaction-name -> a4 (invoke)"), + }, + }, + { + action: "a5", + handlerType: "invoke", + error: { + message: "123", + stack: expect.stringContaining("transaction-name -> a5 (invoke)"), + }, + }, + { + action: "a6", + handlerType: "invoke", + error: { + message: "{}", + stack: expect.stringContaining("transaction-name -> a6 (invoke)"), + }, + }, + { + action: "a7", + handlerType: "invoke", + error: { + message: "null", + stack: expect.stringContaining("transaction-name -> a7 (invoke)"), + }, + }, + { + action: "a8", + handlerType: "invoke", + error: { + message: '[1,2,"b"]', + stack: expect.stringContaining("transaction-name -> a8 (invoke)"), + }, + }, + { + action: "a9", + handlerType: "invoke", + error: { + message: expect.any(String), + stack: expect.stringContaining("transaction-name -> a9 (invoke)"), + }, + }, + ]) + }) + it("Should complete a transaction if a failing step has the flag 'continueOnPermanentFailure' set to true", async () => { const mocks = { one: jest.fn().mockImplementation((payload) => { diff --git a/packages/core/orchestration/src/transaction/transaction-orchestrator.ts b/packages/core/orchestration/src/transaction/transaction-orchestrator.ts index 9b46be99a1210..870fae6ba54fb 100644 --- a/packages/core/orchestration/src/transaction/transaction-orchestrator.ts +++ b/packages/core/orchestration/src/transaction/transaction-orchestrator.ts @@ -686,6 +686,20 @@ export class TransactionOrchestrator extends EventEmitter { if (isErrorLike(error)) { error = serializeError(error) + } else { + try { + if (error?.message) { + error = JSON.parse(JSON.stringify(error)) + } else { + error = { + message: JSON.stringify(error), + } + } + } catch (e) { + error = { + message: "Unknown non-serializable error", + } + } } if ( @@ -712,15 +726,15 @@ export class TransactionOrchestrator extends EventEmitter { ? TransactionHandlerType.COMPENSATE : TransactionHandlerType.INVOKE - if (error?.stack) { - const workflowId = transaction.modelId - const stepAction = step.definition.action - const sourcePath = transaction.getFlow().metadata?.sourcePath - const sourceStack = sourcePath - ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${TransactionHandlerType.INVOKE})]` - : `\n⮑ \sat [${workflowId} -> ${stepAction} (${TransactionHandlerType.INVOKE})]` - error.stack += sourceStack - } + error.stack ??= "" + + const workflowId = transaction.modelId + const stepAction = step.definition.action + const sourcePath = transaction.getFlow().metadata?.sourcePath + const sourceStack = sourcePath + ? `\n⮑ \sat ${sourcePath}: [${workflowId} -> ${stepAction} (${TransactionHandlerType.INVOKE})]` + : `\n⮑ \sat [${workflowId} -> ${stepAction} (${TransactionHandlerType.INVOKE})]` + error.stack += sourceStack transaction.addError(step.definition.action!, handlerType, error) } @@ -1171,7 +1185,9 @@ export class TransactionOrchestrator extends EventEmitter { ) if (ret.transactionIsCancelling) { - return await this.cancelTransaction(transaction) + await this.cancelTransaction(transaction, { + preventExecuteNext: true, + }) } if (isAsync && !ret.stopExecution) { @@ -1190,6 +1206,10 @@ export class TransactionOrchestrator extends EventEmitter { isPermanent: boolean, response?: unknown ): Promise { + const isAsync = step.isCompensating() + ? step.definition.compensateAsync + : step.definition.async + if (isDefined(response) && step.saveResponse) { transaction.addResponse( step.definition.action!, @@ -1208,7 +1228,14 @@ export class TransactionOrchestrator extends EventEmitter { ) if (ret.transactionIsCancelling) { - return await this.cancelTransaction(transaction) + await this.cancelTransaction(transaction, { + preventExecuteNext: true, + }) + } + + if (isAsync && !ret.stopExecution) { + // Schedule to continue the execution of async steps because they are not awaited on purpose and can be handled by another machine + await transaction.scheduleRetry(step, 0) } } @@ -1273,7 +1300,8 @@ export class TransactionOrchestrator extends EventEmitter { * @param transaction - The transaction to be reverted */ public async cancelTransaction( - transaction: DistributedTransactionType + transaction: DistributedTransactionType, + options?: { preventExecuteNext?: boolean } ): Promise { if (transaction.modelId !== this.id) { throw new MedusaError( @@ -1305,6 +1333,10 @@ export class TransactionOrchestrator extends EventEmitter { await transaction.saveCheckpoint() + if (options?.preventExecuteNext) { + return + } + await this.executeNext(transaction) } diff --git a/packages/core/types/CHANGELOG.md b/packages/core/types/CHANGELOG.md index 4582a86cf26b2..74b0b5bb31037 100644 --- a/packages/core/types/CHANGELOG.md +++ b/packages/core/types/CHANGELOG.md @@ -1,5 +1,13 @@ # @medusajs/types +## 2.8.8 + +### Patch Changes + +- [#13002](https://github.com/medusajs/medusa/pull/13002) [`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - fix(modules-sdk): Entity types + +- [#12936](https://github.com/medusajs/medusa/pull/12936) [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5) Thanks [@shahednasser](https://github.com/shahednasser)! - fix(types): add attachments to CreateNotificationDTO type + ## 2.8.7 ### Patch Changes diff --git a/packages/core/types/package.json b/packages/core/types/package.json index 25565c53da21b..447e11628086f 100644 --- a/packages/core/types/package.json +++ b/packages/core/types/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/types", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Types definition", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/core/types/src/file/service.ts b/packages/core/types/src/file/service.ts index 4845a3e2700a6..3b8416b44f746 100644 --- a/packages/core/types/src/file/service.ts +++ b/packages/core/types/src/file/service.ts @@ -176,7 +176,7 @@ export interface IFileModuleService extends IModuleService { * This method retrieves a file by its ID and returns a stream to download the file. Under the hood, it will use the * file provider that was used to upload the file to retrievethe stream. * - * @version 2.8.0 + * @since 2.8.0 * * @param {string} id - The ID of the file. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. @@ -192,7 +192,7 @@ export interface IFileModuleService extends IModuleService { * This method retrieves a file by its ID and returns the file contents as a buffer. Under the hood, it will use the * file provider that was used to upload the file to retrieve the buffer. * - * @version 2.8.0 + * @since 2.8.0 * * @param {string} id - The ID of the file. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. diff --git a/packages/core/types/src/http/common/response.ts b/packages/core/types/src/http/common/response.ts index 4fb23a28019e0..6c0bba75c2f2a 100644 --- a/packages/core/types/src/http/common/response.ts +++ b/packages/core/types/src/http/common/response.ts @@ -42,7 +42,7 @@ export type PaginatedResponse = { * The estimated count retrieved from the PostgreSQL query planner, which may be inaccurate. * * @featureFlag index_engine - * @version 2.8.0 + * @since 2.8.0 */ estimate_count?: number } & T diff --git a/packages/core/types/src/index-data/query-config/query-input-config.ts b/packages/core/types/src/index-data/query-config/query-input-config.ts index 11242b7ae9d45..c6e6a27984dd2 100644 --- a/packages/core/types/src/index-data/query-config/query-input-config.ts +++ b/packages/core/types/src/index-data/query-config/query-input-config.ts @@ -71,7 +71,7 @@ export type QueryFunctionReturnPagination = { take: number /** * @featureFlag index_engine - * @version 2.8.0 + * @since 2.8.0 */ estimate_count: number } diff --git a/packages/core/types/src/modules-sdk/remote-query-object-from-string.ts b/packages/core/types/src/modules-sdk/remote-query-object-from-string.ts index 9cf833c110334..4263a5d664af3 100644 --- a/packages/core/types/src/modules-sdk/remote-query-object-from-string.ts +++ b/packages/core/types/src/modules-sdk/remote-query-object-from-string.ts @@ -80,7 +80,7 @@ export type RemoteQueryInput = /** * The name of the entity to retrieve. For example, `product`. */ - entity: TEntry + entity: TEntry | keyof RemoteQueryEntryPoints /** * The fields and relations to retrieve in the entity. */ diff --git a/packages/core/types/src/notification/mutations.ts b/packages/core/types/src/notification/mutations.ts index 416c7aafd3828..3c9a3c33b8471 100644 --- a/packages/core/types/src/notification/mutations.ts +++ b/packages/core/types/src/notification/mutations.ts @@ -1,4 +1,4 @@ -import { NotificationContent } from "./common" +import { Attachment, NotificationContent } from "./common" /** * @interface @@ -52,4 +52,8 @@ export interface CreateNotificationDTO { * An idempotency key that ensures the same notification is not sent multiple times. */ idempotency_key?: string | null + /** + * Optional attachments for the notification. + */ + attachments?: Attachment[] | null } diff --git a/packages/core/types/src/payment/provider.ts b/packages/core/types/src/payment/provider.ts index 86b5b5501d83f..d65dac3e151c6 100644 --- a/packages/core/types/src/payment/provider.ts +++ b/packages/core/types/src/payment/provider.ts @@ -419,7 +419,7 @@ export interface IPaymentProvider { * @param data - Input data including the details of the account holder to create. * @returns The result of creating the account holder. If an error occurs, throw it. * - * @version 2.5.0 + * @since 2.5.0 * * @example * import { MedusaError } from "@medusajs/framework/utils" @@ -467,7 +467,7 @@ export interface IPaymentProvider { * @param data - Input data including the details of the account holder to update. * @returns The result of updating the account holder. If an error occurs, throw it. * - * @version 2.5.1 + * @since 2.5.1 * * @example * import { MedusaError } from "@medusajs/framework/utils" @@ -508,7 +508,7 @@ export interface IPaymentProvider { * @param data - Input data including the details of the account holder to delete. * @returns The result of deleting the account holder. If an error occurs, throw it. * - * @version 2.5.0 + * @since 2.5.0 * * @example * import { MedusaError } from "@medusajs/framework/utils" @@ -544,7 +544,7 @@ export interface IPaymentProvider { * in the third-party payment provider. A payment provider that supports saving payment methods * must implement this method. * - * @version 2.5.0 + * @since 2.5.0 * * @param data - Input data including the details of the account holder to list payment methods for. * @returns The list of payment methods saved for the account holder. If an error occurs, throw it. @@ -587,7 +587,7 @@ export interface IPaymentProvider { * third-party payment provider. A payment provider that supports saving payment methods * must implement this method. * - * @version 2.5.0 + * @since 2.5.0 * * @param data - The details of the payment method to save. * @returns The result of saving the payment method. If an error occurs, throw it. diff --git a/packages/core/types/src/promotion/common/compute-actions.ts b/packages/core/types/src/promotion/common/compute-actions.ts index 49831a5b527ca..da40d48a62eb1 100644 --- a/packages/core/types/src/promotion/common/compute-actions.ts +++ b/packages/core/types/src/promotion/common/compute-actions.ts @@ -61,11 +61,10 @@ export interface AddItemAdjustmentAction { amount: BigNumberInput /** - * Whether the adjustment amount includes tax. + * Whether the promotion amount includes tax. */ is_tax_inclusive?: boolean - /** /** * The promotion's code. */ @@ -186,6 +185,11 @@ export interface ComputeActionItemLine extends Record { */ subtotal: BigNumberInput + /** + * The total of the line item. + */ + original_total: BigNumberInput + /** * Whether the line item is discountable. */ @@ -211,6 +215,11 @@ export interface ComputeActionShippingLine extends Record { */ subtotal: BigNumberInput + /** + * The total of the shipping method. + */ + original_total: BigNumberInput + /** * The adjustments applied before on the shipping method. */ diff --git a/packages/core/types/src/tax/service.ts b/packages/core/types/src/tax/service.ts index ca67a49a6f521..16945076f6a5f 100644 --- a/packages/core/types/src/tax/service.ts +++ b/packages/core/types/src/tax/service.ts @@ -585,7 +585,7 @@ export interface ITaxModuleService extends IModuleService { /** * This method retrieves a paginated list of tax providers based on optional filters and configuration. * - * @version 2.8.0 + * @since 2.8.0 * * @param {FilterableTaxProviderProps} filters - The filters to apply on the retrieved tax providers. * @param {FindConfig} config - The configurations determining how the tax provider is retrieved. Its properties, such as `select` or `relations`, accept the diff --git a/packages/core/utils/CHANGELOG.md b/packages/core/utils/CHANGELOG.md index 37ca95d07485d..abd21cfbe6aae 100644 --- a/packages/core/utils/CHANGELOG.md +++ b/packages/core/utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/utils +## 2.8.8 + +### Patch Changes + +- [#12969](https://github.com/medusajs/medusa/pull/12969) [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47) Thanks [@juanzgc](https://github.com/juanzgc)! - fix: accepted values in import with template + +- Updated dependencies [[`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5)]: + - @medusajs/types@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/utils/package.json b/packages/core/utils/package.json index 0977fd1f1cbce..cbd4c8a334eba 100644 --- a/packages/core/utils/package.json +++ b/packages/core/utils/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/utils", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa utilities functions shared by Medusa core and Modules", "main": "dist/index.js", "exports": { @@ -47,7 +47,7 @@ "@graphql-codegen/typescript": "^4.0.9", "@graphql-tools/merge": "^9.0.7", "@graphql-tools/schema": "^10.0.6", - "@medusajs/types": "2.8.7", + "@medusajs/types": "2.8.8", "@types/pluralize": "^0.0.33", "bignumber.js": "^9.1.2", "dotenv": "^16.4.5", @@ -57,7 +57,7 @@ "pg-connection-string": "^2.7.0", "pluralize": "^8.0.0", "ulid": "^2.3.0", - "zod": "3.22.4" + "zod": "3.25.76" }, "peerDependencies": { "@mikro-orm/core": "6.4.3", diff --git a/packages/core/utils/src/common/container.ts b/packages/core/utils/src/common/container.ts index da778c1a6f9c3..6f0873b4ec3bc 100644 --- a/packages/core/utils/src/common/container.ts +++ b/packages/core/utils/src/common/container.ts @@ -10,7 +10,7 @@ export const ContainerRegistrationKeys = { */ REMOTE_LINK: "remoteLink", /** - * @version 2.2.0 + * @since 2.2.0 */ LINK: "link", FEATURE_FLAG_ROUTER: "featureFlagRouter", diff --git a/packages/core/utils/src/core-flows/events.ts b/packages/core/utils/src/core-flows/events.ts index 50f51499b4876..c0c5f5660405a 100644 --- a/packages/core/utils/src/core-flows/events.ts +++ b/packages/core/utils/src/core-flows/events.ts @@ -52,7 +52,7 @@ export const CartWorkflowEvents = { /** * Emitted when the customer in the cart is transferred. * - * @version 2.8.0 + * @since 2.8.0 * * @eventPayload * ```ts @@ -269,7 +269,7 @@ export const OrderEditWorkflowEvents = { /** * Emitted when an order edit is requested. * - * @version 2.8.0 + * @since 2.8.0 * * @eventPayload * ```ts @@ -283,7 +283,7 @@ export const OrderEditWorkflowEvents = { /** * Emitted when an order edit request is confirmed. * - * @version 2.8.0 + * @since 2.8.0 * * @eventPayload * ```ts @@ -297,7 +297,7 @@ export const OrderEditWorkflowEvents = { /** * Emitted when an order edit request is canceled. * - * @version 2.8.0 + * @since 2.8.0 * * @eventPayload * ```ts diff --git a/packages/core/utils/src/defaults/currencies.ts b/packages/core/utils/src/defaults/currencies.ts index 2240be74f61e1..0ca6d88388f62 100644 --- a/packages/core/utils/src/defaults/currencies.ts +++ b/packages/core/utils/src/defaults/currencies.ts @@ -675,6 +675,15 @@ export const defaultCurrencies: Record = { code: "MUR", name_plural: "Mauritian rupees", }, + MWK: { + symbol: "K", + name: "Malawian Kwacha", + symbol_native: "K", + decimal_digits: 2, + rounding: 0, + code: "MWK", + name_plural: "Malawian Kwachas", + }, MXN: { symbol: "MX$", name: "Mexican Peso", @@ -1053,6 +1062,15 @@ export const defaultCurrencies: Record = { code: "XOF", name_plural: "CFA francs BCEAO", }, + XPF: { + symbol: "₣", + name: "CFP Franc", + symbol_native: "₣", + decimal_digits: 0, + rounding: 0, + code: "XPF", + name_plural: "CFP francs" + }, YER: { symbol: "YR", name: "Yemeni Rial", diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index 9da9b3a2db786..6f7b8697b1d64 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -242,7 +242,7 @@ export class EntityBuilder { * This method defines a float property that allows for * values with decimal places * - * @version 2.1.2 + * @since 2.1.2 * * @example * import { model } from "@medusajs/framework/utils" diff --git a/packages/core/utils/src/file/abstract-file-provider.ts b/packages/core/utils/src/file/abstract-file-provider.ts index 68dc4aaa42e10..fe070684cf579 100644 --- a/packages/core/utils/src/file/abstract-file-provider.ts +++ b/packages/core/utils/src/file/abstract-file-provider.ts @@ -192,7 +192,7 @@ export class AbstractFileProviderService implements IFileProvider { * @param {FileTypes.ProviderGetFileDTO} fileData - The details of the file to get its stream. * @returns {Promise} The file's stream. * - * @version 2.8.0 + * @since 2.8.0 * * @example * class MyFileProviderService extends AbstractFileProviderService { @@ -217,7 +217,7 @@ export class AbstractFileProviderService implements IFileProvider { * @param {FileTypes.ProviderGetFileDTO} fileData - The details of the file to get its buffer. * @returns {Promise} The file's buffer. * - * @version 2.8.0 + * @since 2.8.0 * * @example * class MyFileProviderService extends AbstractFileProviderService { diff --git a/packages/core/utils/src/modules-sdk/__tests__/medusa-service.spec.ts b/packages/core/utils/src/modules-sdk/__tests__/medusa-service.spec.ts index 79deea214ac8d..c3ba5df0805f8 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/medusa-service.spec.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/medusa-service.spec.ts @@ -29,6 +29,14 @@ describe("Abstract Module Service Factory", () => { mainModelMockService: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + // Return single object if single object passed, array if array passed + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + // Return single object if single object passed, array if array passed + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue([]), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), @@ -36,6 +44,12 @@ describe("Abstract Module Service Factory", () => { otherModelMock1Service: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue([]), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), @@ -43,6 +57,12 @@ describe("Abstract Module Service Factory", () => { otherModelMock2Service: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue([]), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), @@ -122,6 +142,77 @@ describe("Abstract Module Service Factory", () => { defaultTransactionContext ) }) + + it("should have create method that handles single object input", async () => { + const inputData = { name: "Test Item" } + const result = await instance.createMainModelMocks(inputData) + + // Should return single object when single object is passed + expect(result).toEqual({ name: "Test Item", id: "1" }) + expect(Array.isArray(result)).toBe(false) + + // Should call underlying service with single object (not wrapped in array) + expect(containerMock.mainModelMockService.create).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have create method that handles array input", async () => { + const inputData = [{ name: "Test Item 1" }, { name: "Test Item 2" }] + const result = await instance.createMainModelMocks(inputData) + + // Should return array when array is passed + expect(result).toEqual([ + { name: "Test Item 1", id: "1" }, + { name: "Test Item 2", id: "1" } + ]) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) + + // Should call underlying service with array + expect(containerMock.mainModelMockService.create).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have update method that handles single object input", async () => { + const inputData = { id: "1", name: "Updated Item" } + const result = await instance.updateMainModelMocks(inputData) + + // Should return single object when single object is passed + expect(result).toEqual({ id: "1", name: "Updated Item", updated: true }) + expect(Array.isArray(result)).toBe(false) + + // Should call underlying service with single object (not wrapped in array) + expect(containerMock.mainModelMockService.update).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have update method that handles array input", async () => { + const inputData = [ + { id: "1", name: "Updated Item 1" }, + { id: "2", name: "Updated Item 2" } + ] + const result = await instance.updateMainModelMocks(inputData) + + // Should return array when array is passed + expect(result).toEqual([ + { id: "1", name: "Updated Item 1", updated: true }, + { id: "2", name: "Updated Item 2", updated: true } + ]) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) + + // Should call underlying service with array + expect(containerMock.mainModelMockService.update).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) }) describe("Other Models Methods", () => { @@ -185,6 +276,77 @@ describe("Abstract Module Service Factory", () => { defaultTransactionContext ) }) + + it("should have create method for other models that handles single object input", async () => { + const inputData = { name: "Test Other Item" } + const result = await instance.createOtherModelMock1s(inputData) + + // Should return single object when single object is passed + expect(result).toEqual({ name: "Test Other Item", id: "1" }) + expect(Array.isArray(result)).toBe(false) + + // Should call underlying service with single object (not wrapped in array) + expect(containerMock.otherModelMock1Service.create).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have create method for other models that handles array input", async () => { + const inputData = [{ name: "Test Other Item 1" }, { name: "Test Other Item 2" }] + const result = await instance.createOtherModelMock1s(inputData) + + // Should return array when array is passed + expect(result).toEqual([ + { name: "Test Other Item 1", id: "1" }, + { name: "Test Other Item 2", id: "1" } + ]) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) + + // Should call underlying service with array + expect(containerMock.otherModelMock1Service.create).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have update method for other models that handles single object input", async () => { + const inputData = { id: "1", name: "Updated Other Item" } + const result = await instance.updateOtherModelMock1s(inputData) + + // Should return single object when single object is passed + expect(result).toEqual({ id: "1", name: "Updated Other Item", updated: true }) + expect(Array.isArray(result)).toBe(false) + + // Should call underlying service with single object (not wrapped in array) + expect(containerMock.otherModelMock1Service.update).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) + + it("should have update method for other models that handles array input", async () => { + const inputData = [ + { id: "1", name: "Updated Other Item 1" }, + { id: "2", name: "Updated Other Item 2" } + ] + const result = await instance.updateOtherModelMock1s(inputData) + + // Should return array when array is passed + expect(result).toEqual([ + { id: "1", name: "Updated Other Item 1", updated: true }, + { id: "2", name: "Updated Other Item 2", updated: true } + ]) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) + + // Should call underlying service with array + expect(containerMock.otherModelMock1Service.update).toHaveBeenCalledWith( + inputData, + defaultTransactionContext + ) + }) }) describe("Custom configuration using DML", () => { @@ -196,6 +358,12 @@ describe("Abstract Module Service Factory", () => { houseService: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue(undefined), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), @@ -203,6 +371,12 @@ describe("Abstract Module Service Factory", () => { carService: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue(undefined), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), @@ -210,6 +384,12 @@ describe("Abstract Module Service Factory", () => { userService: { retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + create: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, id: "1" })) : { ...data, id: "1" } + }), + update: jest.fn().mockImplementation((data) => { + return Array.isArray(data) ? data.map(item => ({ ...item, updated: true })) : { ...data, updated: true } + }), delete: jest.fn().mockResolvedValue(undefined), softDelete: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]), diff --git a/packages/core/utils/src/modules-sdk/medusa-service.ts b/packages/core/utils/src/modules-sdk/medusa-service.ts index ddf91048af1f1..ce1639ad6425a 100644 --- a/packages/core/utils/src/modules-sdk/medusa-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-service.ts @@ -219,10 +219,8 @@ export function MedusaService< data = [], sharedContext: Context = {} ): Promise { - const serviceData = Array.isArray(data) ? data : [data] const service = this.__container__[serviceRegistrationName] - const models = await service.create(serviceData, sharedContext) - const response = Array.isArray(data) ? models : models[0] + const response = await service.create(data, sharedContext) klassPrototype.aggregatedEvents.bind(this)({ action: CommonEvents.CREATED, @@ -243,9 +241,8 @@ export function MedusaService< data = [], sharedContext: Context = {} ): Promise { - const serviceData = Array.isArray(data) ? data : [data] const service = this.__container__[serviceRegistrationName] - const response = await service.update(serviceData, sharedContext) + const response = await service.update(data, sharedContext) klassPrototype.aggregatedEvents.bind(this)({ action: CommonEvents.UPDATED, diff --git a/packages/core/utils/src/payment/abstract-payment-provider.ts b/packages/core/utils/src/payment/abstract-payment-provider.ts index c4afb32cda6a2..ef28ba62ca9a7 100644 --- a/packages/core/utils/src/payment/abstract-payment-provider.ts +++ b/packages/core/utils/src/payment/abstract-payment-provider.ts @@ -599,7 +599,7 @@ export abstract class AbstractPaymentProvider> * { * amount, * currency_code, - * context.customer + * customer: context.customer * } * ) * diff --git a/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts index 0fbfdb33f8afe..03aec2c18e9d2 100644 --- a/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts +++ b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts @@ -1100,4 +1100,234 @@ describe("CSV processor", () => { } `) }) + + describe("System-generated columns", () => { + it("should ignore product timestamp columns during import", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + expect(normalized["product is giftcard"]).toBe("true") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toCreate since we only have handle + expect(result.toCreate["test-product"]).toBeDefined() + expect(result.toCreate["test-product"].is_giftcard).toBe(true) + + // Timestamp fields should not be in the output since they're ignored + expect(result.toCreate["test-product"]["created_at"]).toBeUndefined() + expect(result.toCreate["test-product"]["updated_at"]).toBeUndefined() + expect(result.toCreate["test-product"]["deleted_at"]).toBeUndefined() + + // Verify that the timestamp fields are present in normalized data but ignored during processing + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + }) + + it("should ignore variant timestamp columns during import", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Variant Title": "Test Variant", + "Variant SKU": "TEST-SKU", + "Variant Created At": "", + "Variant Updated At": "", + "Variant Deleted At": "", + "Variant Product Id": "prod_123", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["variant created at"]).toBe("") + expect(normalized["variant updated at"]).toBe("") + expect(normalized["variant deleted at"]).toBe("") + expect(normalized["variant product id"]).toBe("prod_123") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toCreate since we only have handle + expect(result.toCreate["test-product"]).toBeDefined() + expect(result.toCreate["test-product"].variants).toHaveLength(1) + + const variant = result.toCreate["test-product"].variants[0] + expect(variant.title).toBe("Test Variant") + expect(variant.sku).toBe("TEST-SKU") + + // Timestamp fields should not be in the variant output since they're ignored + expect(variant["created_at"]).toBeUndefined() + expect(variant["updated_at"]).toBeUndefined() + expect(variant["deleted_at"]).toBeUndefined() + expect(variant["product_id"]).toBeUndefined() + + // Verify that the timestamp fields are present in normalized data but ignored during processing + expect(normalized["variant created at"]).toBe("") + expect(normalized["variant updated at"]).toBe("") + expect(normalized["variant deleted at"]).toBe("") + expect(normalized["variant product id"]).toBe("prod_123") + }) + + it("should process product is giftcard as boolean correctly", () => { + const csvRow = { + "Product Handle": "giftcard-product", + "Product Title": "Gift Card", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate["giftcard-product"].is_giftcard).toBe(true) + }) + + it("should process product is giftcard as false correctly", () => { + const csvRow = { + "Product Handle": "regular-product", + "Product Title": "Regular Product", + "Product Is Giftcard": "false", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate["regular-product"].is_giftcard).toBe(false) + }) + + it("should handle product is giftcard with various truthy/falsy values", () => { + const testCases = [ + { value: "true", expected: true }, + { value: "false", expected: false }, + { value: "TRUE", expected: true }, + { value: "FALSE", expected: false }, + ] + + testCases.forEach(({ value, expected }) => { + const csvRow = { + "Product Handle": `test-product-${value}`, + "Product Title": "Test Product", + "Product Is Giftcard": value, + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate[`test-product-${value}`].is_giftcard).toBe(expected) + }) + }) + }) + + describe("Column validation", () => { + it("should accept all system-generated columns without error", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + "Product Is Giftcard": "true", + "Variant Title": "Test Variant", + "Variant Created At": "", + "Variant Updated At": "", + "Variant Deleted At": "", + "Variant Product Id": "prod_123", + } + + expect(() => CSVNormalizer.preProcess(csvRow, 1)).not.toThrow() + }) + + it("should still reject truly unknown columns", () => { + const csvRow = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Unknown Column": "some value", + } + + expect(() => CSVNormalizer.preProcess(csvRow, 1)).toThrow( + 'Invalid column name(s) "Unknown Column"' + ) + }) + + it("should handle mixed case column names correctly", () => { + const csvRow = { + "PRODUCT HANDLE": "test-product", + "Product Title": "Test Product", + "PRODUCT IS GIFTCARD": "true", + "Variant Created At": "2024-01-01T00:00:00Z", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product handle"]).toBe("test-product") + expect(normalized["product is giftcard"]).toBe("true") + expect(normalized["variant created at"]).toBe("2024-01-01T00:00:00Z") + }) + }) + + describe("Edge cases", () => { + it("should handle empty timestamp values", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + expect(result.toCreate["test-product"]).toBeDefined() + }) + + it("should handle products with only ID (no handle) correctly", () => { + const csvRow = { + "Product Id": "prod_123", + "Product Title": "Test Product", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toUpdate since we have an ID + expect(result.toUpdate["prod_123"]).toBeDefined() + expect(result.toUpdate["prod_123"].is_giftcard).toBe(true) + }) + + it("should handle products with both ID and handle correctly", () => { + const csvRow = { + "Product Id": "prod_123", + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toUpdate since we have an ID + expect(result.toUpdate["prod_123"]).toBeDefined() + expect(result.toUpdate["prod_123"].is_giftcard).toBe(true) + expect(result.toCreate["test-product"]).toBeUndefined() + }) + }) }) diff --git a/packages/core/utils/src/product/csv-normalizer.ts b/packages/core/utils/src/product/csv-normalizer.ts index f723acd309eb6..65f49c9f6af48 100644 --- a/packages/core/utils/src/product/csv-normalizer.ts +++ b/packages/core/utils/src/product/csv-normalizer.ts @@ -79,6 +79,15 @@ function processAsString( } } +/** + * Processes a column value but ignores it (no-op processor for system-generated fields) + */ +function processAsIgnored(): ColumnProcessor { + return () => { + // Do nothing - this column is intentionally ignored + } +} + /** * Processes the column value as a boolean */ @@ -159,9 +168,9 @@ const productStaticColumns: { "product id": processAsString("product id", "id"), "product handle": processAsString("product handle", "handle"), "product title": processAsString("product title", "title"), + "product subtitle": processAsString("product subtitle", "subtitle"), "product status": processAsString("product status", "status"), "product description": processAsString("product description", "description"), - "product subtitle": processAsString("product subtitle", "subtitle"), "product external id": processAsString("product external id", "external_id"), "product thumbnail": processAsString("product thumbnail", "thumbnail"), "product collection id": processAsString( @@ -189,6 +198,12 @@ const productStaticColumns: { "shipping profile id", "shipping_profile_id" ), + // Product properties that should be imported + "product is giftcard": processAsBoolean("product is giftcard", "is_giftcard"), + // System-generated timestamp fields that should be ignored during import + "product created at": processAsIgnored(), + "product deleted at": processAsIgnored(), + "product updated at": processAsIgnored(), } /** @@ -253,6 +268,12 @@ const variantStaticColumns: { ), "variant width": processAsNumber("variant width", "width"), "variant weight": processAsNumber("variant weight", "weight"), + // System-generated timestamp fields that should be ignored during import + "variant created at": processAsIgnored(), + "variant deleted at": processAsIgnored(), + "variant updated at": processAsIgnored(), + // This field should be ignored as it's redundant (variant already belongs to product) + "variant product id": processAsIgnored(), } /** diff --git a/packages/core/utils/src/totals/__tests__/totals.ts b/packages/core/utils/src/totals/__tests__/totals.ts index 443a3a24b137a..01351de969d90 100644 --- a/packages/core/utils/src/totals/__tests__/totals.ts +++ b/packages/core/utils/src/totals/__tests__/totals.ts @@ -124,8 +124,8 @@ describe("Total calculation", function () { adjustments: [ { amount: 10, - total: 11, subtotal: 10, + total: 11, }, ], subtotal: 100, @@ -244,16 +244,16 @@ describe("Total calculation", function () { adjustments: [ { amount: 9, - subtotal: 8.181818181818182, - total: 9, + subtotal: 9, + total: 9.9, }, ], subtotal: 90, total: 89.1, original_total: 99, - discount_total: 9, + discount_total: 9.9, discount_subtotal: 9, - discount_tax_total: 0.8181818181818182, + discount_tax_total: 0.9, tax_total: 8.1, original_tax_total: 9, }, @@ -298,17 +298,17 @@ describe("Total calculation", function () { adjustments: [ { amount: 9, - subtotal: 8.181818181818182, - total: 9, + subtotal: 9, + total: 9.9, }, ], amount: 99, subtotal: 90, total: 89.1, original_total: 99, - discount_total: 9, + discount_total: 9.9, discount_subtotal: 9, - discount_tax_total: 0.8181818181818182, + discount_tax_total: 0.9, tax_total: 8.1, original_tax_total: 9, }, @@ -342,9 +342,9 @@ describe("Total calculation", function () { total: 191.4, subtotal: 198, tax_total: 17.4, - discount_total: 24.6, + discount_total: 26.4, discount_subtotal: 24, - discount_tax_total: 2.2363636363636363, + discount_tax_total: 2.4, original_total: 217.8, original_tax_total: 19.8, item_total: 95.7, @@ -562,7 +562,7 @@ describe("Total calculation", function () { * TAX INCLUSIVE CART * * Total price -> 120 tax inclusive - * Fixed discount -> 10 tax inclusive + * Fixed discount -> 10 tax inclusive total (which results in a subtotal of 8.33 of the discount) * Tax rate -> 20% */ @@ -574,8 +574,7 @@ describe("Total calculation", function () { is_tax_inclusive: true, adjustments: [ { - amount: 10, - is_tax_inclusive: true, + amount: 8.333333333333334, }, ], tax_lines: [ @@ -615,8 +614,7 @@ describe("Total calculation", function () { ], adjustments: [ { - is_tax_inclusive: true, - amount: 10, // <- amount is tax inclusive so it's equal to total + amount: 8.333333333333334, subtotal: 8.333333333333334, total: 10, }, @@ -625,7 +623,7 @@ describe("Total calculation", function () { ], subtotal: 100, tax_total: 18.333333333333332, - total: 110, // total is 120 - 10 tax inclusive discount + total: 110, original_item_subtotal: 100, original_item_tax_total: 20, @@ -882,4 +880,338 @@ describe("Total calculation", function () { credit_line_total: 40, }) }) + + it("should calculate carts with items + taxes + adjustments", function () { + const cart = { + items: [ + { + unit_price: 119, + quantity: 1, + tax_lines: [ + { + rate: 19, + }, + ], + adjustments: [ + { + amount: 119, + }, + ], + }, + ], + } + + const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart))) + + expect(serialized).toEqual({ + items: [ + { + unit_price: 119, + quantity: 1, + tax_lines: [ + { + rate: 19, + subtotal: 22.61, + }, + ], + adjustments: [ + { + amount: 119, + subtotal: 119, + total: 141.61, + }, + ], + subtotal: 119, + total: 0, + original_total: 141.61, + discount_total: 141.61, + discount_subtotal: 119, + discount_tax_total: 22.61, + tax_total: 0, + original_tax_total: 22.61, + }, + ], + total: 0, + subtotal: 119, + tax_total: 0, + discount_total: 141.61, + discount_subtotal: 119, + discount_tax_total: 22.61, + original_total: 141.61, + original_tax_total: 22.61, + item_total: 0, + item_subtotal: 119, + item_tax_total: 0, + original_item_total: 141.61, + original_item_subtotal: 119, + original_item_tax_total: 22.61, + credit_line_subtotal: 0, + credit_line_tax_total: 0, + credit_line_total: 0, + }) + }) + + it("should calculate carts with items + taxes with is_tax_inclusive", function () { + const cartWithTax = { + items: [ + { + unit_price: 119, + quantity: 1, + is_tax_inclusive: true, + tax_lines: [ + { + rate: 19, + }, + ], + }, + ], + } + + const cartWithoutTax = { + items: [ + { + unit_price: 119, + quantity: 1, + is_tax_inclusive: false, + tax_lines: [ + { + rate: 19, + }, + ], + }, + ], + } + + const cartMixed = { + items: [...cartWithTax.items, ...cartWithoutTax.items], + } + + const serializedWith = JSON.parse( + JSON.stringify(decorateCartTotals(cartWithTax)) + ) + const serializedWithout = JSON.parse( + JSON.stringify(decorateCartTotals(cartWithoutTax)) + ) + const serializedMixed = JSON.parse( + JSON.stringify(decorateCartTotals(cartMixed)) + ) + + expect(serializedWith).toEqual({ + credit_line_subtotal: 0, + credit_line_tax_total: 0, + credit_line_total: 0, + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + item_subtotal: 100, + item_tax_total: 19, + item_total: 119, + items: [ + { + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + is_tax_inclusive: true, + original_tax_total: 19, + original_total: 119, + quantity: 1, + subtotal: 100, + tax_lines: [ + { + rate: 19, + subtotal: 19, + total: 19, + }, + ], + tax_total: 19, + total: 119, + unit_price: 119, + }, + ], + original_item_subtotal: 100, + original_item_tax_total: 19, + original_item_total: 119, + original_tax_total: 19, + original_total: 119, + subtotal: 100, + tax_total: 19, + total: 119, + }) + + expect(serializedWithout).toEqual({ + credit_line_subtotal: 0, + credit_line_tax_total: 0, + credit_line_total: 0, + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + item_subtotal: 119, + item_tax_total: 22.61, + item_total: 141.61, + items: [ + { + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + is_tax_inclusive: false, + original_tax_total: 22.61, + original_total: 141.61, + quantity: 1, + subtotal: 119, + tax_lines: [ + { + rate: 19, + subtotal: 22.61, + total: 22.61, + }, + ], + tax_total: 22.61, + total: 141.61, + unit_price: 119, + }, + ], + original_item_subtotal: 119, + original_item_tax_total: 22.61, + original_item_total: 141.61, + original_tax_total: 22.61, + original_total: 141.61, + subtotal: 119, + tax_total: 22.61, + total: 141.61, + }) + + expect(serializedMixed).toEqual({ + credit_line_subtotal: 0, + credit_line_tax_total: 0, + credit_line_total: 0, + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + item_subtotal: 219, + item_tax_total: 41.61, + item_total: 260.61, + items: [ + { + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + is_tax_inclusive: true, + original_tax_total: 19, + original_total: 119, + quantity: 1, + subtotal: 100, + tax_lines: [ + { + rate: 19, + subtotal: 19, + total: 19, + }, + ], + tax_total: 19, + total: 119, + unit_price: 119, + }, + { + discount_subtotal: 0, + discount_tax_total: 0, + discount_total: 0, + is_tax_inclusive: false, + original_tax_total: 22.61, + original_total: 141.61, + quantity: 1, + subtotal: 119, + tax_lines: [ + { + rate: 19, + subtotal: 22.61, + total: 22.61, + }, + ], + tax_total: 22.61, + total: 141.61, + unit_price: 119, + }, + ], + original_item_subtotal: 219, + original_item_tax_total: 41.61, + original_item_total: 260.61, + original_tax_total: 41.61, + original_total: 260.61, + subtotal: 219, + tax_total: 41.61, + total: 260.61, + }) + }) + + it("should calculate tax inclusive carts with items + taxes with tax inclusive adjustments", function () { + const cart = { + items: [ + { + unit_price: 119, + quantity: 1, + is_tax_inclusive: true, + adjustments: [ + { + amount: 100, + }, + ], + tax_lines: [ + { + rate: 19, + }, + ], + }, + ], + } + + const serialized = JSON.parse(JSON.stringify(decorateCartTotals(cart))) + + expect(serialized).toEqual({ + credit_line_subtotal: 0, + credit_line_tax_total: 0, + credit_line_total: 0, + discount_subtotal: 100, + discount_tax_total: 19, + discount_total: 119, + item_subtotal: 100, + item_tax_total: 0, + item_total: 0, + items: [ + { + adjustments: [ + { + amount: 100, + subtotal: 100, + total: 119, + }, + ], + discount_subtotal: 100, + discount_tax_total: 19, + discount_total: 119, + is_tax_inclusive: true, + original_tax_total: 19, + original_total: 119, + quantity: 1, + subtotal: 100, + tax_lines: [ + { + rate: 19, + subtotal: 19, + }, + ], + tax_total: 0, + total: 0, + unit_price: 119, + }, + ], + original_item_subtotal: 100, + original_item_tax_total: 19, + original_item_total: 119, + original_tax_total: 19, + original_total: 119, + subtotal: 100, + tax_total: 0, + total: 0, + }) + }) }) diff --git a/packages/core/utils/src/totals/adjustment/index.ts b/packages/core/utils/src/totals/adjustment/index.ts index 860f052e80b70..f9e044026413f 100644 --- a/packages/core/utils/src/totals/adjustment/index.ts +++ b/packages/core/utils/src/totals/adjustment/index.ts @@ -5,11 +5,9 @@ import { MathBN } from "../math" export function calculateAdjustmentTotal({ adjustments, - includesTax, taxRate, }: { adjustments: Pick[] - includesTax?: boolean taxRate?: BigNumberInput }) { // the sum of all adjustment amounts excluding tax @@ -24,35 +22,22 @@ export function calculateAdjustmentTotal({ continue } - const adjustmentAmount = MathBN.convert(adj.amount) + const adjustmentSubtotal = + isDefined(taxRate) && adj.is_tax_inclusive + ? MathBN.div(adj.amount, MathBN.add(1, taxRate)) + : adj.amount - if (adj.is_tax_inclusive && isDefined(taxRate)) { - adjustmentsSubtotal = MathBN.add( - adjustmentsSubtotal, - MathBN.div(adjustmentAmount, MathBN.add(1, taxRate)) - ) - } else { - adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentAmount) - } - - if (isDefined(taxRate)) { - const adjustmentSubtotal = includesTax - ? MathBN.div(adjustmentAmount, MathBN.add(1, taxRate)) - : adjustmentAmount + const adjustmentTaxTotal = isDefined(taxRate) + ? MathBN.mult(adjustmentSubtotal, taxRate) + : 0 + const adjustmentTotal = MathBN.add(adjustmentSubtotal, adjustmentTaxTotal) - const adjustmentTaxTotal = MathBN.mult(adjustmentSubtotal, taxRate) - const adjustmentTotal = MathBN.add(adjustmentSubtotal, adjustmentTaxTotal) + adjustmentsSubtotal = MathBN.add(adjustmentsSubtotal, adjustmentSubtotal) + adjustmentsTaxTotal = MathBN.add(adjustmentsTaxTotal, adjustmentTaxTotal) + adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentTotal) - adj["subtotal"] = new BigNumber(adjustmentSubtotal) - adj["total"] = new BigNumber(adjustmentTotal) - - adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentTotal) - adjustmentsTaxTotal = MathBN.add(adjustmentsTaxTotal, adjustmentTaxTotal) - } else { - adj["subtotal"] = new BigNumber(adjustmentAmount) - adj["adjustmentAmount"] = new BigNumber(adjustmentAmount) - adjustmentsTotal = MathBN.add(adjustmentsTotal, adjustmentAmount) - } + adj["subtotal"] = new BigNumber(adjustmentsSubtotal) + adj["total"] = new BigNumber(adjustmentsTotal) } return { diff --git a/packages/core/utils/src/totals/line-item/index.ts b/packages/core/utils/src/totals/line-item/index.ts index dbe97099e5235..2e1f2ad74499a 100644 --- a/packages/core/utils/src/totals/line-item/index.ts +++ b/packages/core/utils/src/totals/line-item/index.ts @@ -16,7 +16,7 @@ export interface GetItemTotalInput { quantity: BigNumber is_tax_inclusive?: boolean tax_lines?: Pick[] - adjustments?: Pick[] + adjustments?: Pick[] detail?: { fulfilled_quantity: BigNumber delivered_quantity: BigNumber @@ -133,7 +133,6 @@ function getLineItemTotals( adjustmentsTaxTotal: discountTaxTotal, } = calculateAdjustmentTotal({ adjustments: item.adjustments || [], - includesTax: isTaxInclusive, taxRate: sumTaxRate, }) diff --git a/packages/core/utils/src/totals/promotion/index.ts b/packages/core/utils/src/totals/promotion/index.ts index 8a443cd3fdaa7..15d9c5bdff883 100644 --- a/packages/core/utils/src/totals/promotion/index.ts +++ b/packages/core/utils/src/totals/promotion/index.ts @@ -5,41 +5,43 @@ import { } from "../../promotion" import { MathBN } from "../math" -function getPromotionValueForPercentage(promotion, lineItemTotal) { - return MathBN.mult(MathBN.div(promotion.value, 100), lineItemTotal) +function getPromotionValueForPercentage(promotion, lineItemAmount) { + return MathBN.convert( + MathBN.mult(MathBN.div(promotion.value, 100), lineItemAmount), + 2 + ) } -function getPromotionValueForFixed(promotion, itemTotal, allItemsTotal) { +function getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) { if (promotion.allocation === ApplicationMethodAllocation.ACROSS) { const promotionValueForItem = MathBN.mult( - MathBN.div(itemTotal, allItemsTotal), + MathBN.div(lineItemAmount, lineItemsAmount), promotion.value ) - if (MathBN.lte(promotionValueForItem, itemTotal)) { + if (MathBN.lte(promotionValueForItem, lineItemAmount)) { return promotionValueForItem } const percentage = MathBN.div( - MathBN.mult(itemTotal, 100), + MathBN.mult(lineItemAmount, 100), promotionValueForItem ) - return MathBN.mult( - promotionValueForItem, - MathBN.div(percentage, 100) - ).precision(4) + return MathBN.convert( + MathBN.mult(promotionValueForItem, MathBN.div(percentage, 100)), + 2 + ) } - return promotion.value } -export function getPromotionValue(promotion, lineItemTotal, lineItemsTotal) { +export function getPromotionValue(promotion, lineItemAmount, lineItemsAmount) { if (promotion.type === ApplicationMethodType.PERCENTAGE) { - return getPromotionValueForPercentage(promotion, lineItemTotal) + return getPromotionValueForPercentage(promotion, lineItemAmount) } - return getPromotionValueForFixed(promotion, lineItemTotal, lineItemsTotal) + return getPromotionValueForFixed(promotion, lineItemAmount, lineItemsAmount) } export function getApplicableQuantity(lineItem, maxQuantity) { @@ -50,14 +52,18 @@ export function getApplicableQuantity(lineItem, maxQuantity) { return lineItem.quantity } -function getLineItemUnitPrice(lineItem) { +function getLineItemSubtotal(lineItem) { return MathBN.div(lineItem.subtotal, lineItem.quantity) } +function getLineItemOriginalTotal(lineItem) { + return MathBN.div(lineItem.original_total, lineItem.quantity) +} + export function calculateAdjustmentAmountFromPromotion( lineItem, promotion, - lineItemsTotal: BigNumberInput = 0 + lineItemsAmount: BigNumberInput = 0 ) { /* For a promotion with an across allocation, we consider not only the line item total, but also the total of all other line items in the order. @@ -89,20 +95,26 @@ export function calculateAdjustmentAmountFromPromotion( */ if (promotion.allocation === ApplicationMethodAllocation.ACROSS) { const quantity = getApplicableQuantity(lineItem, promotion.max_quantity) - const lineItemTotal = MathBN.mult(getLineItemUnitPrice(lineItem), quantity) - const applicableTotal = MathBN.sub(lineItemTotal, promotion.applied_value) - if (MathBN.lte(applicableTotal, 0)) { - return applicableTotal + const lineItemAmount = MathBN.mult( + promotion.is_tax_inclusive + ? getLineItemOriginalTotal(lineItem) + : getLineItemSubtotal(lineItem), + quantity + ) + const applicableAmount = MathBN.sub(lineItemAmount, promotion.applied_value) + + if (MathBN.lte(applicableAmount, 0)) { + return applicableAmount } const promotionValue = getPromotionValue( promotion, - applicableTotal, - lineItemsTotal + applicableAmount, + lineItemsAmount ) - return MathBN.min(promotionValue, applicableTotal) + return MathBN.min(promotionValue, applicableAmount) } /* @@ -124,26 +136,32 @@ export function calculateAdjustmentAmountFromPromotion( We then apply whichever is lower. */ - const remainingItemTotal = MathBN.sub( - lineItem.subtotal, + const remainingItemAmount = MathBN.sub( + promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal, promotion.applied_value ) - const unitPrice = MathBN.div(lineItem.subtotal, lineItem.quantity) - const maximumPromotionTotal = MathBN.mult( - unitPrice, + const itemAmount = MathBN.div( + promotion.is_tax_inclusive ? lineItem.original_total : lineItem.subtotal, + lineItem.quantity + ) + const maximumPromotionAmount = MathBN.mult( + itemAmount, promotion.max_quantity ?? MathBN.convert(1) ) - const applicableTotal = MathBN.min(remainingItemTotal, maximumPromotionTotal) + const applicableAmount = MathBN.min( + remainingItemAmount, + maximumPromotionAmount + ) - if (MathBN.lte(applicableTotal, 0)) { + if (MathBN.lte(applicableAmount, 0)) { return MathBN.convert(0) } const promotionValue = getPromotionValue( promotion, - applicableTotal, - lineItemsTotal + applicableAmount, + lineItemsAmount ) - return MathBN.min(promotionValue, applicableTotal) + return MathBN.min(promotionValue, applicableAmount) } diff --git a/packages/core/utils/src/totals/shipping-method/index.ts b/packages/core/utils/src/totals/shipping-method/index.ts index 8736eb9267e48..1c7c68adcee85 100644 --- a/packages/core/utils/src/totals/shipping-method/index.ts +++ b/packages/core/utils/src/totals/shipping-method/index.ts @@ -72,7 +72,6 @@ export function getShippingMethodTotals( adjustmentsTaxTotal: discountsTaxTotal, } = calculateAdjustmentTotal({ adjustments: shippingMethod.adjustments || [], - includesTax: isTaxInclusive, taxRate: sumTaxRate, }) diff --git a/packages/core/workflows-sdk/CHANGELOG.md b/packages/core/workflows-sdk/CHANGELOG.md index 53e9fbd55d81c..95526004da639 100644 --- a/packages/core/workflows-sdk/CHANGELOG.md +++ b/packages/core/workflows-sdk/CHANGELOG.md @@ -1,5 +1,17 @@ # @medusajs/workflows-sdk +## 2.8.8 + +### Patch Changes + +- [#12926](https://github.com/medusajs/medusa/pull/12926) [`1bd455bc7bd32b0fda8c808b203ad341253f095d`](https://github.com/medusajs/medusa/commit/1bd455bc7bd32b0fda8c808b203ad341253f095d) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - fix(workflows-sdk): step config name before default step + +- Updated dependencies [[`e74044af4de213ba6326a8fb2b5d02e5875b8a4a`](https://github.com/medusajs/medusa/commit/e74044af4de213ba6326a8fb2b5d02e5875b8a4a), [`468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8`](https://github.com/medusajs/medusa/commit/468b81c2cbdbc24b26e31bf6e347d3633a4fb4f8), [`919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5`](https://github.com/medusajs/medusa/commit/919c53e44e2c7bb16bc513b5c96c93ac47bd6ce5), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47), [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5)]: + - @medusajs/orchestration@2.8.8 + - @medusajs/types@2.8.8 + - @medusajs/utils@2.8.8 + - @medusajs/modules-sdk@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/core/workflows-sdk/package.json b/packages/core/workflows-sdk/package.json index d32556abe265f..f00431523d077 100644 --- a/packages/core/workflows-sdk/package.json +++ b/packages/core/workflows-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/workflows-sdk", - "version": "2.8.7", + "version": "2.8.8", "description": "Set of workflows tooling for Medusa", "main": "dist/index.js", "exports": { @@ -39,13 +39,13 @@ "pg": "^8.13.0", "rimraf": "^5.0.1", "typescript": "^5.6.2", - "zod": "3.22.4" + "zod": "3.25.76" }, "dependencies": { - "@medusajs/modules-sdk": "2.8.7", - "@medusajs/orchestration": "2.8.7", - "@medusajs/types": "2.8.7", - "@medusajs/utils": "2.8.7", + "@medusajs/modules-sdk": "2.8.8", + "@medusajs/orchestration": "2.8.8", + "@medusajs/types": "2.8.8", + "@medusajs/utils": "2.8.8", "ulid": "^2.3.0" }, "peerDependencies": { @@ -56,7 +56,7 @@ "awilix": "^8.0.1", "express": "^4.21.0", "pg": "^8.13.0", - "zod": "3.22.4" + "zod": "3.25.76" }, "scripts": { "build": "rimraf dist && tsc --build", diff --git a/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts b/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts index d82ed7e6d6b07..68fb2bbbf95bb 100644 --- a/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts +++ b/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts @@ -1408,6 +1408,41 @@ describe("Workflow composer", function () { expect(logStepFn).toHaveBeenCalledTimes(0) }) + it("should run same step multiple times", async () => { + const log = createStep("log", async (input: number) => { + return new StepResponse(input) + }) + + const sameStepWorkflow = createWorkflow("fake-workflow", () => { + const a = log(1).config({ name: "aaaa" }) + const b = log(2) // without config on purpose + const c = log(3).config({ name: "cccc" }) + return new WorkflowResponse([a, b, c]) + }) + + const sameStepWorkflow2 = createWorkflow("fake-workflow-2", () => { + const a = log(1) + const b = log(2).config({ name: "bbbb" }) + const c = log(3).config({ name: "cccc" }) + return new WorkflowResponse([a, b, c]) + }) + + const sameStepWorkflow3 = createWorkflow("fake-workflow-3", () => { + const a = log(1).config({ name: "aaaa" }) + const b = log(2).config({ name: "bbbb" }) + const c = log(3) + return new WorkflowResponse([a, b, c]) + }) + + const { result } = await sameStepWorkflow().run() + const { result: result2 } = await sameStepWorkflow2().run() + const { result: result3 } = await sameStepWorkflow3().run() + + expect(result).toEqual([1, 2, 3]) + expect(result2).toEqual([1, 2, 3]) + expect(result3).toEqual([1, 2, 3]) + }) + it("should skip steps until the named step in case of permanent failure", async () => { const logStepFn = jest.fn(async ({ input }: { input: object }) => { return new StepResponse("done and returned") diff --git a/packages/core/workflows-sdk/src/utils/composer/create-step.ts b/packages/core/workflows-sdk/src/utils/composer/create-step.ts index 1b8020ff8775e..7991bc5301709 100644 --- a/packages/core/workflows-sdk/src/utils/composer/create-step.ts +++ b/packages/core/workflows-sdk/src/utils/composer/create-step.ts @@ -143,9 +143,9 @@ export function applyStep< this.isAsync ||= !!(stepConfig.async || stepConfig.compensateAsync) - if (!this.handlers.has(stepName)) { - this.handlers.set(stepName, handler) - } + this.overriddenHandler.set(stepName, this.handlers.get(stepName)!) + + this.handlers.set(stepName, handler) const ret = { __type: OrchestrationUtils.SymbolWorkflowStep, @@ -177,7 +177,7 @@ export function applyStep< newConfig.nested ||= newConfig.async } - delete localConfig.name + delete newConfig.name const handler = createStepHandler.bind(this)({ stepName: newStepName, @@ -188,6 +188,9 @@ export function applyStep< wrapAsyncHandler(newConfig, handler) + this.handlers.set(stepName, this.overriddenHandler.get(stepName)!) + this.overriddenHandler.delete(stepName) + this.handlers.set(newStepName, handler) this.flow.replaceAction(stepConfig.uuid!, newStepName, newConfig) diff --git a/packages/core/workflows-sdk/src/utils/composer/create-workflow.ts b/packages/core/workflows-sdk/src/utils/composer/create-workflow.ts index 8a512f5d1a207..7807d9d4fefc3 100644 --- a/packages/core/workflows-sdk/src/utils/composer/create-workflow.ts +++ b/packages/core/workflows-sdk/src/utils/composer/create-workflow.ts @@ -117,6 +117,7 @@ export function createWorkflow( flow: WorkflowManager.getEmptyTransactionDefinition(), isAsync: false, handlers, + overriddenHandler: new Map(), hooks_: { declared: [], registered: [], diff --git a/packages/core/workflows-sdk/src/utils/composer/type.ts b/packages/core/workflows-sdk/src/utils/composer/type.ts index c4a51a0d4c454..5b343e9b65108 100644 --- a/packages/core/workflows-sdk/src/utils/composer/type.ts +++ b/packages/core/workflows-sdk/src/utils/composer/type.ts @@ -113,6 +113,7 @@ export type CreateWorkflowComposerContext = { flow: OrchestratorBuilder isAsync: boolean handlers: WorkflowHandler + overriddenHandler: WorkflowHandler stepBinder: ( fn: StepFunctionResult ) => WorkflowData diff --git a/packages/design-system/icons/CHANGELOG.md b/packages/design-system/icons/CHANGELOG.md index b168ba503d838..e0fe4436b5d8f 100644 --- a/packages/design-system/icons/CHANGELOG.md +++ b/packages/design-system/icons/CHANGELOG.md @@ -1,5 +1,7 @@ # @medusajs/icons +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/design-system/icons/package.json b/packages/design-system/icons/package.json index 0e7d4d239fc7f..1457932c4889d 100644 --- a/packages/design-system/icons/package.json +++ b/packages/design-system/icons/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/icons", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa UI React icon library", "author": "Kasper Kristensen ", "license": "MIT", @@ -29,7 +29,7 @@ }, "devDependencies": { "@atomico/rollup-plugin-sizes": "^1.1.4", - "@medusajs/toolbox": "2.8.7", + "@medusajs/toolbox": "2.8.8", "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-replace": "^5.0.2", "@testing-library/dom": "^9.3.1", diff --git a/packages/design-system/icons/src/components/WIP.tsx b/packages/design-system/icons/src/components/WIP.tsx new file mode 100644 index 0000000000000..c6daefcd2df1c --- /dev/null +++ b/packages/design-system/icons/src/components/WIP.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import type { IconProps } from "../types" +const WIP = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +WIP.displayName = "WIP" +export default WIP diff --git a/packages/design-system/icons/src/components/__tests__/WIP.spec.tsx b/packages/design-system/icons/src/components/__tests__/WIP.spec.tsx new file mode 100644 index 0000000000000..77d0ff8fc8a97 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/WIP.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import WIP from "../WIP" + + describe("WIP", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/badge-check.spec.tsx b/packages/design-system/icons/src/components/__tests__/badge-check.spec.tsx new file mode 100644 index 0000000000000..749e736506c04 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/badge-check.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import BadgeCheck from "../badge-check" + + describe("BadgeCheck", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/broom-sparkle-solid.spec.tsx b/packages/design-system/icons/src/components/__tests__/broom-sparkle-solid.spec.tsx new file mode 100644 index 0000000000000..c4397c28e9764 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/broom-sparkle-solid.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import BroomSparkleSolid from "../broom-sparkle-solid" + + describe("BroomSparkleSolid", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/card-sparkle.spec.tsx b/packages/design-system/icons/src/components/__tests__/card-sparkle.spec.tsx new file mode 100644 index 0000000000000..f376e1b83bcf7 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/card-sparkle.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import CardSparkle from "../card-sparkle" + + describe("CardSparkle", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/chef-hat.spec.tsx b/packages/design-system/icons/src/components/__tests__/chef-hat.spec.tsx new file mode 100644 index 0000000000000..c6c08daca63a5 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/chef-hat.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import ChefHat from "../chef-hat" + + describe("ChefHat", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/circle-half-dotted-clock.spec.tsx b/packages/design-system/icons/src/components/__tests__/circle-half-dotted-clock.spec.tsx new file mode 100644 index 0000000000000..9f90c54d9e69c --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/circle-half-dotted-clock.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import CircleHalfDottedClock from "../circle-half-dotted-clock" + + describe("CircleHalfDottedClock", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/circle-plus.spec.tsx b/packages/design-system/icons/src/components/__tests__/circle-plus.spec.tsx new file mode 100644 index 0000000000000..976dc611b6567 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/circle-plus.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import CirclePlus from "../circle-plus" + + describe("CirclePlus", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/cloud-solid.spec.tsx b/packages/design-system/icons/src/components/__tests__/cloud-solid.spec.tsx new file mode 100644 index 0000000000000..18d2613b89f1d --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/cloud-solid.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import CloudSolid from "../cloud-solid" + + describe("CloudSolid", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/code-pull-request.spec.tsx b/packages/design-system/icons/src/components/__tests__/code-pull-request.spec.tsx new file mode 100644 index 0000000000000..0c3ae2c4a7646 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/code-pull-request.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import CodePullRequest from "../code-pull-request" + + describe("CodePullRequest", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/draft orders.spec.tsx b/packages/design-system/icons/src/components/__tests__/draft orders.spec.tsx new file mode 100644 index 0000000000000..476972a1406e8 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/draft orders.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import DraftOrders from "../draft orders" + + describe("DraftOrders", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/face-crossed-out-eyes.spec.tsx b/packages/design-system/icons/src/components/__tests__/face-crossed-out-eyes.spec.tsx new file mode 100644 index 0000000000000..62f3bef38e209 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/face-crossed-out-eyes.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import FaceCrossedOutEyes from "../face-crossed-out-eyes" + + describe("FaceCrossedOutEyes", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/fire-solid.spec.tsx b/packages/design-system/icons/src/components/__tests__/fire-solid.spec.tsx new file mode 100644 index 0000000000000..ea84b8b06fac3 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/fire-solid.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import FireSolid from "../fire-solid" + + describe("FireSolid", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/fire.spec.tsx b/packages/design-system/icons/src/components/__tests__/fire.spec.tsx new file mode 100644 index 0000000000000..2649b7b85dc7d --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/fire.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Fire from "../fire" + + describe("Fire", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/funnel-plus.spec.tsx b/packages/design-system/icons/src/components/__tests__/funnel-plus.spec.tsx new file mode 100644 index 0000000000000..7319ae912b8d8 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/funnel-plus.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import FunnelPlus from "../funnel-plus" + + describe("FunnelPlus", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/gauge-sparkle.spec.tsx b/packages/design-system/icons/src/components/__tests__/gauge-sparkle.spec.tsx new file mode 100644 index 0000000000000..44052b23f2c5c --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/gauge-sparkle.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import GaugeSparkle from "../gauge-sparkle" + + describe("GaugeSparkle", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/ghost-worried.spec.tsx b/packages/design-system/icons/src/components/__tests__/ghost-worried.spec.tsx new file mode 100644 index 0000000000000..826521645f1a7 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/ghost-worried.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import GhostWorried from "../ghost-worried" + + describe("GhostWorried", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/gift cards.spec.tsx b/packages/design-system/icons/src/components/__tests__/gift cards.spec.tsx new file mode 100644 index 0000000000000..dff8b57013c5b --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/gift cards.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import GiftCards from "../gift cards" + + describe("GiftCards", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/id-badge.spec.tsx b/packages/design-system/icons/src/components/__tests__/id-badge.spec.tsx new file mode 100644 index 0000000000000..bc53910dbe077 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/id-badge.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import IdBadge from "../id-badge" + + describe("IdBadge", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/linear.spec.tsx b/packages/design-system/icons/src/components/__tests__/linear.spec.tsx new file mode 100644 index 0000000000000..516b9001e938b --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/linear.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Linear from "../linear" + + describe("Linear", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/markdown-solid.spec.tsx b/packages/design-system/icons/src/components/__tests__/markdown-solid.spec.tsx new file mode 100644 index 0000000000000..31ab5529f5319 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/markdown-solid.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import MarkdownSolid from "../markdown-solid" + + describe("MarkdownSolid", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/meilisearch.spec.tsx b/packages/design-system/icons/src/components/__tests__/meilisearch.spec.tsx new file mode 100644 index 0000000000000..1b02993512582 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/meilisearch.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Meilisearch from "../meilisearch" + + describe("Meilisearch", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/member-bronze-badge.spec.tsx b/packages/design-system/icons/src/components/__tests__/member-bronze-badge.spec.tsx new file mode 100644 index 0000000000000..fce6579d9a839 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/member-bronze-badge.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import MemberBronzeBadge from "../member-bronze-badge" + + describe("MemberBronzeBadge", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/member-gold-badge.spec.tsx b/packages/design-system/icons/src/components/__tests__/member-gold-badge.spec.tsx new file mode 100644 index 0000000000000..de99d42a84b68 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/member-gold-badge.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import MemberGoldBadge from "../member-gold-badge" + + describe("MemberGoldBadge", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/member-silver-badge.spec.tsx b/packages/design-system/icons/src/components/__tests__/member-silver-badge.spec.tsx new file mode 100644 index 0000000000000..fea6ebc3d1a08 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/member-silver-badge.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import MemberSilverBadge from "../member-silver-badge" + + describe("MemberSilverBadge", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/minus-badge.spec.tsx b/packages/design-system/icons/src/components/__tests__/minus-badge.spec.tsx new file mode 100644 index 0000000000000..386e423a54a90 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/minus-badge.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import MinusBadge from "../minus-badge" + + describe("MinusBadge", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/pause.spec.tsx b/packages/design-system/icons/src/components/__tests__/pause.spec.tsx new file mode 100644 index 0000000000000..ae1713ef941f9 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/pause.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Pause from "../pause" + + describe("Pause", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/paypal.spec.tsx b/packages/design-system/icons/src/components/__tests__/paypal.spec.tsx new file mode 100644 index 0000000000000..d40d12e420240 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/paypal.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Paypal from "../paypal" + + describe("Paypal", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/payphone.spec.tsx b/packages/design-system/icons/src/components/__tests__/payphone.spec.tsx new file mode 100644 index 0000000000000..b8e137fc61928 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/payphone.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Payphone from "../payphone" + + describe("Payphone", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-0.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-0.spec.tsx new file mode 100644 index 0000000000000..d999154560dd0 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-0.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress0 from "../progress-0" + + describe("Progress0", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-100.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-100.spec.tsx new file mode 100644 index 0000000000000..f35edec74f713 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-100.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress100 from "../progress-100" + + describe("Progress100", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-15.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-15.spec.tsx new file mode 100644 index 0000000000000..c7ad9899268a3 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-15.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress15 from "../progress-15" + + describe("Progress15", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-30.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-30.spec.tsx new file mode 100644 index 0000000000000..660d7084d934f --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-30.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress30 from "../progress-30" + + describe("Progress30", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-45.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-45.spec.tsx new file mode 100644 index 0000000000000..25813d49feae9 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-45.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress45 from "../progress-45" + + describe("Progress45", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-60.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-60.spec.tsx new file mode 100644 index 0000000000000..12f8dff567fe6 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-60.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress60 from "../progress-60" + + describe("Progress60", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-75.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-75.spec.tsx new file mode 100644 index 0000000000000..81294ef3791ee --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-75.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress75 from "../progress-75" + + describe("Progress75", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/progress-90.spec.tsx b/packages/design-system/icons/src/components/__tests__/progress-90.spec.tsx new file mode 100644 index 0000000000000..3a8aba15d9c09 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/progress-90.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Progress90 from "../progress-90" + + describe("Progress90", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/resend.spec.tsx b/packages/design-system/icons/src/components/__tests__/resend.spec.tsx new file mode 100644 index 0000000000000..f518619e642d9 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/resend.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Resend from "../resend" + + describe("Resend", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/shippo.spec.tsx b/packages/design-system/icons/src/components/__tests__/shippo.spec.tsx new file mode 100644 index 0000000000000..d07acfbb704ac --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/shippo.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Shippo from "../shippo" + + describe("Shippo", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/sidebar-left-filled.spec.tsx b/packages/design-system/icons/src/components/__tests__/sidebar-left-filled.spec.tsx new file mode 100644 index 0000000000000..3317c122a6317 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/sidebar-left-filled.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import SidebarLeftFilled from "../sidebar-left-filled" + + describe("SidebarLeftFilled", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/sidebar-right-filled.spec.tsx b/packages/design-system/icons/src/components/__tests__/sidebar-right-filled.spec.tsx new file mode 100644 index 0000000000000..2a06959571e5d --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/sidebar-right-filled.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import SidebarRightFilled from "../sidebar-right-filled" + + describe("SidebarRightFilled", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/sparkle-2-solid.spec.tsx b/packages/design-system/icons/src/components/__tests__/sparkle-2-solid.spec.tsx new file mode 100644 index 0000000000000..fc6e77810211d --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/sparkle-2-solid.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Sparkle2Solid from "../sparkle-2-solid" + + describe("Sparkle2Solid", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/store credits.spec.tsx b/packages/design-system/icons/src/components/__tests__/store credits.spec.tsx new file mode 100644 index 0000000000000..cd2dfea118278 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/store credits.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import StoreCredits from "../store credits" + + describe("StoreCredits", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/telegram.spec.tsx b/packages/design-system/icons/src/components/__tests__/telegram.spec.tsx new file mode 100644 index 0000000000000..8048db2fad88f --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/telegram.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import Telegram from "../telegram" + + describe("Telegram", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/__tests__/wish lists.spec.tsx b/packages/design-system/icons/src/components/__tests__/wish lists.spec.tsx new file mode 100644 index 0000000000000..7555aba0889a1 --- /dev/null +++ b/packages/design-system/icons/src/components/__tests__/wish lists.spec.tsx @@ -0,0 +1,17 @@ + import * as React from "react" + import { cleanup, render, screen } from "@testing-library/react" + + import WishLists from "../wish lists" + + describe("WishLists", () => { + it("should render the icon without errors", async () => { + render() + + + const svgElement = screen.getByTestId("icon") + + expect(svgElement).toBeInTheDocument() + + cleanup() + }) + }) \ No newline at end of file diff --git a/packages/design-system/icons/src/components/adjustments-done.tsx b/packages/design-system/icons/src/components/adjustments-done.tsx index 32e02151da3c6..90109e390e3ea 100644 --- a/packages/design-system/icons/src/components/adjustments-done.tsx +++ b/packages/design-system/icons/src/components/adjustments-done.tsx @@ -16,13 +16,14 @@ const AdjustmentsDone = React.forwardRef( fill={color} d="M8.5 2.528h-.778a.75.75 0 1 0 0 1.5h1.08a4 4 0 0 1-.302-1.5M10.695 6.07a4 4 0 0 1-1.21-.94.75.75 0 0 0-.29.592V6.75H1.5a.75.75 0 0 0 0 1.5h7.694v1.028a.75.75 0 0 0 1.5 0zM5.806 1.5a.75.75 0 1 0-1.5 0v1.028H1.5a.75.75 0 0 0 0 1.5h2.806v1.028a.75.75 0 0 0 1.5 0V1.5M12.611 6.75a.75.75 0 0 0 0 1.5h.889a.75.75 0 0 0 0-1.5zM6.972 11.722a.75.75 0 0 1 .75-.75H13.5a.75.75 0 1 1 0 1.5H7.722a.75.75 0 0 1-.75-.75M1.5 10.972a.75.75 0 0 0 0 1.5h2.806V13.5a.75.75 0 0 0 1.5 0V9.944a.75.75 0 1 0-1.5 0v1.028z" /> - + diff --git a/packages/design-system/icons/src/components/badge-check.tsx b/packages/design-system/icons/src/components/badge-check.tsx new file mode 100644 index 0000000000000..f0b40d6d60fad --- /dev/null +++ b/packages/design-system/icons/src/components/badge-check.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import type { IconProps } from "../types" +const BadgeCheck = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +BadgeCheck.displayName = "BadgeCheck" +export default BadgeCheck diff --git a/packages/design-system/icons/src/components/bell-alert-done.tsx b/packages/design-system/icons/src/components/bell-alert-done.tsx index bcb0146c12f85..bfe7580fd3372 100644 --- a/packages/design-system/icons/src/components/bell-alert-done.tsx +++ b/packages/design-system/icons/src/components/bell-alert-done.tsx @@ -12,13 +12,14 @@ const BellAlertDone = React.forwardRef( {...props} > - + ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +BroomSparkleSolid.displayName = "BroomSparkleSolid" +export default BroomSparkleSolid diff --git a/packages/design-system/icons/src/components/card-sparkle.tsx b/packages/design-system/icons/src/components/card-sparkle.tsx new file mode 100644 index 0000000000000..b391316c171de --- /dev/null +++ b/packages/design-system/icons/src/components/card-sparkle.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import type { IconProps } from "../types" +const CardSparkle = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +CardSparkle.displayName = "CardSparkle" +export default CardSparkle diff --git a/packages/design-system/icons/src/components/chef-hat.tsx b/packages/design-system/icons/src/components/chef-hat.tsx new file mode 100644 index 0000000000000..b7361af0e8587 --- /dev/null +++ b/packages/design-system/icons/src/components/chef-hat.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import type { IconProps } from "../types" +const ChefHat = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +ChefHat.displayName = "ChefHat" +export default ChefHat diff --git a/packages/design-system/icons/src/components/circle-half-dotted-clock.tsx b/packages/design-system/icons/src/components/circle-half-dotted-clock.tsx new file mode 100644 index 0000000000000..d863225cb9002 --- /dev/null +++ b/packages/design-system/icons/src/components/circle-half-dotted-clock.tsx @@ -0,0 +1,44 @@ +import * as React from "react" +import type { IconProps } from "../types" +const CircleHalfDottedClock = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + ) + } +) +CircleHalfDottedClock.displayName = "CircleHalfDottedClock" +export default CircleHalfDottedClock diff --git a/packages/design-system/icons/src/components/circle-plus.tsx b/packages/design-system/icons/src/components/circle-plus.tsx new file mode 100644 index 0000000000000..f28c7d16e407a --- /dev/null +++ b/packages/design-system/icons/src/components/circle-plus.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import type { IconProps } from "../types" +const CirclePlus = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +CirclePlus.displayName = "CirclePlus" +export default CirclePlus diff --git a/packages/design-system/icons/src/components/cloud-solid.tsx b/packages/design-system/icons/src/components/cloud-solid.tsx new file mode 100644 index 0000000000000..80cbb0315864f --- /dev/null +++ b/packages/design-system/icons/src/components/cloud-solid.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import type { IconProps } from "../types" +const CloudSolid = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +CloudSolid.displayName = "CloudSolid" +export default CloudSolid diff --git a/packages/design-system/icons/src/components/code-commit.tsx b/packages/design-system/icons/src/components/code-commit.tsx index 5bcfaa10d9157..89212b8c73478 100644 --- a/packages/design-system/icons/src/components/code-commit.tsx +++ b/packages/design-system/icons/src/components/code-commit.tsx @@ -16,7 +16,7 @@ const CodeCommit = React.forwardRef( strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} - d="M.833 7.5h3.959M14.167 7.5h-3.959M7.5 10.209a2.708 2.708 0 1 0 0-5.417 2.708 2.708 0 0 0 0 5.417" + d="M7.5 14.167v-3.958M7.5.834v3.958M10.208 7.5a2.708 2.708 0 1 0-5.416 0 2.708 2.708 0 0 0 5.416 0" /> ) diff --git a/packages/design-system/icons/src/components/code-pull-request.tsx b/packages/design-system/icons/src/components/code-pull-request.tsx new file mode 100644 index 0000000000000..6ac1eb83c1ef7 --- /dev/null +++ b/packages/design-system/icons/src/components/code-pull-request.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import type { IconProps } from "../types" +const CodePullRequest = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +CodePullRequest.displayName = "CodePullRequest" +export default CodePullRequest diff --git a/packages/design-system/icons/src/components/command-line.tsx b/packages/design-system/icons/src/components/command-line.tsx index a29cc4be42f9a..7ab4f3e652ad4 100644 --- a/packages/design-system/icons/src/components/command-line.tsx +++ b/packages/design-system/icons/src/components/command-line.tsx @@ -11,21 +11,20 @@ const CommandLine = React.forwardRef( ref={ref} {...props} > - - - - - - - - - + d="M11.278 1.944H3.722c-.982 0-1.778.796-1.778 1.778v7.556c0 .981.796 1.777 1.778 1.777h7.556c.982 0 1.778-.796 1.778-1.777V3.722c0-.982-.796-1.778-1.778-1.778M8.167 10.389h2.222" + /> + ) } diff --git a/packages/design-system/icons/src/components/draft orders.tsx b/packages/design-system/icons/src/components/draft orders.tsx new file mode 100644 index 0000000000000..9d6a68faadc01 --- /dev/null +++ b/packages/design-system/icons/src/components/draft orders.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import type { IconProps } from "../types" +const DraftOrders = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + ) + } +) +DraftOrders.displayName = "DraftOrders" +export default DraftOrders diff --git a/packages/design-system/icons/src/components/ellipse-blue-solid.tsx b/packages/design-system/icons/src/components/ellipse-blue-solid.tsx index a2efa504bec30..e38c3333c7c79 100644 --- a/packages/design-system/icons/src/components/ellipse-blue-solid.tsx +++ b/packages/design-system/icons/src/components/ellipse-blue-solid.tsx @@ -13,7 +13,7 @@ const EllipseBlueSolid = React.forwardRef( > - + diff --git a/packages/design-system/icons/src/components/ellipse-purple-solid.tsx b/packages/design-system/icons/src/components/ellipse-purple-solid.tsx index be7601945fd56..fadf321d70039 100644 --- a/packages/design-system/icons/src/components/ellipse-purple-solid.tsx +++ b/packages/design-system/icons/src/components/ellipse-purple-solid.tsx @@ -13,7 +13,7 @@ const EllipsePurpleSolid = React.forwardRef( > - + diff --git a/packages/design-system/icons/src/components/exclamation-circle.tsx b/packages/design-system/icons/src/components/exclamation-circle.tsx index fd1fe4258fe29..e4b46b4e1c839 100644 --- a/packages/design-system/icons/src/components/exclamation-circle.tsx +++ b/packages/design-system/icons/src/components/exclamation-circle.tsx @@ -20,7 +20,7 @@ const ExclamationCircle = React.forwardRef( /> diff --git a/packages/design-system/icons/src/components/eye.tsx b/packages/design-system/icons/src/components/eye.tsx index d8ed36de86d4c..7ec4bab59e09b 100644 --- a/packages/design-system/icons/src/components/eye.tsx +++ b/packages/design-system/icons/src/components/eye.tsx @@ -11,9 +11,15 @@ const Eye = React.forwardRef( ref={ref} {...props} > - - - + + + diff --git a/packages/design-system/icons/src/components/face-crossed-out-eyes.tsx b/packages/design-system/icons/src/components/face-crossed-out-eyes.tsx new file mode 100644 index 0000000000000..3adfb0158d1ec --- /dev/null +++ b/packages/design-system/icons/src/components/face-crossed-out-eyes.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import type { IconProps } from "../types" +const FaceCrossedOutEyes = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +FaceCrossedOutEyes.displayName = "FaceCrossedOutEyes" +export default FaceCrossedOutEyes diff --git a/packages/design-system/icons/src/components/face-smile.tsx b/packages/design-system/icons/src/components/face-smile.tsx index 024295349be1f..ec93f84ae2159 100644 --- a/packages/design-system/icons/src/components/face-smile.tsx +++ b/packages/design-system/icons/src/components/face-smile.tsx @@ -24,7 +24,7 @@ const FaceSmile = React.forwardRef( strokeWidth={1.5} d="M10.832 9.278a3.78 3.78 0 0 1-3.332 2 3.78 3.78 0 0 1-3.332-2" /> - + diff --git a/packages/design-system/icons/src/components/featured-badge.tsx b/packages/design-system/icons/src/components/featured-badge.tsx index 9dc9ec2f52c41..f54c2bb8778d0 100644 --- a/packages/design-system/icons/src/components/featured-badge.tsx +++ b/packages/design-system/icons/src/components/featured-badge.tsx @@ -11,15 +11,15 @@ const FeaturedBadge = React.forwardRef( ref={ref} {...props} > - + ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +FireSolid.displayName = "FireSolid" +export default FireSolid diff --git a/packages/design-system/icons/src/components/fire.tsx b/packages/design-system/icons/src/components/fire.tsx new file mode 100644 index 0000000000000..6912e925afccf --- /dev/null +++ b/packages/design-system/icons/src/components/fire.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Fire = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +Fire.displayName = "Fire" +export default Fire diff --git a/packages/design-system/icons/src/components/folder-illustration.tsx b/packages/design-system/icons/src/components/folder-illustration.tsx index c044647112f90..7f045101e3b6f 100644 --- a/packages/design-system/icons/src/components/folder-illustration.tsx +++ b/packages/design-system/icons/src/components/folder-illustration.tsx @@ -30,7 +30,7 @@ const FolderIllustration = React.forwardRef( strokeLinejoin="round" strokeOpacity={0.15} strokeWidth={0.5} - d="M7.034 2.955a.25.25 0 0 0 .196.093h4.387c1.142 0 2.068.926 2.068 2.069v6.066a2.07 2.07 0 0 1-2.068 2.069H3.383a2.07 2.07 0 0 1-2.068-2.069V3.817c0-1.143.926-2.069 2.068-2.069h1.691c.628 0 1.22.285 1.613.774z" + d="M3.383 1.748h1.69c.55 0 1.073.218 1.458.6l.156.174.348.432a.25.25 0 0 0 .195.094h4.387c1.142 0 2.068.926 2.068 2.069v6.066a2.07 2.07 0 0 1-2.068 2.068H3.383a2.07 2.07 0 0 1-2.068-2.068V3.817c0-1.143.926-2.069 2.068-2.069" /> ( strokeLinejoin="round" strokeOpacity={0.15} strokeWidth={0.5} - d="M7.034 2.922a.25.25 0 0 0 .196.094h4.387c1.142 0 2.068.926 2.068 2.068v6.067a2.07 2.07 0 0 1-2.068 2.068H3.383a2.07 2.07 0 0 1-2.068-2.068V3.784c0-1.142.926-2.068 2.068-2.068h1.691c.628 0 1.22.284 1.613.773z" + d="M3.383 1.716h1.69c.55 0 1.073.218 1.458.6l.156.173.348.433a.25.25 0 0 0 .195.094h4.387c1.142 0 2.068.926 2.068 2.068v6.067a2.07 2.07 0 0 1-2.068 2.068H3.383a2.07 2.07 0 0 1-2.068-2.068V3.783c0-1.142.926-2.068 2.068-2.068" /> ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +FunnelPlus.displayName = "FunnelPlus" +export default FunnelPlus diff --git a/packages/design-system/icons/src/components/gauge-sparkle.tsx b/packages/design-system/icons/src/components/gauge-sparkle.tsx new file mode 100644 index 0000000000000..bc8bd64ce4f68 --- /dev/null +++ b/packages/design-system/icons/src/components/gauge-sparkle.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import type { IconProps } from "../types" +const GaugeSparkle = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + ) + } +) +GaugeSparkle.displayName = "GaugeSparkle" +export default GaugeSparkle diff --git a/packages/design-system/icons/src/components/ghost-worried.tsx b/packages/design-system/icons/src/components/ghost-worried.tsx new file mode 100644 index 0000000000000..9d91f040867e1 --- /dev/null +++ b/packages/design-system/icons/src/components/ghost-worried.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import type { IconProps } from "../types" +const GhostWorried = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +GhostWorried.displayName = "GhostWorried" +export default GhostWorried diff --git a/packages/design-system/icons/src/components/gift cards.tsx b/packages/design-system/icons/src/components/gift cards.tsx new file mode 100644 index 0000000000000..2b1972bfaeb33 --- /dev/null +++ b/packages/design-system/icons/src/components/gift cards.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import type { IconProps } from "../types" +const GiftCards = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + + ) + } +) +GiftCards.displayName = "GiftCards" +export default GiftCards diff --git a/packages/design-system/icons/src/components/github.tsx b/packages/design-system/icons/src/components/github.tsx index 1c4e6662352c5..d94f39b253fa2 100644 --- a/packages/design-system/icons/src/components/github.tsx +++ b/packages/design-system/icons/src/components/github.tsx @@ -1,22 +1,38 @@ import * as React from "react" import type { IconProps } from "../types" -const Github = React.forwardRef>( - (props, ref) => { +const Github = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { return ( + + + + + + + + + ) } diff --git a/packages/design-system/icons/src/components/id-badge.tsx b/packages/design-system/icons/src/components/id-badge.tsx new file mode 100644 index 0000000000000..f13c3d00c49a9 --- /dev/null +++ b/packages/design-system/icons/src/components/id-badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import type { IconProps } from "../types" +const IdBadge = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + ) + } +) +IdBadge.displayName = "IdBadge" +export default IdBadge diff --git a/packages/design-system/icons/src/components/index.ts b/packages/design-system/icons/src/components/index.ts index 273c1e227464a..874269e604038 100644 --- a/packages/design-system/icons/src/components/index.ts +++ b/packages/design-system/icons/src/components/index.ts @@ -1,4 +1,5 @@ // This file is generated automatically. +export { default as WIP } from "./WIP" export { default as AcademicCapSolid } from "./academic-cap-solid" export { default as AcademicCap } from "./academic-cap" export { default as AdjustmentsDone } from "./adjustments-done" @@ -40,6 +41,7 @@ export { default as ArrowsPointingOut } from "./arrows-pointing-out" export { default as ArrowsReduceDiagonal } from "./arrows-reduce-diagonal" export { default as AtSymbol } from "./at-symbol" export { default as BackwardSolid } from "./backward-solid" +export { default as BadgeCheck } from "./badge-check" export { default as BarsArrowDown } from "./bars-arrow-down" export { default as BarsThree } from "./bars-three" export { default as Beaker } from "./beaker" @@ -53,6 +55,7 @@ export { default as Book } from "./book" export { default as Bookmarks } from "./bookmarks" export { default as BottomToTop } from "./bottom-to-top" export { default as Brackets } from "./brackets" +export { default as BroomSparkleSolid } from "./broom-sparkle-solid" export { default as BroomSparkle } from "./broom-sparkle" export { default as BugAntSolid } from "./bug-ant-solid" export { default as Bug } from "./bug" @@ -66,6 +69,7 @@ export { default as CalendarMini } from "./calendar-mini" export { default as CalendarSolid } from "./calendar-solid" export { default as Calendar } from "./calendar" export { default as Camera } from "./camera" +export { default as CardSparkle } from "./card-sparkle" export { default as CaretMaximizeDiagonal } from "./caret-maximize-diagonal" export { default as CaretMinimizeDiagonal } from "./caret-minimize-diagonal" export { default as CashSolid } from "./cash-solid" @@ -82,6 +86,7 @@ export { default as CheckCircleSolid } from "./check-circle-solid" export { default as CheckCircle } from "./check-circle" export { default as CheckMini } from "./check-mini" export { default as Check } from "./check" +export { default as ChefHat } from "./chef-hat" export { default as ChevronDoubleLeftMiniSolid } from "./chevron-double-left-mini-solid" export { default as ChevronDoubleLeft } from "./chevron-double-left" export { default as ChevronDoubleRightMiniSolid } from "./chevron-double-right-mini-solid" @@ -97,10 +102,12 @@ export { default as ChevronUpMini } from "./chevron-up-mini" export { default as CircleArrowUp } from "./circle-arrow-up" export { default as CircleDottedLine } from "./circle-dotted-line" export { default as CircleFilledSolid } from "./circle-filled-solid" +export { default as CircleHalfDottedClock } from "./circle-half-dotted-clock" export { default as CircleHalfSolid } from "./circle-half-solid" export { default as CircleMiniFilledSolid } from "./circle-mini-filled-solid" export { default as CircleMiniSolid } from "./circle-mini-solid" export { default as CircleMinus } from "./circle-minus" +export { default as CirclePlus } from "./circle-plus" export { default as CircleQuarterSolid } from "./circle-quarter-solid" export { default as CircleSliders } from "./circle-sliders" export { default as CircleSolid } from "./circle-solid" @@ -114,8 +121,10 @@ export { default as Clock } from "./clock" export { default as CloneDashed } from "./clone-dashed" export { default as CloudArrowDown } from "./cloud-arrow-down" export { default as CloudArrowUp } from "./cloud-arrow-up" +export { default as CloudSolid } from "./cloud-solid" export { default as CodeCommit } from "./code-commit" export { default as CodeMerge } from "./code-merge" +export { default as CodePullRequest } from "./code-pull-request" export { default as CogSixToothSolid } from "./cog-six-tooth-solid" export { default as CogSixTooth } from "./cog-six-tooth" export { default as CommandLineSolid } from "./command-line-solid" @@ -138,6 +147,7 @@ export { default as DocumentSeries } from "./document-series" export { default as DocumentTextSolid } from "./document-text-solid" export { default as DocumentText } from "./document-text" export { default as DotsSix } from "./dots-six" +export { default as DraftOrders } from "./draft orders" export { default as DropCap } from "./drop-cap" export { default as EllipseBlueSolid } from "./ellipse-blue-solid" export { default as EllipseGreenSolid } from "./ellipse-green-solid" @@ -158,10 +168,13 @@ export { default as EyeMini } from "./eye-mini" export { default as EyeSlashMini } from "./eye-slash-mini" export { default as EyeSlash } from "./eye-slash" export { default as Eye } from "./eye" +export { default as FaceCrossedOutEyes } from "./face-crossed-out-eyes" export { default as FaceSmile } from "./face-smile" export { default as Facebook } from "./facebook" export { default as FeaturedBadge } from "./featured-badge" export { default as Figma } from "./figma" +export { default as FireSolid } from "./fire-solid" +export { default as Fire } from "./fire" export { default as FlagMini } from "./flag-mini" export { default as FlyingBox } from "./flying-box" export { default as FolderIllustration } from "./folder-illustration" @@ -169,9 +182,13 @@ export { default as FolderOpenIllustration } from "./folder-open-illustration" export { default as FolderOpen } from "./folder-open" export { default as Folder } from "./folder" export { default as ForwardSolid } from "./forward-solid" +export { default as FunnelPlus } from "./funnel-plus" export { default as Funnel } from "./funnel" export { default as GatsbyEx } from "./gatsby-ex" export { default as Gatsby } from "./gatsby" +export { default as GaugeSparkle } from "./gauge-sparkle" +export { default as GhostWorried } from "./ghost-worried" +export { default as GiftCards } from "./gift cards" export { default as GiftSolid } from "./gift-solid" export { default as Gift } from "./gift" export { default as Github } from "./github" @@ -187,6 +204,7 @@ export { default as Heart } from "./heart" export { default as History } from "./history" export { default as HouseStar } from "./house-star" export { default as House } from "./house" +export { default as IdBadge } from "./id-badge" export { default as InboxSolid } from "./inbox-solid" export { default as InformationCircleSolid } from "./information-circle-solid" export { default as InformationCircle } from "./information-circle" @@ -202,6 +220,7 @@ export { default as Levels } from "./levels" export { default as Lifebuoy } from "./lifebuoy" export { default as LightBulbSolid } from "./light-bulb-solid" export { default as LightBulb } from "./light-bulb" +export { default as Linear } from "./linear" export { default as Link } from "./link" export { default as Linkedin } from "./linkedin" export { default as ListBullet } from "./list-bullet" @@ -216,10 +235,16 @@ export { default as MagnifyingGlassMini } from "./magnifying-glass-mini" export { default as MagnifyingGlass } from "./magnifying-glass" export { default as MapPin } from "./map-pin" export { default as Map } from "./map" +export { default as MarkdownSolid } from "./markdown-solid" export { default as Mastercard } from "./mastercard" export { default as MediaPlay } from "./media-play" export { default as Medusa } from "./medusa" +export { default as Meilisearch } from "./meilisearch" +export { default as MemberBronzeBadge } from "./member-bronze-badge" +export { default as MemberGoldBadge } from "./member-gold-badge" +export { default as MemberSilverBadge } from "./member-silver-badge" export { default as Meta } from "./meta" +export { default as MinusBadge } from "./minus-badge" export { default as MinusMini } from "./minus-mini" export { default as Minus } from "./minus" export { default as MoonSolid } from "./moon-solid" @@ -230,6 +255,9 @@ export { default as OpenRectArrowOut } from "./open-rect-arrow-out" export { default as PaperClip } from "./paper-clip" export { default as Party } from "./party" export { default as PauseSolid } from "./pause-solid" +export { default as Pause } from "./pause" +export { default as Paypal } from "./paypal" +export { default as Payphone } from "./payphone" export { default as PenPlus } from "./pen-plus" export { default as PencilSquareSolid } from "./pencil-square-solid" export { default as PencilSquare } from "./pencil-square" @@ -241,6 +269,14 @@ export { default as PlayMiniSolid } from "./play-mini-solid" export { default as PlaySolid } from "./play-solid" export { default as PlusMini } from "./plus-mini" export { default as Plus } from "./plus" +export { default as Progress0 } from "./progress-0" +export { default as Progress100 } from "./progress-100" +export { default as Progress15 } from "./progress-15" +export { default as Progress30 } from "./progress-30" +export { default as Progress45 } from "./progress-45" +export { default as Progress60 } from "./progress-60" +export { default as Progress75 } from "./progress-75" +export { default as Progress90 } from "./progress-90" export { default as PuzzleSolid } from "./puzzle-solid" export { default as Puzzle } from "./puzzle" export { default as QuestionMarkCircle } from "./question-mark-circle" @@ -252,6 +288,7 @@ export { default as ReceiptPercent } from "./receipt-percent" export { default as Receipt } from "./receipt" export { default as Reduce } from "./reduce" export { default as ReplaySolid } from "./replay-solid" +export { default as Resend } from "./resend" export { default as Resize } from "./resize" export { default as RocketLaunchSolid } from "./rocket-launch-solid" export { default as RocketLaunch } from "./rocket-launch" @@ -266,14 +303,18 @@ export { default as Server } from "./server" export { default as Share } from "./share" export { default as ShieldCheck } from "./shield-check" export { default as Shipbob } from "./shipbob" +export { default as Shippo } from "./shippo" export { default as ShoppingBag } from "./shopping-bag" export { default as ShoppingCartSolid } from "./shopping-cart-solid" export { default as ShoppingCart } from "./shopping-cart" export { default as Shopping } from "./shopping" +export { default as SidebarLeftFilled } from "./sidebar-left-filled" export { default as SidebarLeft } from "./sidebar-left" +export { default as SidebarRightFilled } from "./sidebar-right-filled" export { default as SidebarRight } from "./sidebar-right" export { default as Slack } from "./slack" export { default as Snooze } from "./snooze" +export { default as Sparkle2Solid } from "./sparkle-2-solid" export { default as SparklesMiniSolid } from "./sparkles-mini-solid" export { default as SparklesMini } from "./sparkles-mini" export { default as SparklesSolid } from "./sparkles-solid" @@ -294,6 +335,7 @@ export { default as StackPerspective } from "./stack-perspective" export { default as StarSolid } from "./star-solid" export { default as Star } from "./star" export { default as Stopwatch } from "./stopwatch" +export { default as StoreCredits } from "./store credits" export { default as Stripe } from "./stripe" export { default as SunSolid } from "./sun-solid" export { default as Sun } from "./sun" @@ -306,6 +348,7 @@ export { default as Tailwind } from "./tailwind" export { default as Target } from "./target" export { default as TaxExclusive } from "./tax-exclusive" export { default as TaxInclusive } from "./tax-inclusive" +export { default as Telegram } from "./telegram" export { default as TextHighlight } from "./text-highlight" export { default as Text } from "./text" export { default as ThumbDown } from "./thumb-down" @@ -337,6 +380,7 @@ export { default as Visa } from "./visa" export { default as WandSparkle } from "./wand-sparkle" export { default as Webshipper } from "./webshipper" export { default as Window } from "./window" +export { default as WishLists } from "./wish lists" export { default as XCircleSolid } from "./x-circle-solid" export { default as XCircle } from "./x-circle" export { default as XMarkMini } from "./x-mark-mini" diff --git a/packages/design-system/icons/src/components/key.tsx b/packages/design-system/icons/src/components/key.tsx index a470834dfb8c7..d5d86917e6fa1 100644 --- a/packages/design-system/icons/src/components/key.tsx +++ b/packages/design-system/icons/src/components/key.tsx @@ -18,7 +18,7 @@ const Key = React.forwardRef( strokeWidth={1.5} d="m1.345 13.497.158-2.53L6.739 5.73a3.6 3.6 0 0 1-.128-.897 3.556 3.556 0 1 1 3.556 3.556c-.322 0-.629-.057-.926-.137L7.5 9.944h-2v2l-1.462 1.559z" /> - + diff --git a/packages/design-system/icons/src/components/linear.tsx b/packages/design-system/icons/src/components/linear.tsx new file mode 100644 index 0000000000000..b1caabe1090a0 --- /dev/null +++ b/packages/design-system/icons/src/components/linear.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Linear = React.forwardRef>( + (props, ref) => { + return ( + + + + + + + + + + + ) + } +) +Linear.displayName = "Linear" +export default Linear diff --git a/packages/design-system/icons/src/components/loader.tsx b/packages/design-system/icons/src/components/loader.tsx index 89152c8dfb77b..66ff8c0152f02 100644 --- a/packages/design-system/icons/src/components/loader.tsx +++ b/packages/design-system/icons/src/components/loader.tsx @@ -18,14 +18,14 @@ const Loader = React.forwardRef( strokeWidth={1.5} clipPath="url(#a)" > - - - - - - - - + + + + + + + + diff --git a/packages/design-system/icons/src/components/markdown-solid.tsx b/packages/design-system/icons/src/components/markdown-solid.tsx new file mode 100644 index 0000000000000..d414c8273fc39 --- /dev/null +++ b/packages/design-system/icons/src/components/markdown-solid.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import type { IconProps } from "../types" +const MarkdownSolid = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +MarkdownSolid.displayName = "MarkdownSolid" +export default MarkdownSolid diff --git a/packages/design-system/icons/src/components/mastercard.tsx b/packages/design-system/icons/src/components/mastercard.tsx index a8362d437582e..38b3b123d52ff 100644 --- a/packages/design-system/icons/src/components/mastercard.tsx +++ b/packages/design-system/icons/src/components/mastercard.tsx @@ -1,25 +1,40 @@ import * as React from "react" import type { IconProps } from "../types" -const Mastercard = React.forwardRef>( - (props, ref) => { +const Mastercard = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { return ( - + + + + + + + + + ) } diff --git a/packages/design-system/icons/src/components/medusa.tsx b/packages/design-system/icons/src/components/medusa.tsx index 64d4957892838..c3600f80a4b49 100644 --- a/packages/design-system/icons/src/components/medusa.tsx +++ b/packages/design-system/icons/src/components/medusa.tsx @@ -1,20 +1,35 @@ import * as React from "react" import type { IconProps } from "../types" -const Medusa = React.forwardRef>( - (props, ref) => { +const Medusa = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { return ( + + + + + + + + ) } diff --git a/packages/design-system/icons/src/components/meilisearch.tsx b/packages/design-system/icons/src/components/meilisearch.tsx new file mode 100644 index 0000000000000..5c1a9de45dfd3 --- /dev/null +++ b/packages/design-system/icons/src/components/meilisearch.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Meilisearch = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Meilisearch.displayName = "Meilisearch" +export default Meilisearch diff --git a/packages/design-system/icons/src/components/member-bronze-badge.tsx b/packages/design-system/icons/src/components/member-bronze-badge.tsx new file mode 100644 index 0000000000000..9a8983ebf32b7 --- /dev/null +++ b/packages/design-system/icons/src/components/member-bronze-badge.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import type { IconProps } from "../types" +const MemberBronzeBadge = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + + ) + } +) +MemberBronzeBadge.displayName = "MemberBronzeBadge" +export default MemberBronzeBadge diff --git a/packages/design-system/icons/src/components/member-gold-badge.tsx b/packages/design-system/icons/src/components/member-gold-badge.tsx new file mode 100644 index 0000000000000..bd1aa7d03936e --- /dev/null +++ b/packages/design-system/icons/src/components/member-gold-badge.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import type { IconProps } from "../types" +const MemberGoldBadge = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + + ) + } +) +MemberGoldBadge.displayName = "MemberGoldBadge" +export default MemberGoldBadge diff --git a/packages/design-system/icons/src/components/member-silver-badge.tsx b/packages/design-system/icons/src/components/member-silver-badge.tsx new file mode 100644 index 0000000000000..7f43de9ecdd8b --- /dev/null +++ b/packages/design-system/icons/src/components/member-silver-badge.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import type { IconProps } from "../types" +const MemberSilverBadge = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + + ) + } +) +MemberSilverBadge.displayName = "MemberSilverBadge" +export default MemberSilverBadge diff --git a/packages/design-system/icons/src/components/minus-badge.tsx b/packages/design-system/icons/src/components/minus-badge.tsx new file mode 100644 index 0000000000000..9cc9b2dc8b4ab --- /dev/null +++ b/packages/design-system/icons/src/components/minus-badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import type { IconProps } from "../types" +const MinusBadge = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + ) + } +) +MinusBadge.displayName = "MinusBadge" +export default MinusBadge diff --git a/packages/design-system/icons/src/components/pause.tsx b/packages/design-system/icons/src/components/pause.tsx new file mode 100644 index 0000000000000..a26e0f3647f77 --- /dev/null +++ b/packages/design-system/icons/src/components/pause.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Pause = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + ) + } +) +Pause.displayName = "Pause" +export default Pause diff --git a/packages/design-system/icons/src/components/paypal.tsx b/packages/design-system/icons/src/components/paypal.tsx new file mode 100644 index 0000000000000..502211aeaa375 --- /dev/null +++ b/packages/design-system/icons/src/components/paypal.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Paypal = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + + ) + } +) +Paypal.displayName = "Paypal" +export default Paypal diff --git a/packages/design-system/icons/src/components/payphone.tsx b/packages/design-system/icons/src/components/payphone.tsx new file mode 100644 index 0000000000000..8a7362528d4b4 --- /dev/null +++ b/packages/design-system/icons/src/components/payphone.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Payphone = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Payphone.displayName = "Payphone" +export default Payphone diff --git a/packages/design-system/icons/src/components/progress-0.tsx b/packages/design-system/icons/src/components/progress-0.tsx new file mode 100644 index 0000000000000..44ba8f7375e37 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-0.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress0 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + ) + } +) +Progress0.displayName = "Progress0" +export default Progress0 diff --git a/packages/design-system/icons/src/components/progress-100.tsx b/packages/design-system/icons/src/components/progress-100.tsx new file mode 100644 index 0000000000000..7d779153fbea4 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-100.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress100 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress100.displayName = "Progress100" +export default Progress100 diff --git a/packages/design-system/icons/src/components/progress-15.tsx b/packages/design-system/icons/src/components/progress-15.tsx new file mode 100644 index 0000000000000..ad76b003d0230 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-15.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress15 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress15.displayName = "Progress15" +export default Progress15 diff --git a/packages/design-system/icons/src/components/progress-30.tsx b/packages/design-system/icons/src/components/progress-30.tsx new file mode 100644 index 0000000000000..cf919988b5801 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-30.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress30 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress30.displayName = "Progress30" +export default Progress30 diff --git a/packages/design-system/icons/src/components/progress-45.tsx b/packages/design-system/icons/src/components/progress-45.tsx new file mode 100644 index 0000000000000..a9243ad80bba8 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-45.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress45 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress45.displayName = "Progress45" +export default Progress45 diff --git a/packages/design-system/icons/src/components/progress-60.tsx b/packages/design-system/icons/src/components/progress-60.tsx new file mode 100644 index 0000000000000..ecf5ac866dd99 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-60.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress60 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress60.displayName = "Progress60" +export default Progress60 diff --git a/packages/design-system/icons/src/components/progress-75.tsx b/packages/design-system/icons/src/components/progress-75.tsx new file mode 100644 index 0000000000000..8603ed475c42b --- /dev/null +++ b/packages/design-system/icons/src/components/progress-75.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress75 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress75.displayName = "Progress75" +export default Progress75 diff --git a/packages/design-system/icons/src/components/progress-90.tsx b/packages/design-system/icons/src/components/progress-90.tsx new file mode 100644 index 0000000000000..061653a0c7549 --- /dev/null +++ b/packages/design-system/icons/src/components/progress-90.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Progress90 = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Progress90.displayName = "Progress90" +export default Progress90 diff --git a/packages/design-system/icons/src/components/resend.tsx b/packages/design-system/icons/src/components/resend.tsx new file mode 100644 index 0000000000000..43f483f5c21af --- /dev/null +++ b/packages/design-system/icons/src/components/resend.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Resend = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Resend.displayName = "Resend" +export default Resend diff --git a/packages/design-system/icons/src/components/shippo.tsx b/packages/design-system/icons/src/components/shippo.tsx new file mode 100644 index 0000000000000..4dacf720ea665 --- /dev/null +++ b/packages/design-system/icons/src/components/shippo.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Shippo = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Shippo.displayName = "Shippo" +export default Shippo diff --git a/packages/design-system/icons/src/components/sidebar-left-filled.tsx b/packages/design-system/icons/src/components/sidebar-left-filled.tsx new file mode 100644 index 0000000000000..f6b5a5cef6003 --- /dev/null +++ b/packages/design-system/icons/src/components/sidebar-left-filled.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import type { IconProps } from "../types" +const SidebarLeftFilled = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +SidebarLeftFilled.displayName = "SidebarLeftFilled" +export default SidebarLeftFilled diff --git a/packages/design-system/icons/src/components/sidebar-right-filled.tsx b/packages/design-system/icons/src/components/sidebar-right-filled.tsx new file mode 100644 index 0000000000000..7d8cad8f5e958 --- /dev/null +++ b/packages/design-system/icons/src/components/sidebar-right-filled.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import type { IconProps } from "../types" +const SidebarRightFilled = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +SidebarRightFilled.displayName = "SidebarRightFilled" +export default SidebarRightFilled diff --git a/packages/design-system/icons/src/components/sparkle-2-solid.tsx b/packages/design-system/icons/src/components/sparkle-2-solid.tsx new file mode 100644 index 0000000000000..283c58b9d2b43 --- /dev/null +++ b/packages/design-system/icons/src/components/sparkle-2-solid.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import type { IconProps } from "../types" +const Sparkle2Solid = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + ) + } +) +Sparkle2Solid.displayName = "Sparkle2Solid" +export default Sparkle2Solid diff --git a/packages/design-system/icons/src/components/sparkles-mini.tsx b/packages/design-system/icons/src/components/sparkles-mini.tsx index 580e5cd12ab0c..7c25526734a92 100644 --- a/packages/design-system/icons/src/components/sparkles-mini.tsx +++ b/packages/design-system/icons/src/components/sparkles-mini.tsx @@ -14,7 +14,7 @@ const SparklesMini = React.forwardRef( ( ( strokeWidth={1.5} clipPath="url(#a)" > - - - - - - - - + + + + + + + + diff --git a/packages/design-system/icons/src/components/square-blue-solid.tsx b/packages/design-system/icons/src/components/square-blue-solid.tsx index d03e862473015..cf17600681df1 100644 --- a/packages/design-system/icons/src/components/square-blue-solid.tsx +++ b/packages/design-system/icons/src/components/square-blue-solid.tsx @@ -11,7 +11,7 @@ const SquareBlueSolid = React.forwardRef( ref={ref} {...props} > - + ( ref={ref} {...props} > - + ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +StoreCredits.displayName = "StoreCredits" +export default StoreCredits diff --git a/packages/design-system/icons/src/components/stripe.tsx b/packages/design-system/icons/src/components/stripe.tsx index 86bc331ca7634..f571c240521ac 100644 --- a/packages/design-system/icons/src/components/stripe.tsx +++ b/packages/design-system/icons/src/components/stripe.tsx @@ -1,22 +1,37 @@ import * as React from "react" import type { IconProps } from "../types" -const Stripe = React.forwardRef>( - (props, ref) => { +const Stripe = React.forwardRef( + ({ color = "currentColor", ...props }, ref) => { return ( + + + + + + + + ) } diff --git a/packages/design-system/icons/src/components/tag-illustration.tsx b/packages/design-system/icons/src/components/tag-illustration.tsx index 6ef19bee5bf20..5fede9f5efc97 100644 --- a/packages/design-system/icons/src/components/tag-illustration.tsx +++ b/packages/design-system/icons/src/components/tag-illustration.tsx @@ -26,7 +26,7 @@ const TagIllustration = React.forwardRef( stroke="#000" strokeOpacity={0.15} strokeWidth={0.5} - d="M6.863 12.687 2.313 8.137a1.9 1.9 0 0 1-.564-1.361V2.884c0-.625.509-1.134 1.134-1.134h3.893c.515 0 .997.2 1.361.564l4.549 4.548c.363.364.563.847.563 1.361 0 .515-.2.998-.563 1.361l-3.103 3.103a1.9 1.9 0 0 1-1.36.563c-.515 0-.998-.2-1.361-.563Z" + d="M2.884 1.75h3.892c.515 0 .998.2 1.363.564l4.548 4.548c.363.364.563.847.563 1.362 0 .45-.153.876-.435 1.218l-.128.142-3.103 3.103a1.9 1.9 0 0 1-1.36.563c-.45 0-.877-.152-1.22-.435l-.142-.128-4.548-4.548a1.91 1.91 0 0 1-.564-1.363V2.884c0-.625.509-1.134 1.134-1.134Z" /> ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + ) + } +) +Telegram.displayName = "Telegram" +export default Telegram diff --git a/packages/design-system/icons/src/components/thumbnail-badge.tsx b/packages/design-system/icons/src/components/thumbnail-badge.tsx index 8b407b878d7b2..669e8b8deddb6 100644 --- a/packages/design-system/icons/src/components/thumbnail-badge.tsx +++ b/packages/design-system/icons/src/components/thumbnail-badge.tsx @@ -11,15 +11,15 @@ const ThumbnailBadge = React.forwardRef( ref={ref} {...props} > - + diff --git a/packages/design-system/icons/src/components/verified-badge.tsx b/packages/design-system/icons/src/components/verified-badge.tsx index f860f6fdaa420..cc21cad2b30fd 100644 --- a/packages/design-system/icons/src/components/verified-badge.tsx +++ b/packages/design-system/icons/src/components/verified-badge.tsx @@ -12,7 +12,7 @@ const VerifiedBadge = React.forwardRef( {...props} > ( ( + ({ color = "currentColor", ...props }, ref) => { + return ( + + + + + + + + + + + + + ) + } +) +WishLists.displayName = "WishLists" +export default WishLists diff --git a/packages/design-system/toolbox/CHANGELOG.md b/packages/design-system/toolbox/CHANGELOG.md index efe35c4e6909d..e9e46c11148b9 100644 --- a/packages/design-system/toolbox/CHANGELOG.md +++ b/packages/design-system/toolbox/CHANGELOG.md @@ -1,5 +1,7 @@ # @medusajs/toolbox +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/design-system/toolbox/package.json b/packages/design-system/toolbox/package.json index 72eb663d9d75a..d7951c226a8a1 100644 --- a/packages/design-system/toolbox/package.json +++ b/packages/design-system/toolbox/package.json @@ -1,7 +1,7 @@ { "name": "@medusajs/toolbox", "private": true, - "version": "2.8.7", + "version": "2.8.8", "description": "CLI tool for importing Figma designs for Medusa UI", "license": "MIT", "author": "Kasper Kristensen ", diff --git a/packages/design-system/ui-preset/CHANGELOG.md b/packages/design-system/ui-preset/CHANGELOG.md index 2f948d97b42ca..febfddefaf829 100644 --- a/packages/design-system/ui-preset/CHANGELOG.md +++ b/packages/design-system/ui-preset/CHANGELOG.md @@ -1,5 +1,7 @@ # @medusajs/ui-preset +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/design-system/ui-preset/package.json b/packages/design-system/ui-preset/package.json index 6bb40eafb4df2..b1c63358a756f 100644 --- a/packages/design-system/ui-preset/package.json +++ b/packages/design-system/ui-preset/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/ui-preset", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa UI preset", "license": "MIT", "repository": { @@ -30,7 +30,7 @@ "tailwindcss": ">=3.0.0" }, "devDependencies": { - "@medusajs/toolbox": "2.8.7", + "@medusajs/toolbox": "2.8.8", "tailwindcss": "^3.4.1", "tsup": "^7.1.0", "typescript": "^5.1.6" diff --git a/packages/design-system/ui-preset/src/theme/extension/theme.ts b/packages/design-system/ui-preset/src/theme/extension/theme.ts index f0c6f2a3a06ef..e037d1aea1bff 100644 --- a/packages/design-system/ui-preset/src/theme/extension/theme.ts +++ b/packages/design-system/ui-preset/src/theme/extension/theme.ts @@ -313,13 +313,10 @@ export const theme = { } }, "boxShadow": { - "borders-interactive-with-active": "var(--borders-interactive-with-active)", "buttons-danger-focus": "var(--buttons-danger-focus)", "details-contrast-on-bg-interactive": "var(--details-contrast-on-bg-interactive)", - "borders-interactive-with-focus": "var(--borders-interactive-with-focus)", "borders-error": "var(--borders-error)", "borders-focus": "var(--borders-focus)", - "borders-interactive-with-shadow": "var(--borders-interactive-with-shadow)", "buttons-danger": "var(--buttons-danger)", "buttons-inverted-focus": "var(--buttons-inverted-focus)", "elevation-card-hover": "var(--elevation-card-hover)", @@ -335,7 +332,10 @@ export const theme = { "elevation-modal": "var(--elevation-modal)", "elevation-code-block": "var(--elevation-code-block)", "buttons-inverted": "var(--buttons-inverted)", - "elevation-commandbar": "var(--elevation-commandbar)" + "elevation-commandbar": "var(--elevation-commandbar)", + "borders-interactive-with-focus": "var(--borders-interactive-with-focus)", + "borders-interactive-with-shadow": "var(--borders-interactive-with-shadow)", + "borders-interactive-with-active": "var(--borders-interactive-with-active)" } } } \ No newline at end of file diff --git a/packages/design-system/ui-preset/src/theme/tokens/colors.ts b/packages/design-system/ui-preset/src/theme/tokens/colors.ts index 210b2769b049e..fd146c0cdeb32 100644 --- a/packages/design-system/ui-preset/src/theme/tokens/colors.ts +++ b/packages/design-system/ui-preset/src/theme/tokens/colors.ts @@ -180,7 +180,7 @@ export const colors = { "--bg-overlay": "rgba(24, 24, 27, 0.4)", "--fg-disabled": "rgba(161, 161, 170, 1)", "--fg-muted": "rgba(113, 113, 122, 1)", - "--alpha-400": "rgba(24, 24, 27, 0.24)", - "--alpha-250": "rgba(24, 24, 27, 0.1)" + "--alpha-250": "rgba(24, 24, 27, 0.1)", + "--alpha-400": "rgba(24, 24, 27, 0.24)" } } \ No newline at end of file diff --git a/packages/design-system/ui-preset/src/theme/tokens/effects.ts b/packages/design-system/ui-preset/src/theme/tokens/effects.ts index 2ce5f5f457faf..157a40a6da186 100644 --- a/packages/design-system/ui-preset/src/theme/tokens/effects.ts +++ b/packages/design-system/ui-preset/src/theme/tokens/effects.ts @@ -1,11 +1,9 @@ export const effects = { "dark": { - "--borders-interactive-with-shadow": "0px 1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1)", "--details-contrast-on-bg-interactive": "0px 1px 2px 0px rgba(30, 58, 138, 0.6)", "--details-switch-handle": "0px 0px 2px 1px rgba(255, 255, 255, 1) inset, 0px 1px 0px 0px rgba(255, 255, 255, 1) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.16), 0px 5px 4px 0px rgba(0, 0, 0, 0.1), 0px 3px 3px 0px rgba(0, 0, 0, 0.1), 0px 1px 2px 0px rgba(0, 0, 0, 0.1), 0px 0px 1px 0px rgba(0, 0, 0, 0.1)", "--borders-interactive-with-active": "0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.25)", "--borders-focus": "0px 0px 0px 1px rgba(24, 24, 27, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8)", - "--borders-interactive-with-focus": "0px 1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)", "--details-switch-background-focus": "0px 0px 0px 1px rgba(24, 24, 27, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8), 0px 1px 1px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.1) inset, 0px 0px 0px 0.75px rgba(255, 255, 255, 0.12) inset, 0px 0px 8px 0px rgba(0, 0, 0, 0.1) inset", "--buttons-danger": "0px -1px 0px 0px rgba(255, 255, 255, 0.16), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 0px 0px 1px rgba(159, 18, 57, 1), 0px 0px 1px 1.5px rgba(0, 0, 0, 0.24), 0px 2px 2px 0px rgba(0, 0, 0, 0.24)", "--buttons-danger-focus": "0px -1px 0px 0px rgba(255, 255, 255, 0.16), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 0px 0px 1px rgba(159, 18, 57, 1), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)", @@ -22,16 +20,15 @@ export const effects = { "--buttons-neutral-focus": "0px -1px 0px 0px rgba(255, 255, 255, 0.06), 0px 0px 0px 1px rgba(255, 255, 255, 0.06), 0px 0px 0px 1px rgba(39, 39, 42, 1), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)", "--elevation-modal": "0px 0px 0px 1px rgba(24, 24, 27, 1) inset, 0px 0px 0px 1.5px rgba(255, 255, 255, 0.06) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.32), 0px 8px 16px 0px rgba(0, 0, 0, 0.32)", "--elevation-commandbar": "0px 0px 0px 0.75px rgba(24, 24, 27, 1) inset, 0px 0px 0px 1.25px rgba(255, 255, 255, 0.1) inset, 0px 4px 8px 0px rgba(0, 0, 0, 0.32), 0px 8px 16px 0px rgba(0, 0, 0, 0.32)", - "--elevation-tooltip": "0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 2px 4px 0px rgba(0, 0, 0, 0.32), 0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.32)" + "--elevation-tooltip": "0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 2px 4px 0px rgba(0, 0, 0, 0.32), 0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.32)", + "--borders-interactive-with-focus": "0px -1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)", + "--borders-interactive-with-shadow": "0px -1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1)" }, "light": { - "--borders-interactive-with-active": "0px 0px 0px 1px rgba(59, 130, 246, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.2)", "--buttons-danger-focus": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(190, 18, 60, 0.4), 0px 0px 0px 1px rgba(190, 18, 60, 1), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)", "--details-contrast-on-bg-interactive": "0px 1px 2px 0px rgba(30, 58, 138, 0.6)", - "--borders-interactive-with-focus": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(59, 130, 246, 1), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)", "--borders-error": "0px 0px 0px 1px rgba(225, 29, 72, 1), 0px 0px 0px 3px rgba(225, 29, 72, 0.15)", "--borders-focus": "0px 0px 0px 1px rgba(255, 255, 255, 1), 0px 0px 0px 3px rgba(59, 130, 246, 0.6)", - "--borders-interactive-with-shadow": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(59, 130, 246, 1)", "--buttons-danger": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(190, 18, 60, 0.4), 0px 0px 0px 1px rgba(190, 18, 60, 1)", "--buttons-inverted-focus": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 0px 1px rgba(24, 24, 27, 1), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)", "--elevation-card-hover": "0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 1px 2px -1px rgba(0, 0, 0, 0.08), 0px 2px 8px 0px rgba(0, 0, 0, 0.1)", @@ -47,6 +44,9 @@ export const effects = { "--elevation-modal": "0px 0px 0px 1px rgba(255, 255, 255, 1) inset, 0px 0px 0px 1.5px rgba(228, 228, 231, 0.6) inset, 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 8px 16px 0px rgba(0, 0, 0, 0.08), 0px 16px 32px 0px rgba(0, 0, 0, 0.08)", "--elevation-code-block": "0px 0px 0px 1px rgba(24, 24, 27, 1) inset, 0px 0px 0px 1.5px rgba(255, 255, 255, 0.2) inset", "--buttons-inverted": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 0px 1px rgba(24, 24, 27, 1)", - "--elevation-commandbar": "0px 0px 0px 0.75px rgba(39, 39, 42, 1) inset, 0px 0px 0px 1.25px rgba(255, 255, 255, 0.3) inset, 0px 8px 16px 0px rgba(0, 0, 0, 0.08), 0px 16px 32px 0px rgba(0, 0, 0, 0.08)" + "--elevation-commandbar": "0px 0px 0px 0.75px rgba(39, 39, 42, 1) inset, 0px 0px 0px 1.25px rgba(255, 255, 255, 0.3) inset, 0px 8px 16px 0px rgba(0, 0, 0, 0.08), 0px 16px 32px 0px rgba(0, 0, 0, 0.08)", + "--borders-interactive-with-focus": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(37, 99, 235, 1), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(37, 99, 235, 0.6)", + "--borders-interactive-with-shadow": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(37, 99, 235, 1)", + "--borders-interactive-with-active": "0px 0px 0px 4px rgba(37, 99, 235, 0.2), 0px 0px 0px 1px rgba(37, 99, 235, 1)" } } \ No newline at end of file diff --git a/packages/design-system/ui-preset/src/theme/tokens/typography.ts b/packages/design-system/ui-preset/src/theme/tokens/typography.ts index 4f1b91875a142..95f8937a4f521 100644 --- a/packages/design-system/ui-preset/src/theme/tokens/typography.ts +++ b/packages/design-system/ui-preset/src/theme/tokens/typography.ts @@ -35,12 +35,6 @@ export const typography = { "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-xlarge": { - "fontSize": "1.125rem", - "lineHeight": "1.6875rem", - "fontWeight": "400", - "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" - }, ".txt-compact-small-plus": { "fontSize": "0.8125rem", "lineHeight": "1.25rem", @@ -59,12 +53,6 @@ export const typography = { "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-medium": { - "fontSize": "0.875rem", - "lineHeight": "1.3125rem", - "fontWeight": "400", - "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" - }, ".txt-compact-large": { "fontSize": "1rem", "lineHeight": "1.25rem", @@ -89,106 +77,136 @@ export const typography = { "fontWeight": "400", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-xsmall-plus": { - "fontSize": "0.75rem", - "lineHeight": "1.125rem", + ".h4-webs": { + "fontSize": "1.5rem", + "lineHeight": "1.875rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-small": { - "fontSize": "0.8125rem", - "lineHeight": "1.21875rem", - "fontWeight": "400", + ".h2-core": { + "fontSize": "1rem", + "lineHeight": "1.5rem", + "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-small-plus": { - "fontSize": "0.8125rem", - "lineHeight": "1.21875rem", + ".h3-core": { + "fontSize": "0.875rem", + "lineHeight": "1.25rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-large": { - "fontSize": "1rem", - "lineHeight": "1.5rem", - "fontWeight": "400", + ".h1-core": { + "fontSize": "1.125rem", + "lineHeight": "1.75rem", + "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-medium-plus": { + ".h1-docs": { + "fontSize": "1.5rem", + "lineHeight": "1.875rem", + "fontWeight": "500", + "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" + }, + ".h4-docs": { "fontSize": "0.875rem", - "lineHeight": "1.3125rem", + "lineHeight": "1.399999976158142rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-xsmall": { + ".code-label-plus": { + "fontSize": "0.75rem", + "lineHeight": "0.9375rem", + "fontWeight": "500", + "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" + }, + ".code-label": { + "fontSize": "0.75rem", + "lineHeight": "0.9375rem", + "fontWeight": "400", + "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" + }, + ".code-paragraph": { + "fontSize": "0.75rem", + "lineHeight": "1.2000000476837158rem", + "fontWeight": "400", + "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" + }, + ".code-paragraph-plus": { + "fontSize": "0.75rem", + "lineHeight": "1.2000000476837158rem", + "fontWeight": "500", + "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" + }, + ".txt-xsmall-plus": { "fontSize": "0.75rem", - "lineHeight": "1.125rem", + "lineHeight": "1.2000000476837158rem", + "fontWeight": "500", + "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" + }, + ".txt-small": { + "fontSize": "0.8125rem", + "lineHeight": "1.3000000715255737rem", "fontWeight": "400", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-xlarge-plus": { + ".h2-docs": { "fontSize": "1.125rem", - "lineHeight": "1.6875rem", + "lineHeight": "1.8000000715255737rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".txt-large-plus": { - "fontSize": "1rem", - "lineHeight": "1.5rem", - "fontWeight": "500", + ".txt-medium": { + "fontSize": "0.875rem", + "lineHeight": "1.399999976158142rem", + "fontWeight": "400", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h4-webs": { - "fontSize": "1.5rem", - "lineHeight": "1.875rem", + ".txt-small-plus": { + "fontSize": "0.8125rem", + "lineHeight": "1.3000000715255737rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h2-core": { + ".txt-large": { "fontSize": "1rem", - "lineHeight": "1.5rem", - "fontWeight": "500", + "lineHeight": "1.600000023841858rem", + "fontWeight": "400", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h3-core": { + ".txt-medium-plus": { "fontSize": "0.875rem", - "lineHeight": "1.25rem", + "lineHeight": "1.399999976158142rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h1-core": { - "fontSize": "1.125rem", - "lineHeight": "1.75rem", - "fontWeight": "500", + ".txt-xsmall": { + "fontSize": "0.75rem", + "lineHeight": "1.2000000476837158rem", + "fontWeight": "400", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h1-docs": { - "fontSize": "1.5rem", - "lineHeight": "1.875rem", + ".h3-docs": { + "fontSize": "1rem", + "lineHeight": "1.600000023841858rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h2-docs": { + ".txt-xlarge-plus": { "fontSize": "1.125rem", - "lineHeight": "1.6875rem", + "lineHeight": "1.8000000715255737rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".h3-docs": { + ".txt-large-plus": { "fontSize": "1rem", - "lineHeight": "1.5rem", + "lineHeight": "1.600000023841858rem", "fontWeight": "500", "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" }, - ".code-body": { - "fontSize": "0.75rem", - "lineHeight": "1.125rem", + ".txt-xlarge": { + "fontSize": "1.125rem", + "lineHeight": "1.8000000715255737rem", "fontWeight": "400", - "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" - }, - ".code-label": { - "fontSize": "0.75rem", - "lineHeight": "0.9375rem", - "fontWeight": "500", - "fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" + "fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji" } } \ No newline at end of file diff --git a/packages/design-system/ui/CHANGELOG.md b/packages/design-system/ui/CHANGELOG.md index d757e1873713f..7fbf60cb23499 100644 --- a/packages/design-system/ui/CHANGELOG.md +++ b/packages/design-system/ui/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/ui +## 4.0.18 + +### Patch Changes + +- Updated dependencies []: + - @medusajs/icons@2.8.8 + ## 4.0.17 ### Patch Changes diff --git a/packages/design-system/ui/package.json b/packages/design-system/ui/package.json index ae57388da51d3..8a547d284508b 100644 --- a/packages/design-system/ui/package.json +++ b/packages/design-system/ui/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/ui", - "version": "4.0.17", + "version": "4.0.18", "author": "Kasper Kristensen ", "license": "MIT", "repository": { @@ -43,7 +43,7 @@ }, "devDependencies": { "@faker-js/faker": "^9.2.0", - "@medusajs/ui-preset": "2.8.7", + "@medusajs/ui-preset": "2.8.8", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", "@storybook/addon-links": "^8.3.5", @@ -81,7 +81,7 @@ "vitest": "^3.0.5" }, "dependencies": { - "@medusajs/icons": "2.8.7", + "@medusajs/icons": "2.8.8", "@tanstack/react-table": "8.20.5", "clsx": "^1.2.1", "copy-to-clipboard": "^3.3.3", diff --git a/packages/medusa-telemetry/CHANGELOG.md b/packages/medusa-telemetry/CHANGELOG.md index 61c44b587066e..175e753b104bc 100644 --- a/packages/medusa-telemetry/CHANGELOG.md +++ b/packages/medusa-telemetry/CHANGELOG.md @@ -1,5 +1,7 @@ # Change Log +## 2.8.8 + ## 2.8.7 ## 2.8.6 diff --git a/packages/medusa-telemetry/package.json b/packages/medusa-telemetry/package.json index d7e450e558b41..b26ed536371c0 100644 --- a/packages/medusa-telemetry/package.json +++ b/packages/medusa-telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/telemetry", - "version": "2.8.7", + "version": "2.8.8", "description": "Telemetry for Medusa", "main": "dist/index.js", "repository": { diff --git a/packages/medusa-test-utils/CHANGELOG.md b/packages/medusa-test-utils/CHANGELOG.md index 25ef4564525b1..3ffa20fcb3489 100644 --- a/packages/medusa-test-utils/CHANGELOG.md +++ b/packages/medusa-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 2.8.8 + +### Patch Changes + +- [#12903](https://github.com/medusajs/medusa/pull/12903) [`c5d609d09cb29c6cf01d1c6c65305cc566f391c5`](https://github.com/medusajs/medusa/commit/c5d609d09cb29c6cf01d1c6c65305cc566f391c5) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(orchestration): Prevent workf. cancellation to execute while rescheduling + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + - @medusajs/medusa@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/medusa-test-utils/package.json b/packages/medusa-test-utils/package.json index ed7ca2d6009e7..e2345717c7593 100644 --- a/packages/medusa-test-utils/package.json +++ b/packages/medusa-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/test-utils", - "version": "2.8.7", + "version": "2.8.8", "description": "Test utils for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -45,8 +45,8 @@ "randomatic": "^3.1.1" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/medusa": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/medusa": "2.8.8", "@mikro-orm/postgresql": "6.4.3", "awilix": "^8.0.1" }, diff --git a/packages/medusa-test-utils/src/__tests__/events.spec.ts b/packages/medusa-test-utils/src/__tests__/events.spec.ts index ea8a5a0580f96..15c788dd4d919 100644 --- a/packages/medusa-test-utils/src/__tests__/events.spec.ts +++ b/packages/medusa-test-utils/src/__tests__/events.spec.ts @@ -1,5 +1,8 @@ import { EventEmitter } from "events" import { waitSubscribersExecution } from "../events" +import { setTimeout } from "timers/promises" + +jest.setTimeout(30000) // Mock the IEventBusModuleService class MockEventBus { @@ -31,11 +34,13 @@ describe("waitSubscribersExecution", () => { describe("with no existing listeners", () => { it("should resolve when event is fired before timeout", async () => { const waitPromise = waitSubscribersExecution(TEST_EVENT, eventBus as any) - setTimeout(() => eventBus.emit(TEST_EVENT, "test-data"), 100).unref() + await setTimeout(100) + eventBus.emit(TEST_EVENT, "test-data") jest.advanceTimersByTime(100) - await expect(waitPromise).resolves.toEqual(["test-data"]) + const res = await waitPromise + expect(res).toEqual(["test-data"]) }) it("should reject when timeout is reached before event is fired", async () => { @@ -70,12 +75,29 @@ describe("waitSubscribersExecution", () => { `Timeout of ${customTimeout}ms exceeded while waiting for event "${TEST_EVENT}"` ) }) + + it("should resolve when event is fired multiple times", async () => { + const waitPromise = waitSubscribersExecution( + TEST_EVENT, + eventBus as any, + { triggerCount: 2 } + ) + eventBus.emit(TEST_EVENT, "test-data") + eventBus.emit(TEST_EVENT, "test-data") + + const promisesRes = await waitPromise + const res = promisesRes.pop() + expect(res).toHaveLength(2) + expect(res[0]).toEqual(["test-data"]) + expect(res[1]).toEqual(["test-data"]) + }) }) describe("with existing listeners", () => { it("should resolve when all listeners complete successfully", async () => { - const listener = jest.fn().mockImplementation(() => { - return new Promise((resolve) => setTimeout(resolve, 200).unref()) + const listener = jest.fn().mockImplementation(async () => { + await setTimeout(200) + return "res" }) eventBus.eventEmitter_.on(TEST_EVENT, listener) @@ -132,20 +154,49 @@ describe("waitSubscribersExecution", () => { expect(listener).not.toHaveBeenCalled() }) + + it("should resolve when event is fired multiple times", async () => { + const listener = jest.fn().mockImplementation(async () => { + await setTimeout(200) + return "res" + }) + + eventBus.eventEmitter_.on(TEST_EVENT, listener) + + const waitPromise = waitSubscribersExecution( + TEST_EVENT, + eventBus as any, + { + triggerCount: 2, + } + ) + + eventBus.emit(TEST_EVENT, "test-data") + eventBus.emit(TEST_EVENT, "test-data") + + const promisesRes = await waitPromise + const res = promisesRes.pop() + expect(res).toHaveLength(2) + expect(res[0]).toEqual("res") + expect(res[1]).toEqual("res") + }) }) describe("with multiple listeners", () => { it("should resolve when all listeners complete", async () => { - const listener1 = jest.fn().mockImplementation(() => { - return new Promise((resolve) => setTimeout(resolve, 100).unref()) + const listener1 = jest.fn().mockImplementation(async () => { + await setTimeout(100) + return "res" }) - const listener2 = jest.fn().mockImplementation(() => { - return new Promise((resolve) => setTimeout(resolve, 200).unref()) + const listener2 = jest.fn().mockImplementation(async () => { + await setTimeout(200) + return "res" }) - const listener3 = jest.fn().mockImplementation(() => { - return new Promise((resolve) => setTimeout(resolve, 300).unref()) + const listener3 = jest.fn().mockImplementation(async () => { + await setTimeout(300) + return "res" }) eventBus.eventEmitter_.on(TEST_EVENT, listener1) diff --git a/packages/medusa-test-utils/src/events.ts b/packages/medusa-test-utils/src/events.ts index 1e7be09956693..4b9cf9e04b71a 100644 --- a/packages/medusa-test-utils/src/events.ts +++ b/packages/medusa-test-utils/src/events.ts @@ -5,7 +5,10 @@ type EventBus = { } type WaitSubscribersExecutionOptions = { + /** Timeout in milliseconds for waiting for the event. Defaults to 15000ms. */ timeout?: number + /** Number of times the event should be triggered before resolving. Defaults to 1. */ + triggerCount?: number } // Map to hold pending promises for each event. @@ -41,7 +44,7 @@ const createTimeoutPromise = ( const doWaitSubscribersExecution = ( eventName: string | symbol, eventBus: EventBus, - { timeout = 15000 }: WaitSubscribersExecutionOptions = {} + { timeout = 15000, triggerCount = 1 }: WaitSubscribersExecutionOptions = {} ): Promise => { const eventEmitter = eventBus.eventEmitter_ const subscriberPromises: Promise[] = [] @@ -50,6 +53,8 @@ const doWaitSubscribersExecution = ( eventName ) + let currentCount = 0 + if (!eventEmitter.listeners(eventName).length) { let ok: (value?: any) => void const promise = new Promise((resolve) => { @@ -57,9 +62,19 @@ const doWaitSubscribersExecution = ( }) subscriberPromises.push(promise) + let res: any[] = [] const newListener = async (...args: any[]) => { - eventEmitter.removeListener(eventName, newListener) - ok(...args) + currentCount++ + res.push(args) + + if (currentCount >= triggerCount) { + eventEmitter.removeListener(eventName, newListener) + if (triggerCount === 1) { + ok(...args) + } else { + ok(res) + } + } } Object.defineProperty(newListener, "__isSubscribersExecutionWrapper", { @@ -83,22 +98,38 @@ const doWaitSubscribersExecution = ( nok = reject }) subscriberPromises.push(promise) + let res: any[] = [] const newListener = async (...args2: any[]) => { - // As soon as the subscriber is executed, we restore the original listener - eventEmitter.removeListener(eventName, newListener) - let listenerToAdd = listener - while (listenerToAdd.originalListener) { - listenerToAdd = listenerToAdd.originalListener - } - eventEmitter.on(eventName, listenerToAdd) - try { - const res = await listener.apply(eventBus, args2) - ok(res) + const listenerRes = listener.apply(eventBus, args2) + if (typeof listenerRes?.then === "function") { + await listenerRes.then((res_) => { + res.push(res_) + currentCount++ + }) + } else { + res.push(listenerRes) + currentCount++ + } + + if (currentCount >= triggerCount) { + const res_ = triggerCount === 1 ? res[0] : res + ok(res_) + } } catch (error) { nok(error) } + + if (currentCount >= triggerCount) { + // As soon as the subscriber is executed the required number of times, we restore the original listener + eventEmitter.removeListener(eventName, newListener) + let listenerToAdd = listener + while (listenerToAdd.originalListener) { + listenerToAdd = listenerToAdd.originalListener + } + eventEmitter.on(eventName, listenerToAdd) + } } Object.defineProperty(newListener, "__isSubscribersExecutionWrapper", { @@ -130,7 +161,7 @@ const doWaitSubscribersExecution = ( * * @param eventName - The name of the event to wait for. * @param eventBus - The event bus instance. - * @param options - Options including timeout. + * @param options - Options including timeout and triggerCount. */ export const waitSubscribersExecution = ( eventName: string | symbol, diff --git a/packages/medusa-test-utils/src/medusa-test-runner-utils/wait-workflow-executions.ts b/packages/medusa-test-utils/src/medusa-test-runner-utils/wait-workflow-executions.ts index 8d425f87af9d6..5d2406567cc53 100644 --- a/packages/medusa-test-utils/src/medusa-test-runner-utils/wait-workflow-executions.ts +++ b/packages/medusa-test-utils/src/medusa-test-runner-utils/wait-workflow-executions.ts @@ -17,7 +17,7 @@ export async function waitWorkflowExecutions(container: MedusaContainer) { const timeout = setTimeout(() => { throw new Error("Timeout waiting for workflow executions to finish") - }, 10000).unref() + }, 60000).unref() let waitWorkflowsToFinish = true while (waitWorkflowsToFinish) { diff --git a/packages/medusa/CHANGELOG.md b/packages/medusa/CHANGELOG.md index 8d6bc34a651d7..a64fcb7ca3e55 100644 --- a/packages/medusa/CHANGELOG.md +++ b/packages/medusa/CHANGELOG.md @@ -1,5 +1,58 @@ # Change Log +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`a28226af80a8880afbdb926a5001f0cb0d89fdc9`](https://github.com/medusajs/medusa/commit/a28226af80a8880afbdb926a5001f0cb0d89fdc9), [`40625c82d6deb28ecb4e4c0f911c28fdd7356bf7`](https://github.com/medusajs/medusa/commit/40625c82d6deb28ecb4e4c0f911c28fdd7356bf7), [`822217fa366d4399d3f6a0407451c9649197d5d9`](https://github.com/medusajs/medusa/commit/822217fa366d4399d3f6a0407451c9649197d5d9), [`97a8e5cb2e8819672803bfcd9cde19cb1ce1acc0`](https://github.com/medusajs/medusa/commit/97a8e5cb2e8819672803bfcd9cde19cb1ce1acc0), [`7b1debfe12fc096f7b4e20f13bdeb925c96085c1`](https://github.com/medusajs/medusa/commit/7b1debfe12fc096f7b4e20f13bdeb925c96085c1), [`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7), [`eb83954f23077c0714125b6f2f19fd0ef0f288f9`](https://github.com/medusajs/medusa/commit/eb83954f23077c0714125b6f2f19fd0ef0f288f9), [`8c4228fc42e717f9ab72230040e708f606a585b7`](https://github.com/medusajs/medusa/commit/8c4228fc42e717f9ab72230040e708f606a585b7), [`439c7118450c5f9ee0b541de9014093a42b7d0ea`](https://github.com/medusajs/medusa/commit/439c7118450c5f9ee0b541de9014093a42b7d0ea), [`238e7d53c13a1c033886d7c33254919f8b5b22dc`](https://github.com/medusajs/medusa/commit/238e7d53c13a1c033886d7c33254919f8b5b22dc), [`7669dbb03e2f65fa76cff1c5b90a0777e475cb47`](https://github.com/medusajs/medusa/commit/7669dbb03e2f65fa76cff1c5b90a0777e475cb47), [`2c2528a08751945d6f0363473605f1a8ef1a8a2a`](https://github.com/medusajs/medusa/commit/2c2528a08751945d6f0363473605f1a8ef1a8a2a), [`798ac0068ed841d31f6aefc11c58738dc6bf8bd0`](https://github.com/medusajs/medusa/commit/798ac0068ed841d31f6aefc11c58738dc6bf8bd0)]: + - @medusajs/core-flows@2.8.8 + - @medusajs/pricing@2.8.8 + - @medusajs/link-modules@2.8.8 + - @medusajs/workflow-engine-inmemory@2.8.8 + - @medusajs/framework@2.8.8 + - @medusajs/workflow-engine-redis@2.8.8 + - @medusajs/product@2.8.8 + - @medusajs/payment@2.8.8 + - @medusajs/admin-bundler@2.8.8 + - @medusajs/analytics@2.8.8 + - @medusajs/api-key@2.8.8 + - @medusajs/auth@2.8.8 + - @medusajs/cache-inmemory@2.8.8 + - @medusajs/cache-redis@2.8.8 + - @medusajs/cart@2.8.8 + - @medusajs/currency@2.8.8 + - @medusajs/customer@2.8.8 + - @medusajs/event-bus-local@2.8.8 + - @medusajs/event-bus-redis@2.8.8 + - @medusajs/file@2.8.8 + - @medusajs/fulfillment@2.8.8 + - @medusajs/index@2.8.8 + - @medusajs/inventory@2.8.8 + - @medusajs/locking@2.8.8 + - @medusajs/notification@2.8.8 + - @medusajs/order@2.8.8 + - @medusajs/promotion@2.8.8 + - @medusajs/analytics-local@2.8.8 + - @medusajs/analytics-posthog@2.8.8 + - @medusajs/auth-emailpass@2.8.8 + - @medusajs/auth-github@2.8.8 + - @medusajs/auth-google@2.8.8 + - @medusajs/file-local@2.8.8 + - @medusajs/file-s3@2.8.8 + - @medusajs/fulfillment-manual@2.8.8 + - @medusajs/locking-postgres@2.8.8 + - @medusajs/locking-redis@2.8.8 + - @medusajs/notification-local@2.8.8 + - @medusajs/notification-sendgrid@2.8.8 + - @medusajs/payment-stripe@2.8.8 + - @medusajs/region@2.8.8 + - @medusajs/sales-channel@2.8.8 + - @medusajs/stock-location@2.8.8 + - @medusajs/store@2.8.8 + - @medusajs/tax@2.8.8 + - @medusajs/user@2.8.8 + - @medusajs/telemetry@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 75717607b3ef7..aa625abc2b4b8 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa", - "version": "2.8.7", + "version": "2.8.8", "description": "Building blocks for digital commerce", "main": "dist/index.js", "exports": { @@ -36,6 +36,10 @@ }, "author": "Sebastian Rindom", "license": "MIT", + "homepage": "https://medusajs.com", + "llms": "https://docs.medusajs.com/llms.txt", + "llmsFull": "https://docs.medusajs.com/llms-full.txt", + "mcpServer": "https://docs.medusajs.com/mcp", "scripts": { "watch": "tsc --build --watch", "build": "rimraf dist && tsc --build", @@ -44,7 +48,7 @@ "test:integration": "jest --forceExit -- src/**/integration-tests/**/__tests__/**/*.ts" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@opentelemetry/instrumentation-pg": "^0.52.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-node": "^0.200.0", @@ -65,52 +69,52 @@ "dependencies": { "@inquirer/checkbox": "^2.3.11", "@inquirer/input": "^2.2.9", - "@medusajs/admin-bundler": "2.8.7", - "@medusajs/analytics": "2.8.7", - "@medusajs/analytics-local": "2.8.7", - "@medusajs/analytics-posthog": "2.8.7", - "@medusajs/api-key": "2.8.7", - "@medusajs/auth": "2.8.7", - "@medusajs/auth-emailpass": "2.8.7", - "@medusajs/auth-github": "2.8.7", - "@medusajs/auth-google": "2.8.7", - "@medusajs/cache-inmemory": "2.8.7", - "@medusajs/cache-redis": "2.8.7", - "@medusajs/cart": "2.8.7", - "@medusajs/core-flows": "2.8.7", - "@medusajs/currency": "2.8.7", - "@medusajs/customer": "2.8.7", - "@medusajs/event-bus-local": "2.8.7", - "@medusajs/event-bus-redis": "2.8.7", - "@medusajs/file": "2.8.7", - "@medusajs/file-local": "2.8.7", - "@medusajs/file-s3": "2.8.7", - "@medusajs/fulfillment": "2.8.7", - "@medusajs/fulfillment-manual": "2.8.7", - "@medusajs/index": "2.8.7", - "@medusajs/inventory": "2.8.7", - "@medusajs/link-modules": "2.8.7", - "@medusajs/locking": "2.8.7", - "@medusajs/locking-postgres": "2.8.7", - "@medusajs/locking-redis": "2.8.7", - "@medusajs/notification": "2.8.7", - "@medusajs/notification-local": "2.8.7", - "@medusajs/notification-sendgrid": "2.8.7", - "@medusajs/order": "2.8.7", - "@medusajs/payment": "2.8.7", - "@medusajs/payment-stripe": "2.8.7", - "@medusajs/pricing": "2.8.7", - "@medusajs/product": "2.8.7", - "@medusajs/promotion": "2.8.7", - "@medusajs/region": "2.8.7", - "@medusajs/sales-channel": "2.8.7", - "@medusajs/stock-location": "2.8.7", - "@medusajs/store": "2.8.7", - "@medusajs/tax": "2.8.7", - "@medusajs/telemetry": "2.8.7", - "@medusajs/user": "2.8.7", - "@medusajs/workflow-engine-inmemory": "2.8.7", - "@medusajs/workflow-engine-redis": "2.8.7", + "@medusajs/admin-bundler": "2.8.8", + "@medusajs/analytics": "2.8.8", + "@medusajs/analytics-local": "2.8.8", + "@medusajs/analytics-posthog": "2.8.8", + "@medusajs/api-key": "2.8.8", + "@medusajs/auth": "2.8.8", + "@medusajs/auth-emailpass": "2.8.8", + "@medusajs/auth-github": "2.8.8", + "@medusajs/auth-google": "2.8.8", + "@medusajs/cache-inmemory": "2.8.8", + "@medusajs/cache-redis": "2.8.8", + "@medusajs/cart": "2.8.8", + "@medusajs/core-flows": "2.8.8", + "@medusajs/currency": "2.8.8", + "@medusajs/customer": "2.8.8", + "@medusajs/event-bus-local": "2.8.8", + "@medusajs/event-bus-redis": "2.8.8", + "@medusajs/file": "2.8.8", + "@medusajs/file-local": "2.8.8", + "@medusajs/file-s3": "2.8.8", + "@medusajs/fulfillment": "2.8.8", + "@medusajs/fulfillment-manual": "2.8.8", + "@medusajs/index": "2.8.8", + "@medusajs/inventory": "2.8.8", + "@medusajs/link-modules": "2.8.8", + "@medusajs/locking": "2.8.8", + "@medusajs/locking-postgres": "2.8.8", + "@medusajs/locking-redis": "2.8.8", + "@medusajs/notification": "2.8.8", + "@medusajs/notification-local": "2.8.8", + "@medusajs/notification-sendgrid": "2.8.8", + "@medusajs/order": "2.8.8", + "@medusajs/payment": "2.8.8", + "@medusajs/payment-stripe": "2.8.8", + "@medusajs/pricing": "2.8.8", + "@medusajs/product": "2.8.8", + "@medusajs/promotion": "2.8.8", + "@medusajs/region": "2.8.8", + "@medusajs/sales-channel": "2.8.8", + "@medusajs/stock-location": "2.8.8", + "@medusajs/store": "2.8.8", + "@medusajs/tax": "2.8.8", + "@medusajs/telemetry": "2.8.8", + "@medusajs/user": "2.8.8", + "@medusajs/workflow-engine-inmemory": "2.8.8", + "@medusajs/workflow-engine-redis": "2.8.8", "@opentelemetry/api": "^1.9.0", "boxen": "^5.0.1", "chalk": "^4.0.0", @@ -126,10 +130,10 @@ "request-ip": "^3.3.0", "slugify": "^1.6.6", "uuid": "^9.0.0", - "zod": "3.22.4" + "zod": "3.25.76" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", "@mikro-orm/migrations": "6.4.3", diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/route.ts index e0020a803fbee..c7a9526758dbc 100644 --- a/packages/medusa/src/api/admin/draft-orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/draft-orders/[id]/route.ts @@ -57,7 +57,7 @@ export const POST = async ( } /** - * @version 2.8.4 + * @since 2.8.4 */ export const DELETE = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/admin/products/imports/[transaction_id]/confirm/route.ts b/packages/medusa/src/api/admin/products/imports/[transaction_id]/confirm/route.ts index 8174d78a5e2fc..67690e1fa2d48 100644 --- a/packages/medusa/src/api/admin/products/imports/[transaction_id]/confirm/route.ts +++ b/packages/medusa/src/api/admin/products/imports/[transaction_id]/confirm/route.ts @@ -12,7 +12,7 @@ import { Modules, TransactionHandlerType } from "@medusajs/framework/utils" import { StepResponse } from "@medusajs/framework/workflows-sdk" /** - * @version 2.8.5 + * @since 2.8.5 */ export const POST = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/admin/products/imports/route.ts b/packages/medusa/src/api/admin/products/imports/route.ts index 831f459e91b16..22574772e8182 100644 --- a/packages/medusa/src/api/admin/products/imports/route.ts +++ b/packages/medusa/src/api/admin/products/imports/route.ts @@ -7,7 +7,7 @@ import type { AdminImportProductsType } from "../validators" import { importProductsAsChunksWorkflow } from "@medusajs/core-flows" /** - * @version 2.8.5 + * @since 2.8.5 */ export const POST = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts index 589c7be7ba8fc..f343f2f1bbcbe 100644 --- a/packages/medusa/src/api/admin/products/validators.ts +++ b/packages/medusa/src/api/admin/products/validators.ts @@ -68,7 +68,7 @@ export const AdminGetProductsParams = createFindParams({ .merge(applyAndAndOrOperators(AdminGetProductsParamsDirectFields)) .merge(GetProductsParams) ) - .transform(transformProductParams) + .transform(transformProductParams as any) export const AdminGetProductOptionsParamsFields = z.object({ q: z.string().optional(), diff --git a/packages/medusa/src/api/admin/tax-providers/route.ts b/packages/medusa/src/api/admin/tax-providers/route.ts index 6a29f8f1d12c1..cbf55eda4d9d6 100644 --- a/packages/medusa/src/api/admin/tax-providers/route.ts +++ b/packages/medusa/src/api/admin/tax-providers/route.ts @@ -7,7 +7,7 @@ import { import { HttpTypes } from "@medusajs/framework/types" /** - * @version 2.8.0 + * @since 2.8.0 */ export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api/store/carts/query-config.ts b/packages/medusa/src/api/store/carts/query-config.ts index 5dadbb7bca59b..4b5ca64f668e5 100644 --- a/packages/medusa/src/api/store/carts/query-config.ts +++ b/packages/medusa/src/api/store/carts/query-config.ts @@ -35,6 +35,7 @@ export const defaultStoreCartFields = [ "promotions.id", "promotions.code", "promotions.is_automatic", + "promotions.is_tax_inclusive", "promotions.application_method.value", "promotions.application_method.type", "promotions.application_method.currency_code", @@ -77,6 +78,7 @@ export const defaultStoreCartFields = [ "items.adjustments.code", "items.adjustments.promotion_id", "items.adjustments.amount", + "items.adjustments.is_tax_inclusive", "customer.id", "customer.email", "customer.groups.id", diff --git a/packages/medusa/src/api/store/products/validators.ts b/packages/medusa/src/api/store/products/validators.ts index 8f3fd9c7cf8cc..42bfacd5e4f3c 100644 --- a/packages/medusa/src/api/store/products/validators.ts +++ b/packages/medusa/src/api/store/products/validators.ts @@ -81,4 +81,4 @@ export const StoreGetProductsParams = createFindParams({ .merge(applyAndAndOrOperators(StoreGetProductParamsDirectFields)) .strict() ) - .transform(recursivelyNormalizeSchema(transformProductParams)) + .transform(recursivelyNormalizeSchema(transformProductParams) as any) diff --git a/packages/medusa/src/api/store/returns/route.ts b/packages/medusa/src/api/store/returns/route.ts index 67141ef8767e7..0fcfd8cf2ba7c 100644 --- a/packages/medusa/src/api/store/returns/route.ts +++ b/packages/medusa/src/api/store/returns/route.ts @@ -3,7 +3,7 @@ import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import { HttpTypes } from "@medusajs/framework/types" /** - * @version 2.8.0 + * @since 2.8.0 */ export const POST = async ( req: MedusaRequest, diff --git a/packages/medusa/src/api/utils/common-validators/common.ts b/packages/medusa/src/api/utils/common-validators/common.ts index e813b192b8bbd..f0be066db46e4 100644 --- a/packages/medusa/src/api/utils/common-validators/common.ts +++ b/packages/medusa/src/api/utils/common-validators/common.ts @@ -31,7 +31,9 @@ export const BigNumberInput = z.union([ * @param {ZodObject} schema * @return {ZodObject} */ -export const applyAndAndOrOperators = (schema: z.ZodObject) => { +export const applyAndAndOrOperators = >( + schema: T +) => { return schema.merge( z.object({ $and: z.lazy(() => schema.array()).optional(), diff --git a/packages/modules/analytics/CHANGELOG.md b/packages/modules/analytics/CHANGELOG.md index 1968227d550c5..be0da20de974d 100644 --- a/packages/modules/analytics/CHANGELOG.md +++ b/packages/modules/analytics/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/analytics +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/analytics/package.json b/packages/modules/analytics/package.json index 1044fa293f467..c4fb39dfb938f 100644 --- a/packages/modules/analytics/package.json +++ b/packages/modules/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/analytics", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Analytics module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -32,8 +32,8 @@ "test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -42,7 +42,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" } } diff --git a/packages/modules/api-key/CHANGELOG.md b/packages/modules/api-key/CHANGELOG.md index d08ecfe0b6311..d15e8b4e966bd 100644 --- a/packages/modules/api-key/CHANGELOG.md +++ b/packages/modules/api-key/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/api-key +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/api-key/package.json b/packages/modules/api-key/package.json index 2c27a07a4bb76..fef177971238f 100644 --- a/packages/modules/api-key/package.json +++ b/packages/modules/api-key/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/api-key", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa API Key module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", @@ -47,7 +47,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/auth/CHANGELOG.md b/packages/modules/auth/CHANGELOG.md index 33949dbdcd8ee..b848f0197d9a1 100644 --- a/packages/modules/auth/CHANGELOG.md +++ b/packages/modules/auth/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/auth +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/auth/package.json b/packages/modules/auth/package.json index 5f4808ed22731..8e602470cc202 100644 --- a/packages/modules/auth/package.json +++ b/packages/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/auth", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Auth module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/auth/src/services/auth-provider.ts b/packages/modules/auth/src/services/auth-provider.ts index f919faba844c2..5dff91bdde764 100644 --- a/packages/modules/auth/src/services/auth-provider.ts +++ b/packages/modules/auth/src/services/auth-provider.ts @@ -35,6 +35,10 @@ export default class AuthProviderService { const errMessage = ` Unable to retrieve the auth provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/cache-inmemory/CHANGELOG.md b/packages/modules/cache-inmemory/CHANGELOG.md index d4da4b363ddcf..8e6b63a53bcf0 100644 --- a/packages/modules/cache-inmemory/CHANGELOG.md +++ b/packages/modules/cache-inmemory/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/cache-inmemory +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/cache-inmemory/package.json b/packages/modules/cache-inmemory/package.json index f32856af386b7..6cda57e105cc1 100644 --- a/packages/modules/cache-inmemory/package.json +++ b/packages/modules/cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/cache-inmemory", - "version": "2.8.7", + "version": "2.8.8", "description": "In-memory Cache Module for Medusa", "main": "dist/index.js", "repository": { @@ -23,7 +23,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -36,6 +36,6 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" } } diff --git a/packages/modules/cache-redis/CHANGELOG.md b/packages/modules/cache-redis/CHANGELOG.md index aa07fc4dd7ae4..69d1cb8231e2f 100644 --- a/packages/modules/cache-redis/CHANGELOG.md +++ b/packages/modules/cache-redis/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/cache-redis +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/cache-redis/package.json b/packages/modules/cache-redis/package.json index f5fe5156e0507..06b1bfc69eb98 100644 --- a/packages/modules/cache-redis/package.json +++ b/packages/modules/cache-redis/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/cache-redis", - "version": "2.8.7", + "version": "2.8.8", "description": "Redis Cache Module for Medusa", "main": "dist/index.js", "repository": { @@ -23,7 +23,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -39,7 +39,7 @@ "ioredis": "^5.4.1" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" } } diff --git a/packages/modules/cart/CHANGELOG.md b/packages/modules/cart/CHANGELOG.md index 82c59641c2281..f8c71a4d3a240 100644 --- a/packages/modules/cart/CHANGELOG.md +++ b/packages/modules/cart/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/cart +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/cart/package.json b/packages/modules/cart/package.json index fc8c9f02b3dc9..a3cd3d25f64a1 100644 --- a/packages/modules/cart/package.json +++ b/packages/modules/cart/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/cart", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Cart module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/currency/CHANGELOG.md b/packages/modules/currency/CHANGELOG.md index 030f8d372ea96..481cd71d6d4e1 100644 --- a/packages/modules/currency/CHANGELOG.md +++ b/packages/modules/currency/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/currency +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/currency/integration-tests/__tests__/currency-module-service.spec.ts b/packages/modules/currency/integration-tests/__tests__/currency-module-service.spec.ts index 12f51c424774e..c438e55048076 100644 --- a/packages/modules/currency/integration-tests/__tests__/currency-module-service.spec.ts +++ b/packages/modules/currency/integration-tests/__tests__/currency-module-service.spec.ts @@ -86,7 +86,7 @@ moduleIntegrationTestRunner({ const [currenciesResult, count] = await service.listAndCountCurrencies({}, {}) - expect(count).toEqual(120) + expect(count).toEqual(122) expect(currenciesResult).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -123,7 +123,7 @@ moduleIntegrationTestRunner({ const [currenciesResult, count] = await service.listAndCountCurrencies({}, { skip: 5, take: 1 }) - expect(count).toEqual(120) + expect(count).toEqual(122) expect(currenciesResult).toEqual([ expect.objectContaining({ code: "aud", @@ -144,7 +144,7 @@ moduleIntegrationTestRunner({ const serialized = JSON.parse(JSON.stringify(currenciesResult)) - expect(count).toEqual(120) + expect(count).toEqual(122) expect(serialized).toEqual([ { code: "aed", diff --git a/packages/modules/currency/package.json b/packages/modules/currency/package.json index ab7b4af52370b..6ce58b3ce01d3 100644 --- a/packages/modules/currency/package.json +++ b/packages/modules/currency/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/currency", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Currency module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/customer/CHANGELOG.md b/packages/modules/customer/CHANGELOG.md index 88a7121b80654..aae063bf35f73 100644 --- a/packages/modules/customer/CHANGELOG.md +++ b/packages/modules/customer/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/customer +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/customer/package.json b/packages/modules/customer/package.json index 55a52651c6cfa..61da1f661ecbe 100644 --- a/packages/modules/customer/package.json +++ b/packages/modules/customer/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/customer", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Customer module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -37,8 +37,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -51,7 +51,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/event-bus-local/CHANGELOG.md b/packages/modules/event-bus-local/CHANGELOG.md index 568623bec9afc..7ebfa17cc416b 100644 --- a/packages/modules/event-bus-local/CHANGELOG.md +++ b/packages/modules/event-bus-local/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/event-bus-local +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/event-bus-local/package.json b/packages/modules/event-bus-local/package.json index f377b7944ded5..c157ccc0186b7 100644 --- a/packages/modules/event-bus-local/package.json +++ b/packages/modules/event-bus-local/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/event-bus-local", - "version": "2.8.7", + "version": "2.8.8", "description": "Local Event Bus Module for Medusa", "main": "dist/index.js", "files": [ @@ -23,7 +23,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -39,6 +39,6 @@ "ulid": "^2.3.0" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" } } diff --git a/packages/modules/event-bus-redis/CHANGELOG.md b/packages/modules/event-bus-redis/CHANGELOG.md index 7d24de2dfcf6c..55dde3759ac22 100644 --- a/packages/modules/event-bus-redis/CHANGELOG.md +++ b/packages/modules/event-bus-redis/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/event-bus-redis +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/event-bus-redis/package.json b/packages/modules/event-bus-redis/package.json index e2c5a1a1d2b0b..1b463f9ffda15 100644 --- a/packages/modules/event-bus-redis/package.json +++ b/packages/modules/event-bus-redis/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/event-bus-redis", - "version": "2.8.7", + "version": "2.8.8", "description": "Redis Event Bus Module for Medusa", "main": "dist/index.js", "files": [ @@ -23,7 +23,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -40,7 +40,7 @@ "ioredis": "^5.4.1" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" } } diff --git a/packages/modules/file/CHANGELOG.md b/packages/modules/file/CHANGELOG.md index 2337620c747c3..ee8e6fe44664c 100644 --- a/packages/modules/file/CHANGELOG.md +++ b/packages/modules/file/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/product +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/file/package.json b/packages/modules/file/package.json index ddca9cfb0d728..0010bd1b45b04 100644 --- a/packages/modules/file/package.json +++ b/packages/modules/file/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/file", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa File module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" } } diff --git a/packages/modules/fulfillment/CHANGELOG.md b/packages/modules/fulfillment/CHANGELOG.md index 3c83acb7c9909..d83e36ca8b1c9 100644 --- a/packages/modules/fulfillment/CHANGELOG.md +++ b/packages/modules/fulfillment/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/fulfillment +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/fulfillment/package.json b/packages/modules/fulfillment/package.json index 21ef6c36be3a2..3f6deaa3291bf 100644 --- a/packages/modules/fulfillment/package.json +++ b/packages/modules/fulfillment/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/fulfillment", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Fulfillment module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/fulfillment/src/services/fulfillment-provider.ts b/packages/modules/fulfillment/src/services/fulfillment-provider.ts index 104410fc25391..c10b16b05accb 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-provider.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-provider.ts @@ -66,6 +66,10 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.MedusaIn const errMessage = ` Unable to retrieve the fulfillment provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/index/CHANGELOG.md b/packages/modules/index/CHANGELOG.md index 271516ad5a329..2c1c6ba0499c9 100644 --- a/packages/modules/index/CHANGELOG.md +++ b/packages/modules/index/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/index +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/index/package.json b/packages/modules/index/package.json index 10490acb07312..bb723633e5fa5 100644 --- a/packages/modules/index/package.json +++ b/packages/modules/index/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/index", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Index module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", @@ -51,7 +51,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/knex": "6.4.3", "@mikro-orm/migrations": "6.4.3", diff --git a/packages/modules/inventory/CHANGELOG.md b/packages/modules/inventory/CHANGELOG.md index f352c84b0fcc6..b0497b8f1556a 100644 --- a/packages/modules/inventory/CHANGELOG.md +++ b/packages/modules/inventory/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/inventory-next +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/inventory/package.json b/packages/modules/inventory/package.json index 6449f903e20df..7e4b65c5faffb 100644 --- a/packages/modules/inventory/package.json +++ b/packages/modules/inventory/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/inventory", - "version": "2.8.7", + "version": "2.8.8", "description": "Inventory Module for Medusa", "main": "dist/index.js", "repository": { @@ -23,8 +23,8 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -37,7 +37,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/link-modules/CHANGELOG.md b/packages/modules/link-modules/CHANGELOG.md index 9507eb7a6abaf..1816a4b4b5002 100644 --- a/packages/modules/link-modules/CHANGELOG.md +++ b/packages/modules/link-modules/CHANGELOG.md @@ -1,5 +1,16 @@ # @medusajs/link-modules +## 2.8.8 + +### Patch Changes + +- [#12928](https://github.com/medusajs/medusa/pull/12928) [`97a8e5cb2e8819672803bfcd9cde19cb1ce1acc0`](https://github.com/medusajs/medusa/commit/97a8e5cb2e8819672803bfcd9cde19cb1ce1acc0) Thanks [@riqwan](https://github.com/riqwan)! - chore(link-modules): keep promotion alias + +- [#12920](https://github.com/medusajs/medusa/pull/12920) [`8c4228fc42e717f9ab72230040e708f606a585b7`](https://github.com/medusajs/medusa/commit/8c4228fc42e717f9ab72230040e708f606a585b7) Thanks [@riqwan](https://github.com/riqwan)! - fix(link-modules,core-flows): Carry over cart promotions to order promotions + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/link-modules/package.json b/packages/modules/link-modules/package.json index 0166520152f5a..9d071f82f535c 100644 --- a/packages/modules/link-modules/package.json +++ b/packages/modules/link-modules/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/link-modules", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Default Link Modules Package and Definitions", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -33,8 +33,8 @@ "test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", @@ -47,7 +47,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/link-modules/src/definitions/order-promotion.ts b/packages/modules/link-modules/src/definitions/order-promotion.ts index ee1a6a0388265..dba4139a2ac16 100644 --- a/packages/modules/link-modules/src/definitions/order-promotion.ts +++ b/packages/modules/link-modules/src/definitions/order-promotion.ts @@ -48,6 +48,13 @@ export const OrderPromotion: ModuleJoinerConfig = { path: "promotion_link.promotions", isList: true, }, + /** + * @deprecated use the promotions field alias instead + */ + promotion: { + path: "promotion_link.promotions", + isList: true, + }, }, relationship: { serviceName: LINKS.OrderPromotion, @@ -57,5 +64,15 @@ export const OrderPromotion: ModuleJoinerConfig = { isList: true, }, }, + { + serviceName: Modules.PROMOTION, + entity: "Promotion", + relationship: { + serviceName: LINKS.OrderPromotion, + primaryKey: "promotion_id", + foreignKey: "id", + alias: "order_link", + }, + }, ], } diff --git a/packages/modules/locking/CHANGELOG.md b/packages/modules/locking/CHANGELOG.md index aebec1e469e10..eff40c204ea1f 100644 --- a/packages/modules/locking/CHANGELOG.md +++ b/packages/modules/locking/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/locking +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/locking/package.json b/packages/modules/locking/package.json index 6c5cf356a033e..0cfd6bb26d245 100644 --- a/packages/modules/locking/package.json +++ b/packages/modules/locking/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/locking", - "version": "2.8.7", + "version": "2.8.8", "description": "Locking Module for Medusa", "main": "dist/index.js", "repository": { @@ -32,8 +32,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -46,7 +46,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/locking/src/services/locking-provider.ts b/packages/modules/locking/src/services/locking-provider.ts index 75e972dc703ff..9287a4d8e6f63 100644 --- a/packages/modules/locking/src/services/locking-provider.ts +++ b/packages/modules/locking/src/services/locking-provider.ts @@ -44,6 +44,10 @@ export default class LockingProviderService { const errMessage = ` Unable to retrieve the locking provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/notification/CHANGELOG.md b/packages/modules/notification/CHANGELOG.md index 14497bb483208..6957d57708229 100644 --- a/packages/modules/notification/CHANGELOG.md +++ b/packages/modules/notification/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/notification +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/notification/package.json b/packages/modules/notification/package.json index b9ddd6fae25cf..a5cd0742d10c5 100644 --- a/packages/modules/notification/package.json +++ b/packages/modules/notification/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/notification", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Notification module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/notification/src/services/notification-provider.ts b/packages/modules/notification/src/services/notification-provider.ts index 54ae334856a6a..0a0497b2d6b31 100644 --- a/packages/modules/notification/src/services/notification-provider.ts +++ b/packages/modules/notification/src/services/notification-provider.ts @@ -58,6 +58,10 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI const errMessage = ` Unable to retrieve the notification provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/order/CHANGELOG.md b/packages/modules/order/CHANGELOG.md index d6f154692b089..10bedc42471f6 100644 --- a/packages/modules/order/CHANGELOG.md +++ b/packages/modules/order/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/order +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/order/package.json b/packages/modules/order/package.json index 19cbc999cd8d3..91f8deaa12375 100644 --- a/packages/modules/order/package.json +++ b/packages/modules/order/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/order", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Order module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/payment/CHANGELOG.md b/packages/modules/payment/CHANGELOG.md index aaa8d30dc03da..d46e7fe94f7a9 100644 --- a/packages/modules/payment/CHANGELOG.md +++ b/packages/modules/payment/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/payment +## 2.8.8 + +### Patch Changes + +- [#13004](https://github.com/medusajs/medusa/pull/13004) [`798ac0068ed841d31f6aefc11c58738dc6bf8bd0`](https://github.com/medusajs/medusa/commit/798ac0068ed841d31f6aefc11c58738dc6bf8bd0) Thanks [@carlos-r-l-rodrigues](https://github.com/carlos-r-l-rodrigues)! - chore(payment): remove fixed schema from joiner config + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index a77664747721f..3343e54d09413 100644 --- a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -23,11 +23,13 @@ moduleIntegrationTestRunner({ service: PaymentModuleService, }).linkable - expect(Object.keys(linkable)).toHaveLength(6) + expect(Object.keys(linkable)).toHaveLength(8) expect(Object.keys(linkable)).toEqual([ "paymentCollection", "paymentSession", "payment", + "capture", + "refund", "refundReason", "accountHolder", "paymentProvider", @@ -38,58 +40,76 @@ moduleIntegrationTestRunner({ }) expect(linkable).toEqual({ + paymentCollection: { + id: { + linkable: "payment_collection_id", + primaryKey: "id", + serviceName: "payment", + field: "paymentCollection", + entity: "PaymentCollection", + }, + }, + paymentSession: { + id: { + linkable: "payment_session_id", + primaryKey: "id", + serviceName: "payment", + field: "paymentSession", + entity: "PaymentSession", + }, + }, payment: { id: { linkable: "payment_id", - entity: "Payment", primaryKey: "id", serviceName: "payment", field: "payment", + entity: "Payment", }, }, - paymentCollection: { + capture: { id: { - linkable: "payment_collection_id", - entity: "PaymentCollection", + linkable: "capture_id", primaryKey: "id", serviceName: "payment", - field: "paymentCollection", + field: "capture", + entity: "Capture", }, }, - paymentSession: { + refund: { id: { - field: "paymentSession", - entity: "PaymentSession", - linkable: "payment_session_id", + linkable: "refund_id", primaryKey: "id", serviceName: "payment", + field: "refund", + entity: "Refund", }, }, refundReason: { id: { linkable: "refund_reason_id", - entity: "RefundReason", primaryKey: "id", serviceName: "payment", field: "refundReason", + entity: "RefundReason", }, }, accountHolder: { id: { linkable: "account_holder_id", - entity: "AccountHolder", primaryKey: "id", serviceName: "payment", field: "accountHolder", + entity: "AccountHolder", }, }, paymentProvider: { id: { linkable: "payment_provider_id", - entity: "PaymentProvider", primaryKey: "id", serviceName: "payment", field: "paymentProvider", + entity: "PaymentProvider", }, }, }) diff --git a/packages/modules/payment/package.json b/packages/modules/payment/package.json index b31d984374bf5..6273b2cd0a5e1 100644 --- a/packages/modules/payment/package.json +++ b/packages/modules/payment/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/payment", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Payment module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/payment/src/joiner-config.ts b/packages/modules/payment/src/joiner-config.ts index 09a4b73f75b3a..6a36eda8edc33 100644 --- a/packages/modules/payment/src/joiner-config.ts +++ b/packages/modules/payment/src/joiner-config.ts @@ -1,24 +1,13 @@ import { defineJoinerConfig, Modules } from "@medusajs/framework/utils" import { + AccountHolder, Payment, PaymentCollection, PaymentProvider, - PaymentSession, RefundReason, - AccountHolder, } from "@models" -import { default as schema } from "./schema" export const joinerConfig = defineJoinerConfig(Modules.PAYMENT, { - schema, - models: [ - Payment, - PaymentCollection, - PaymentProvider, - PaymentSession, - RefundReason, - AccountHolder, - ], linkableKeys: { payment_id: Payment.name, payment_collection_id: PaymentCollection.name, diff --git a/packages/modules/payment/src/schema/index.ts b/packages/modules/payment/src/schema/index.ts deleted file mode 100644 index ba50266f90dae..0000000000000 --- a/packages/modules/payment/src/schema/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -export default ` -enum PaymentCollectionStatus { - not_paid - awaiting - authorized - partially_authorized - canceled -} - -enum PaymentSessionStatus { - authorized - captured - pending - requires_more - error - canceled -} - -type PaymentCollection { - id: ID! - currency_code: String! - region_id: String! - amount: Float! - authorized_amount: Float - refunded_amount: Float - captured_amount: Float - completed_at: DateTime - created_at: DateTime - updated_at: DateTime - metadata: JSON - status: PaymentCollectionStatus! - payment_providers: [PaymentProviderDTO!]! - payment_sessions: [PaymentSessionDTO] - payments: [PaymentDTO] -} - -type Payment { - id: ID! - amount: Float! - raw_amount: Float - authorized_amount: Float - raw_authorized_amount: Float - currency_code: String! - provider_id: String! - cart_id: String - order_id: String - order_edit_id: String - customer_id: String - data: JSON - created_at: DateTime - updated_at: DateTime - captured_at: DateTime - canceled_at: DateTime - captured_amount: Float - raw_captured_amount: Float - refunded_amount: Float - raw_refunded_amount: Float - captures: [CaptureDTO] - refunds: [RefundDTO] - payment_collection_id: String! - payment_collection: PaymentCollectionDTO - payment_session: PaymentSessionDTO -} - -type Capture { - id: ID! - amount: Float! - created_at: DateTime! - created_by: String - payment: Payment! -} - -type Refund { - id: ID! - amount: Float! - refund_reason_id: String - refund_reason: RefundReason - note: String - created_at: DateTime! - created_by: String - payment: Payment! -} - -type PaymentSession { - id: ID! - amount: Float! - currency_code: String! - provider_id: String! - data: JSON! - context: JSON - status: PaymentSessionStatus! - authorized_at: DateTime - payment_collection_id: String! - payment_collection: PaymentCollection - payment: Payment -} - -type PaymentProvider { - id: ID! - is_enabled: String! -} - -type RefundReason { - id: ID! - label: String! - description: String - metadata: JSON - created_at: DateTime! - updated_at: DateTime! -} -` diff --git a/packages/modules/payment/src/services/payment-provider.ts b/packages/modules/payment/src/services/payment-provider.ts index d70416deca66d..48402ddcb9d0e 100644 --- a/packages/modules/payment/src/services/payment-provider.ts +++ b/packages/modules/payment/src/services/payment-provider.ts @@ -60,6 +60,10 @@ export default class PaymentProviderService extends ModulesSdkUtils.MedusaIntern const errMessage = ` Unable to retrieve the payment provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/pricing/CHANGELOG.md b/packages/modules/pricing/CHANGELOG.md index 21b710d622970..625a34928d82b 100644 --- a/packages/modules/pricing/CHANGELOG.md +++ b/packages/modules/pricing/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/pricing +## 2.8.8 + +### Patch Changes + +- [#12981](https://github.com/medusajs/medusa/pull/12981) [`822217fa366d4399d3f6a0407451c9649197d5d9`](https://github.com/medusajs/medusa/commit/822217fa366d4399d3f6a0407451c9649197d5d9) Thanks [@riqwan](https://github.com/riqwan)! - fix(pricing): fix pricing query when max_quantity is null + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index 3b093b49ecf56..8e441a5250140 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -76,6 +76,16 @@ moduleIntegrationTestRunner({ max_quantity: 10, rules_count: 0, }, + { + id: "price-PLN-min-quantity-only", + title: "price PLN - min quantity only", + price_set_id: "price-set-PLN", + currency_code: "PLN", + amount: 1250, + min_quantity: 20, + max_quantity: null, + rules_count: 0, + }, { id: "price-ETH", title: "price ETH", @@ -451,6 +461,54 @@ moduleIntegrationTestRunner({ ]) }) + it("should successfully calculate prices where only min quantity is set", async () => { + const context = { + currency_code: "PLN", + region_id: "PL", + quantity: 255, + } + + const calculatedPrice = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { context } + ) + + expect(calculatedPrice).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: false, + is_calculated_price_tax_inclusive: false, + calculated_amount: 1250, + raw_calculated_amount: { + value: "1250", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 1250, + raw_original_amount: { + value: "1250", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: "price-PLN-min-quantity-only", + price_list_id: null, + price_list_type: null, + min_quantity: 20, + max_quantity: null, + }, + original_price: { + id: "price-PLN-min-quantity-only", + price_list_id: null, + price_list_type: null, + min_quantity: 20, + max_quantity: null, + }, + }, + ]) + }) + it("should throw an error when currency code is not set", async () => { let result = service.calculatePrices( { id: ["price-set-EUR", "price-set-PLN"] }, @@ -1174,11 +1232,27 @@ moduleIntegrationTestRunner({ ]) }) - it("should return best price list price first when price list conditions match", async () => { - await createPriceLists(service) + it("should return cheapest price list price first when price list conditions match", async () => { await createPriceLists( service, + { + title: "Test Price List One", + description: "test description", + type: PriceListType.OVERRIDE, + status: PriceListStatus.ACTIVE, + }, {}, + defaultPriceListPrices + ) + + await createPriceLists( + service, + { + title: "Test Price List Two", + description: "test description", + type: PriceListType.OVERRIDE, + status: PriceListStatus.ACTIVE, + }, {}, defaultPriceListPrices.map((price) => { return { ...price, amount: price.amount / 2 } @@ -1186,7 +1260,7 @@ moduleIntegrationTestRunner({ ) const priceSetsResult = await service.calculatePrices( - { id: ["price-set-EUR", "price-set-PLN"] }, + { id: ["price-set-PLN"] }, { context: { currency_code: "PLN", @@ -1202,32 +1276,32 @@ moduleIntegrationTestRunner({ id: "price-set-PLN", is_calculated_price_price_list: true, is_calculated_price_tax_inclusive: false, - calculated_amount: 232, + calculated_amount: 116, raw_calculated_amount: { - value: "232", + value: "116", precision: 20, }, - is_original_price_price_list: false, + is_original_price_price_list: true, is_original_price_tax_inclusive: false, - original_amount: 400, + original_amount: 116, raw_original_amount: { - value: "400", + value: "116", precision: 20, }, currency_code: "PLN", calculated_price: { id: expect.any(String), price_list_id: expect.any(String), - price_list_type: "sale", + price_list_type: "override", min_quantity: null, max_quantity: null, }, original_price: { id: expect.any(String), - price_list_id: null, - price_list_type: null, - min_quantity: 1, - max_quantity: 5, + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, }, }, ]) @@ -1919,6 +1993,149 @@ moduleIntegrationTestRunner({ ]) }) + it("should return price list prices for multiple price lists with customer groups", async () => { + const [{ id }] = await createPriceLists( + service, + { type: "override" }, + { + ["customer.groups.id"]: ["vip-customer-group-id"], + }, + [ + { + amount: 600, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const [{ id: idTwo }] = await createPriceLists( + service, + { type: "override" }, + { + ["customer.groups.id"]: ["vip-customer-group-id-1"], + }, + [ + { + amount: 400, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR"] }, + { + context: { + currency_code: "EUR", + // @ts-ignore + customer: { + groups: { + id: ["vip-customer-group-id", "vip-customer-group-id-1"], + }, + }, + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 400, + raw_calculated_amount: { + value: "400", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 400, + raw_original_amount: { + value: "400", + precision: 20, + }, + currency_code: "EUR", + calculated_price: { + id: expect.any(String), + price_list_id: idTwo, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: idTwo, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + ]) + }) + + it("should return price list prices when price list conditions match within prices", async () => { + await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [ + ...defaultPriceListPrices, + { + amount: 111, + currency_code: "PLN", + price_set_id: "price-set-PLN", + rules: { + region_id: "DE", + }, + }, + ]) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + region_id: "DE", + customer_group_id: "vip-customer-group-id", + company_id: "medusa-company-id", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 111, + raw_calculated_amount: { + value: "111", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 400, + raw_original_amount: { + value: "400", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 5, + }, + }, + ]) + }) + it("should not return price list prices when price list conditions are met but price rules are not", async () => { await createPriceLists(service, {}, { region_id: ["DE", "PL"] }, [ ...defaultPriceListPrices, diff --git a/packages/modules/pricing/package.json b/packages/modules/pricing/package.json index d7315f1ac8619..b54848f4b7f52 100644 --- a/packages/modules/pricing/package.json +++ b/packages/modules/pricing/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/pricing", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Pricing module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/pricing/src/repositories/pricing.ts b/packages/modules/pricing/src/repositories/pricing.ts index 43bee3505b4e0..2d8ed67005c62 100644 --- a/packages/modules/pricing/src/repositories/pricing.ts +++ b/packages/modules/pricing/src/repositories/pricing.ts @@ -126,14 +126,24 @@ export class PricingRepository if (quantity !== undefined) { query.andWhere(function (this: Knex.QueryBuilder) { - this.where(function (this: Knex.QueryBuilder) { + this.orWhere(function (this: Knex.QueryBuilder) { this.where("price.min_quantity", "<=", quantity).andWhere( "price.max_quantity", ">=", quantity ) - }).orWhere(function (this: Knex.QueryBuilder) { - this.whereNull("price.min_quantity").whereNull("price.max_quantity") + + this.orWhere("price.min_quantity", "<=", quantity).whereNull( + "price.max_quantity" + ) + + this.orWhereNull("price.min_quantity").whereNull("price.max_quantity") + + this.orWhereNull("price.min_quantity").andWhere( + "price.max_quantity", + ">=", + quantity + ) }) }) } else { @@ -170,10 +180,10 @@ export class PricingRepository WHERE pr.price_id = price.id AND pr.deleted_at IS NULL AND ( - ${flattenedContext - .map(([key, value]) => { - if (typeof value === "number") { - return ` + ${flattenedContext + .map(([key, value]) => { + if (typeof value === "number") { + return ` (pr.attribute = ? AND ( (pr.operator = 'eq' AND pr.value = ?) OR (pr.operator = 'gt' AND ? > pr.value::numeric) OR @@ -182,16 +192,13 @@ export class PricingRepository (pr.operator = 'lte' AND ? <= pr.value::numeric) )) ` - } else { - const normalizeValue = Array.isArray(value) - ? value - : [value] - const placeholders = normalizeValue.map(() => "?").join(",") - return `(pr.attribute = ? AND pr.value IN (${placeholders}))` - } - }) - .join(" OR ")} - ) + } else { + const normalizeValue = Array.isArray(value) ? value : [value] + const placeholders = normalizeValue.map(() => "?").join(",") + return `(pr.attribute = ? AND pr.value IN (${placeholders}))` + } + }) + .join(" OR ")}) ) = ( /* Get total rule count */ SELECT COUNT(*) @@ -222,11 +229,16 @@ export class PricingRepository WHERE plr.price_list_id = pl.id AND plr.deleted_at IS NULL AND ( - ${flattenedContext - .map(([key, value]) => { - return `(plr.attribute = ? AND plr.value @> ?)` - }) - .join(" OR ")} + ${flattenedContext + .map(([key, value]) => { + if (Array.isArray(value)) { + return value + .map((v) => `(plr.attribute = ? AND plr.value @> ?)`) + .join(" OR ") + } + return `(plr.attribute = ? AND plr.value @> ?)` + }) + .join(" OR ")} ) ) = ( /* Get total rule count */ @@ -238,7 +250,8 @@ export class PricingRepository ) `, flattenedContext.flatMap(([key, value]) => { - return [key, JSON.stringify(Array.isArray(value) ? value : [value])] + const valueAsArray = Array.isArray(value) ? value : [value] + return valueAsArray.flatMap((v) => [key, JSON.stringify(v)]) }) ) @@ -265,7 +278,6 @@ export class PricingRepository query .orderByRaw("price.price_list_id IS NOT NULL DESC") .orderByRaw("price.rules_count + COALESCE(pl.rules_count, 0) DESC") - .orderBy("pl.id", "asc") .orderBy("price.amount", "asc") return await query diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 47673d4eb6c74..e015e177c7ac6 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -343,6 +343,7 @@ export default class PricingModuleService /** * When deciding which price to use we follow the following logic: * - If the price list is of type OVERRIDE, we always use the price list price. + * - If there are multiple price list prices of type OVERRIDE, we use the one with the lowest amount. * - If the price list is of type SALE, we use the lowest price between the price list price and the default price */ if (priceListPrice) { @@ -369,6 +370,7 @@ export default class PricingModuleService } } + pricesSetPricesMap.set(key, { calculatedPrice, originalPrice }) priceIds.push( ...(deduplicate( diff --git a/packages/modules/product/CHANGELOG.md b/packages/modules/product/CHANGELOG.md index 65627083b4774..8d2128b8cfe26 100644 --- a/packages/modules/product/CHANGELOG.md +++ b/packages/modules/product/CHANGELOG.md @@ -1,5 +1,14 @@ # @medusajs/product +## 2.8.8 + +### Patch Changes + +- [#13019](https://github.com/medusajs/medusa/pull/13019) [`439c7118450c5f9ee0b541de9014093a42b7d0ea`](https://github.com/medusajs/medusa/commit/439c7118450c5f9ee0b541de9014093a42b7d0ea) Thanks [@fPolic](https://github.com/fPolic)! - fix(dashboard, product): update product attributes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/product/package.json b/packages/modules/product/package.json index 07f960338c87e..83ff990eb6345 100644 --- a/packages/modules/product/package.json +++ b/packages/modules/product/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/product", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Product module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -51,7 +51,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/product/src/repositories/product.ts b/packages/modules/product/src/repositories/product.ts index fc1e4ee2204de..3001ef1271b27 100644 --- a/packages/modules/product/src/repositories/product.ts +++ b/packages/modules/product/src/repositories/product.ts @@ -8,6 +8,7 @@ import { MedusaError, isPresent, mergeMetadata, + isDefined, } from "@medusajs/framework/utils" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql" @@ -57,10 +58,18 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( height?: string | number width?: string | number }) { - productToUpdate.weight = productToUpdate.weight?.toString() - productToUpdate.length = productToUpdate.length?.toString() - productToUpdate.height = productToUpdate.height?.toString() - productToUpdate.width = productToUpdate.width?.toString() + if (isDefined(productToUpdate.weight)) { + productToUpdate.weight = productToUpdate.weight?.toString() + } + if (isDefined(productToUpdate.length)) { + productToUpdate.length = productToUpdate.length?.toString() + } + if (isDefined(productToUpdate.height)) { + productToUpdate.height = productToUpdate.height?.toString() + } + if (isDefined(productToUpdate.width)) { + productToUpdate.width = productToUpdate.width?.toString() + } } async deepUpdate( @@ -72,6 +81,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( context: Context = {} ): Promise[]> { const productIdsToUpdate: string[] = [] + productsToUpdate.forEach((productToUpdate) => { ProductRepository.#correctUpdateDTOTypes(productToUpdate) productIdsToUpdate.push(productToUpdate.id) @@ -151,7 +161,10 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( } if (isPresent(productToUpdate.metadata)) { - productToUpdate.metadata = mergeMetadata(product.metadata ?? {}, productToUpdate.metadata) + productToUpdate.metadata = mergeMetadata( + product.metadata ?? {}, + productToUpdate.metadata + ) } wrappedProduct.assign(productToUpdate) diff --git a/packages/modules/promotion/CHANGELOG.md b/packages/modules/promotion/CHANGELOG.md index 6fb619661c15a..c4099f4806556 100644 --- a/packages/modules/promotion/CHANGELOG.md +++ b/packages/modules/promotion/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/promotion +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/promotion/integration-tests/__fixtures__/campaigns/data.ts b/packages/modules/promotion/integration-tests/__fixtures__/campaigns/data.ts index af31928cc537f..e1fc2b02e7bf8 100644 --- a/packages/modules/promotion/integration-tests/__fixtures__/campaigns/data.ts +++ b/packages/modules/promotion/integration-tests/__fixtures__/campaigns/data.ts @@ -11,7 +11,7 @@ export const defaultCampaignsData = [ budget: { type: CampaignBudgetType.SPEND, limit: 1000, - currency_code: "USD", + currency_code: "usd", used: 0, }, }, @@ -25,7 +25,7 @@ export const defaultCampaignsData = [ budget: { type: CampaignBudgetType.USAGE, limit: 1000, - currency_code: "USD", + currency_code: "usd", used: 0, }, }, diff --git a/packages/modules/promotion/integration-tests/__fixtures__/promotion/data.ts b/packages/modules/promotion/integration-tests/__fixtures__/promotion/data.ts index 61822491dd0dd..d01dab227efd3 100644 --- a/packages/modules/promotion/integration-tests/__fixtures__/promotion/data.ts +++ b/packages/modules/promotion/integration-tests/__fixtures__/promotion/data.ts @@ -7,7 +7,7 @@ export const defaultPromotionsData: CreatePromotionDTO[] = [ code: "PROMOTION_1", type: PromotionType.STANDARD, application_method: { - currency_code: "USD", + currency_code: "usd", target_type: "items", type: "fixed", allocation: "across", @@ -19,7 +19,7 @@ export const defaultPromotionsData: CreatePromotionDTO[] = [ code: "PROMOTION_2", type: PromotionType.STANDARD, application_method: { - currency_code: "USD", + currency_code: "usd", target_type: "items", type: "fixed", allocation: "across", diff --git a/packages/modules/promotion/integration-tests/__fixtures__/promotion/index.ts b/packages/modules/promotion/integration-tests/__fixtures__/promotion/index.ts index 66b8a80545dea..71825eb823a4e 100644 --- a/packages/modules/promotion/integration-tests/__fixtures__/promotion/index.ts +++ b/packages/modules/promotion/integration-tests/__fixtures__/promotion/index.ts @@ -59,7 +59,7 @@ export async function createDefaultPromotion( campaign_id: "campaign-id-1", ...promotion, application_method: { - currency_code: "USD", + currency_code: "usd", target_type: "items", type: "fixed", allocation: "across", diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index 0c084d2c4bd53..8cb172034cb39 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -2694,6 +2694,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -2743,6 +2744,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -2816,6 +2818,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions([], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -2891,6 +2894,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( [], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -2980,6 +2984,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3092,6 +3097,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3177,6 +3183,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3230,6 +3237,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3280,6 +3288,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3352,6 +3361,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions([], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3449,6 +3459,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3559,6 +3570,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3631,6 +3643,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3683,6 +3696,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3731,6 +3745,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3803,6 +3818,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions([], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -3900,6 +3916,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4010,6 +4027,7 @@ moduleIntegrationTestRunner({ const result = await service.computeActions( ["PROMOTION_TEST", "PROMOTION_TEST_2"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4088,6 +4106,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4140,6 +4159,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4183,6 +4203,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4252,6 +4273,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions([], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4260,6 +4282,7 @@ moduleIntegrationTestRunner({ items: [ { id: "item_cotton_tshirt", + is_discountable: true, quantity: 1, subtotal: 100, product_category: { @@ -4271,6 +4294,7 @@ moduleIntegrationTestRunner({ }, { id: "item_cotton_sweater", + is_discountable: true, quantity: 2, subtotal: 300, product_category: { @@ -4351,6 +4375,7 @@ moduleIntegrationTestRunner({ id: "item_cotton_tshirt", quantity: 1, subtotal: 50, + is_discountable: true, product_category: { id: "catg_cotton", }, @@ -4362,6 +4387,7 @@ moduleIntegrationTestRunner({ id: "item_cotton_sweater", quantity: 1, subtotal: 150, + is_discountable: true, product_category: { id: "catg_cotton", }, @@ -4461,6 +4487,7 @@ moduleIntegrationTestRunner({ product: { id: "prod_tshirt", }, + is_discountable: true, }, { id: "item_cotton_sweater", @@ -4472,6 +4499,7 @@ moduleIntegrationTestRunner({ product: { id: "prod_sweater", }, + is_discountable: true, }, ], } @@ -4527,6 +4555,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4537,6 +4566,7 @@ moduleIntegrationTestRunner({ id: "item_cotton_tshirt", quantity: 1, subtotal: 100, + is_discountable: true, product_category: { id: "catg_cotton", }, @@ -4554,6 +4584,7 @@ moduleIntegrationTestRunner({ id: "item_cotton_sweater", quantity: 5, subtotal: 750, + is_discountable: true, product_category: { id: "catg_cotton", }, @@ -4617,6 +4648,7 @@ moduleIntegrationTestRunner({ }) const result = await service.computeActions(["PROMOTION_TEST"], { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4678,6 +4710,7 @@ moduleIntegrationTestRunner({ describe("when promotion of type buyget", () => { it("should compute adjustment when target and buy rules match", async () => { const context = { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4771,6 +4804,7 @@ moduleIntegrationTestRunner({ it("should return empty array when conditions for minimum qty aren't met", async () => { const context = { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4857,6 +4891,7 @@ moduleIntegrationTestRunner({ it("should compute actions for multiple items when conditions for target qty exceed one item", async () => { const context = { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -4957,6 +4992,7 @@ moduleIntegrationTestRunner({ it("should return empty array when target rules arent met with context", async () => { const context = { + currency_code: "usd", customer: { customer_group: { id: "VIP", @@ -5332,24 +5368,28 @@ moduleIntegrationTestRunner({ quantity: 1, subtotal: 500, product: { id: product1 }, + is_discountable: true, }, { id: "item_cotton_tshirt1", quantity: 1, subtotal: 500, product: { id: product1 }, + is_discountable: true, }, { id: "item_cotton_tshirt2", quantity: 1, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, { id: "item_cotton_tshirt3", quantity: 1, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, ], } @@ -5384,6 +5424,7 @@ moduleIntegrationTestRunner({ quantity: 3, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, ], } @@ -5405,18 +5446,21 @@ moduleIntegrationTestRunner({ quantity: 1, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, { id: "item_cotton_tshirt1", quantity: 1, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, { id: "item_cotton_tshirt2", quantity: 1, subtotal: 1000, product: { id: product1 }, + is_discountable: true, }, ], } diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/evaluate-rule-value-condition.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/evaluate-rule-value-condition.spec.ts index 760fda29ffd2d..1a3a4a138b9f1 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/evaluate-rule-value-condition.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/evaluate-rule-value-condition.spec.ts @@ -18,6 +18,16 @@ moduleIntegrationTestRunner({ }) }) + describe("in", () => { + const operator = "in" + + it("should evaluate conditions accurately", async () => { + expect(testFunc(["1", "2"], operator, [2])).toEqual(true) + expect(testFunc(["2"], operator, ["2"])).toEqual(true) + expect(testFunc(["2"], operator, ["22"])).toEqual(false) + }) + }) + describe("ne", () => { const operator = "ne" diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 763cab0cdbc4c..a95577e055fb6 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -154,7 +154,7 @@ moduleIntegrationTestRunner({ ends_at: endsAt, budget: { type: CampaignBudgetType.SPEND, - currency_code: "USD", + currency_code: "usd", used: 100, limit: 100, }, @@ -179,7 +179,7 @@ moduleIntegrationTestRunner({ ends_at: endsAt, budget: expect.objectContaining({ type: CampaignBudgetType.SPEND, - currency_code: "USD", + currency_code: "usd", used: 100, limit: 100, }), diff --git a/packages/modules/promotion/package.json b/packages/modules/promotion/package.json index 45258482574a1..5111735f78001 100644 --- a/packages/modules/promotion/package.json +++ b/packages/modules/promotion/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/promotion", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Promotion module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/promotion/src/services/promotion-module.ts b/packages/modules/promotion/src/services/promotion-module.ts index 66a81a37f4555..7c17ae3b084ba 100644 --- a/packages/modules/promotion/src/services/promotion-module.ts +++ b/packages/modules/promotion/src/services/promotion-module.ts @@ -520,13 +520,17 @@ export default class PromotionModuleService continue } + const isCurrencyCodeValid = + !isDefined(applicationMethod.currency_code) || + applicationContext.currency_code === applicationMethod.currency_code + const isPromotionApplicable = areRulesValidForContext( promotionRules, applicationContext, ApplicationMethodTargetType.ORDER ) - if (!isPromotionApplicable) { + if (!isPromotionApplicable || !isCurrencyCodeValid) { continue } diff --git a/packages/modules/promotion/src/utils/compute-actions/line-items.ts b/packages/modules/promotion/src/utils/compute-actions/line-items.ts index 9480d780a1f35..1c1dcc89db619 100644 --- a/packages/modules/promotion/src/utils/compute-actions/line-items.ts +++ b/packages/modules/promotion/src/utils/compute-actions/line-items.ts @@ -103,24 +103,32 @@ function applyPromotionToItems( ? 1 : applicationMethod?.max_quantity! - let lineItemsTotal = MathBN.convert(0) + let lineItemsAmount = MathBN.convert(0) if (allocation === ApplicationMethodAllocation.ACROSS) { - lineItemsTotal = applicableItems.reduce( + lineItemsAmount = applicableItems.reduce( (acc, item) => MathBN.sub( - MathBN.add(acc, item.subtotal), + MathBN.add( + acc, + promotion.is_tax_inclusive ? item.original_total : item.subtotal + ), appliedPromotionsMap.get(item.id) ?? 0 ), MathBN.convert(0) ) - if (MathBN.lte(lineItemsTotal, 0)) { + if (MathBN.lte(lineItemsAmount, 0)) { return computedActions } } for (const item of applicableItems) { - if (MathBN.lte(item.subtotal, 0)) { + if ( + MathBN.lte( + promotion.is_tax_inclusive ? item.original_total : item.subtotal, + 0 + ) + ) { continue } @@ -135,11 +143,12 @@ function applyPromotionToItems( { value: promotionValue, applied_value: appliedPromoValue, + is_tax_inclusive: promotion.is_tax_inclusive, max_quantity: maxQuantity, type: applicationMethod?.type!, allocation, }, - lineItemsTotal + lineItemsAmount ) if (MathBN.lte(amount, 0)) { diff --git a/packages/modules/promotion/src/utils/validations/promotion-rule.ts b/packages/modules/promotion/src/utils/validations/promotion-rule.ts index dd6716c9c93d7..a71a5141af122 100644 --- a/packages/modules/promotion/src/utils/validations/promotion-rule.ts +++ b/packages/modules/promotion/src/utils/validations/promotion-rule.ts @@ -117,11 +117,14 @@ export function evaluateRuleValueCondition( } switch (operator) { - case "in": case "eq": { const ruleValueSet = new Set(ruleValues) return valuesToCheck.every((val) => ruleValueSet.has(`${val}`)) } + case "in": { + const ruleValueSet = new Set(ruleValues) + return valuesToCheck.some((val) => ruleValueSet.has(`${val}`)) + } case "ne": { const ruleValueSet = new Set(ruleValues) return valuesToCheck.every((val) => !ruleValueSet.has(`${val}`)) diff --git a/packages/modules/providers/analytics-local/CHANGELOG.md b/packages/modules/providers/analytics-local/CHANGELOG.md index 46bd85025d92e..f3db37ff069b3 100644 --- a/packages/modules/providers/analytics-local/CHANGELOG.md +++ b/packages/modules/providers/analytics-local/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/analytics-local +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/analytics-local/package.json b/packages/modules/providers/analytics-local/package.json index 0b7a62bbbe984..915dbed6debb7 100644 --- a/packages/modules/providers/analytics-local/package.json +++ b/packages/modules/providers/analytics-local/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/analytics-local", - "version": "2.8.7", + "version": "2.8.8", "description": "Local analytics provider for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -33,7 +33,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-plugin", diff --git a/packages/modules/providers/analytics-posthog/CHANGELOG.md b/packages/modules/providers/analytics-posthog/CHANGELOG.md index 4b878519c56d2..507350e825087 100644 --- a/packages/modules/providers/analytics-posthog/CHANGELOG.md +++ b/packages/modules/providers/analytics-posthog/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/analytics-posthog +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/analytics-posthog/package.json b/packages/modules/providers/analytics-posthog/package.json index 06294c0085fb0..ee2ae55b35119 100644 --- a/packages/modules/providers/analytics-posthog/package.json +++ b/packages/modules/providers/analytics-posthog/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/analytics-posthog", - "version": "2.8.7", + "version": "2.8.8", "description": "Posthog analytics provider for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -34,7 +34,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "posthog-node": "^4.17.1" }, "keywords": [ diff --git a/packages/modules/providers/auth-emailpass/CHANGELOG.md b/packages/modules/providers/auth-emailpass/CHANGELOG.md index ba652fb8350ef..e22c1a4fb0af0 100644 --- a/packages/modules/providers/auth-emailpass/CHANGELOG.md +++ b/packages/modules/providers/auth-emailpass/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/auth-emailpass +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/auth-emailpass/package.json b/packages/modules/providers/auth-emailpass/package.json index 95e0e19edc397..ca92694690683 100644 --- a/packages/modules/providers/auth-emailpass/package.json +++ b/packages/modules/providers/auth-emailpass/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/auth-emailpass", - "version": "2.8.7", + "version": "2.8.8", "description": "Email and password credential authentication provider for Medusa", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -37,7 +37,7 @@ "scrypt-kdf": "^2.0.1" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-provider", diff --git a/packages/modules/providers/auth-github/CHANGELOG.md b/packages/modules/providers/auth-github/CHANGELOG.md index 595cb8e3182eb..e5dc24f098e8d 100644 --- a/packages/modules/providers/auth-github/CHANGELOG.md +++ b/packages/modules/providers/auth-github/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/auth-github +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/auth-github/package.json b/packages/modules/providers/auth-github/package.json index 8513ce64d61ab..146f9e8a660be 100644 --- a/packages/modules/providers/auth-github/package.json +++ b/packages/modules/providers/auth-github/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/auth-github", - "version": "2.8.7", + "version": "2.8.8", "description": "Github OAuth authentication provider for Medusa", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -35,7 +35,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-provider", diff --git a/packages/modules/providers/auth-google/CHANGELOG.md b/packages/modules/providers/auth-google/CHANGELOG.md index ac4fa85688809..8254b67f4f41b 100644 --- a/packages/modules/providers/auth-google/CHANGELOG.md +++ b/packages/modules/providers/auth-google/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/auth-google +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/auth-google/package.json b/packages/modules/providers/auth-google/package.json index 2bf0b5462446e..ff31ce22abc52 100644 --- a/packages/modules/providers/auth-google/package.json +++ b/packages/modules/providers/auth-google/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/auth-google", - "version": "2.8.7", + "version": "2.8.8", "description": "Google OAuth authentication provider for Medusa", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -38,7 +38,7 @@ "jsonwebtoken": "^9.0.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-provider", diff --git a/packages/modules/providers/file-local/CHANGELOG.md b/packages/modules/providers/file-local/CHANGELOG.md index 6a6b309764655..c3f88e929b73b 100644 --- a/packages/modules/providers/file-local/CHANGELOG.md +++ b/packages/modules/providers/file-local/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/file-local +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/file-local/package.json b/packages/modules/providers/file-local/package.json index f1b0915f5a990..37781c5b21b5c 100644 --- a/packages/modules/providers/file-local/package.json +++ b/packages/modules/providers/file-local/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/file-local", - "version": "2.8.7", + "version": "2.8.8", "description": "Local filesystem file storage for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -33,7 +33,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-plugin", diff --git a/packages/modules/providers/file-s3/CHANGELOG.md b/packages/modules/providers/file-s3/CHANGELOG.md index 8e118ee38e446..d65ac4fe8278c 100644 --- a/packages/modules/providers/file-s3/CHANGELOG.md +++ b/packages/modules/providers/file-s3/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/file-s3 +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/file-s3/package.json b/packages/modules/providers/file-s3/package.json index 1720146a6914d..0564032ea093f 100644 --- a/packages/modules/providers/file-s3/package.json +++ b/packages/modules/providers/file-s3/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/file-s3", - "version": "2.8.7", + "version": "2.8.8", "description": "S3 protocol file storage for Medusa. Supports any S3-compatible storage provider", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "axios": "^1.6.8", @@ -40,7 +40,7 @@ "ulid": "^2.3.0" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-plugin", diff --git a/packages/modules/providers/fulfillment-manual/CHANGELOG.md b/packages/modules/providers/fulfillment-manual/CHANGELOG.md index e5e444856f1d1..053f46b0b941c 100644 --- a/packages/modules/providers/fulfillment-manual/CHANGELOG.md +++ b/packages/modules/providers/fulfillment-manual/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/fulfillment-manual +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/fulfillment-manual/package.json b/packages/modules/providers/fulfillment-manual/package.json index 9abb10f25f78c..deb305573b10e 100644 --- a/packages/modules/providers/fulfillment-manual/package.json +++ b/packages/modules/providers/fulfillment-manual/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/fulfillment-manual", - "version": "2.8.7", + "version": "2.8.8", "description": "Manual fulfillment for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -33,7 +33,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-plugin", diff --git a/packages/modules/providers/locking-postgres/CHANGELOG.md b/packages/modules/providers/locking-postgres/CHANGELOG.md index fe97f78157233..72651cacb9f61 100644 --- a/packages/modules/providers/locking-postgres/CHANGELOG.md +++ b/packages/modules/providers/locking-postgres/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/locking-postgres +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/locking-postgres/package.json b/packages/modules/providers/locking-postgres/package.json index d93fb0d832204..c86ad03fa56cd 100644 --- a/packages/modules/providers/locking-postgres/package.json +++ b/packages/modules/providers/locking-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/locking-postgres", - "version": "2.8.7", + "version": "2.8.8", "description": "Postgres Advisory Locks for Medusa", "main": "dist/index.js", "repository": { @@ -20,7 +20,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -32,7 +32,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "scripts": { "watch": "tsc --build --watch", diff --git a/packages/modules/providers/locking-redis/CHANGELOG.md b/packages/modules/providers/locking-redis/CHANGELOG.md index d1cd5b817e597..8468a21009ff6 100644 --- a/packages/modules/providers/locking-redis/CHANGELOG.md +++ b/packages/modules/providers/locking-redis/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/locking-redis +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/locking-redis/package.json b/packages/modules/providers/locking-redis/package.json index fc6968d8d6a79..106f40478e006 100644 --- a/packages/modules/providers/locking-redis/package.json +++ b/packages/modules/providers/locking-redis/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/locking-redis", - "version": "2.8.7", + "version": "2.8.8", "description": "Redis Lock for Medusa", "main": "dist/index.js", "repository": { @@ -20,7 +20,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -28,7 +28,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "dependencies": { "ioredis": "^5.4.1" diff --git a/packages/modules/providers/notification-local/CHANGELOG.md b/packages/modules/providers/notification-local/CHANGELOG.md index 3f5d607026c66..854f1e73c87fd 100644 --- a/packages/modules/providers/notification-local/CHANGELOG.md +++ b/packages/modules/providers/notification-local/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/notification-local +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/notification-local/package.json b/packages/modules/providers/notification-local/package.json index 3ad70903e08ac..2110ac2429e61 100644 --- a/packages/modules/providers/notification-local/package.json +++ b/packages/modules/providers/notification-local/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/notification-local", - "version": "2.8.7", + "version": "2.8.8", "description": "Local (logging) notification provider for Medusa, useful for testing purposes and log audits", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -34,7 +34,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-provider", diff --git a/packages/modules/providers/notification-sendgrid/CHANGELOG.md b/packages/modules/providers/notification-sendgrid/CHANGELOG.md index 83564b0488c44..0a5e7f4a604ec 100644 --- a/packages/modules/providers/notification-sendgrid/CHANGELOG.md +++ b/packages/modules/providers/notification-sendgrid/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/notification-sendgrid +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/notification-sendgrid/package.json b/packages/modules/providers/notification-sendgrid/package.json index ff1a99d6cfb54..01d60dbdb1137 100644 --- a/packages/modules/providers/notification-sendgrid/package.json +++ b/packages/modules/providers/notification-sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/notification-sendgrid", - "version": "2.8.7", + "version": "2.8.8", "description": "Sendgrid notification provider for Medusa", "main": "dist/index.js", "repository": { @@ -26,7 +26,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -37,7 +37,7 @@ "@sendgrid/mail": "^8.1.3" }, "peerDependencies": { - "@medusajs/framework": "2.8.7" + "@medusajs/framework": "2.8.8" }, "keywords": [ "medusa-provider", diff --git a/packages/modules/providers/payment-stripe/CHANGELOG.md b/packages/modules/providers/payment-stripe/CHANGELOG.md index 51a68db9d6452..8d79575ac8e2f 100644 --- a/packages/modules/providers/payment-stripe/CHANGELOG.md +++ b/packages/modules/providers/payment-stripe/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/payment-stripe +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/providers/payment-stripe/package.json b/packages/modules/providers/payment-stripe/package.json index 1d240585fe73c..c96976b669a59 100644 --- a/packages/modules/providers/payment-stripe/package.json +++ b/packages/modules/providers/payment-stripe/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/payment-stripe", - "version": "2.8.7", + "version": "2.8.8", "description": "Stripe payment provider for Medusa", "main": "dist/index.js", "repository": { @@ -25,7 +25,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", "jest": "^29.7.0", @@ -33,7 +33,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "awilix": "^8.0.1" }, "dependencies": { diff --git a/packages/modules/region/CHANGELOG.md b/packages/modules/region/CHANGELOG.md index 0748e3efa9313..5bc74a98dda3f 100644 --- a/packages/modules/region/CHANGELOG.md +++ b/packages/modules/region/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/region +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/region/package.json b/packages/modules/region/package.json index caea055be6d72..d85062377f6e7 100644 --- a/packages/modules/region/package.json +++ b/packages/modules/region/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/region", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Region module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/sales-channel/CHANGELOG.md b/packages/modules/sales-channel/CHANGELOG.md index c4270e797fb1f..d75305feac12b 100644 --- a/packages/modules/sales-channel/CHANGELOG.md +++ b/packages/modules/sales-channel/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/sales-channel +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/sales-channel/package.json b/packages/modules/sales-channel/package.json index 51370ad4a251a..42c971576d034 100644 --- a/packages/modules/sales-channel/package.json +++ b/packages/modules/sales-channel/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/sales-channel", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Sales Channel module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/stock-location/CHANGELOG.md b/packages/modules/stock-location/CHANGELOG.md index 874edff4451cf..5fa70d6da24ba 100644 --- a/packages/modules/stock-location/CHANGELOG.md +++ b/packages/modules/stock-location/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/stock-location +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/stock-location/package.json b/packages/modules/stock-location/package.json index dfff7c966920a..cf12861922019 100644 --- a/packages/modules/stock-location/package.json +++ b/packages/modules/stock-location/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/stock-location", - "version": "2.8.7", + "version": "2.8.8", "description": "Stock Location Module for Medusa", "main": "dist/index.js", "repository": { @@ -23,8 +23,8 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -37,7 +37,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/store/CHANGELOG.md b/packages/modules/store/CHANGELOG.md index 804b7477ca843..75c0a8085a80c 100644 --- a/packages/modules/store/CHANGELOG.md +++ b/packages/modules/store/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/store +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/store/package.json b/packages/modules/store/package.json index e56eaa8cb89af..813dbee5e344b 100644 --- a/packages/modules/store/package.json +++ b/packages/modules/store/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/store", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Store module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/tax/CHANGELOG.md b/packages/modules/tax/CHANGELOG.md index 941ad9bb9bb88..c36ca55694aaf 100644 --- a/packages/modules/tax/CHANGELOG.md +++ b/packages/modules/tax/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/tax +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/tax/package.json b/packages/modules/tax/package.json index f9d8af7f8c279..079efd41be92e 100644 --- a/packages/modules/tax/package.json +++ b/packages/modules/tax/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/tax", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Tax module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -50,7 +50,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/tax/src/services/tax-provider.ts b/packages/modules/tax/src/services/tax-provider.ts index 51deb0624a830..cbc38f1f92eb8 100644 --- a/packages/modules/tax/src/services/tax-provider.ts +++ b/packages/modules/tax/src/services/tax-provider.ts @@ -29,6 +29,10 @@ export default class TaxProviderService extends ModulesSdkUtils.MedusaInternalSe const errMessage = ` Unable to retrieve the tax provider with id: ${providerId} Please make sure that the provider is registered in the container and it is configured correctly in your project configuration file.` + + // Log full error for debugging + this.#logger.error(`AwilixResolutionError: ${err.message}`, err) + throw new Error(errMessage) } diff --git a/packages/modules/user/CHANGELOG.md b/packages/modules/user/CHANGELOG.md index af58317fdbb73..ca005f807a9a9 100644 --- a/packages/modules/user/CHANGELOG.md +++ b/packages/modules/user/CHANGELOG.md @@ -1,5 +1,12 @@ # @medusajs/user +## 2.8.8 + +### Patch Changes + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/user/package.json b/packages/modules/user/package.json index 5a9f6a5fec6fb..dd611f349966c 100644 --- a/packages/modules/user/package.json +++ b/packages/modules/user/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/user", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Users module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -53,7 +53,7 @@ "jsonwebtoken": "^9.0.2" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/workflow-engine-inmemory/CHANGELOG.md b/packages/modules/workflow-engine-inmemory/CHANGELOG.md index 13b734e426eb2..78e643ed7cbf9 100644 --- a/packages/modules/workflow-engine-inmemory/CHANGELOG.md +++ b/packages/modules/workflow-engine-inmemory/CHANGELOG.md @@ -1,5 +1,18 @@ # @medusajs/workflow-engine-inmemory +## 2.8.8 + +### Patch Changes + +- [#12987](https://github.com/medusajs/medusa/pull/12987) [`7b1debfe12fc096f7b4e20f13bdeb925c96085c1`](https://github.com/medusajs/medusa/commit/7b1debfe12fc096f7b4e20f13bdeb925c96085c1) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(wfe): add missing state in inmemory notify on run finished + +- [#12976](https://github.com/medusajs/medusa/pull/12976) [`eb83954f23077c0714125b6f2f19fd0ef0f288f9`](https://github.com/medusajs/medusa/commit/eb83954f23077c0714125b6f2f19fd0ef0f288f9) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(workflow-engine-\*): Align event subscribers management + +- [#12982](https://github.com/medusajs/medusa/pull/12982) [`238e7d53c13a1c033886d7c33254919f8b5b22dc`](https://github.com/medusajs/medusa/commit/238e7d53c13a1c033886d7c33254919f8b5b22dc) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(wfe): should notify when finished + add state info + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts index 04635d6d64bc1..7f581c011a80a 100644 --- a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts @@ -3,7 +3,8 @@ export * from "./workflow_2" export * from "./workflow_async" export * from "./workflow_conditional_step" export * from "./workflow_idempotent" +export * from "./workflow_not_idempotent_with_retention" +export * from "./workflow_parallel_async" export * from "./workflow_step_timeout" export * from "./workflow_sync" export * from "./workflow_transaction_timeout" -export * from "./workflow_not_idempotent_with_retention" diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_parallel_async.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_parallel_async.ts new file mode 100644 index 0000000000000..0658ba939a14b --- /dev/null +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_parallel_async.ts @@ -0,0 +1,76 @@ +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + parallelize, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step_2 = createStep( + { + name: "step_2", + async: true, + }, + async (_, { container }) => { + const we = container.resolve(Modules.WORKFLOW_ENGINE) + + await we.run("workflow_sub_workflow", { + throwOnError: true, + }) + } +) + +const parallelStep2Invoke = jest.fn(() => { + throw new Error("Error in parallel step") +}) +const step_2_sub = createStep( + { + name: "step_2", + async: true, + }, + parallelStep2Invoke +) + +const subFlow = createWorkflow( + { + name: "workflow_sub_workflow", + retentionTime: 1000, + }, + function (input) { + step_2_sub() + } +) + +const step_1 = createStep( + { + name: "step_1", + async: true, + }, + jest.fn(() => { + return new StepResponse("step_1") + }) +) + +const parallelStep3Invoke = jest.fn(() => { + return new StepResponse({ + done: true, + }) +}) + +const step_3 = createStep( + { + name: "step_3", + async: true, + }, + parallelStep3Invoke +) + +createWorkflow( + { + name: "workflow_parallel_async", + retentionTime: 1000, + }, + function (input) { + parallelize(step_1(), step_2(), step_3()) + } +) diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts index 20e7094fd484f..f1f9aa8ef45f3 100644 --- a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts @@ -1,3 +1,4 @@ +import { MedusaContainer } from "@medusajs/framework" import { DistributedTransactionType, TransactionState, @@ -43,7 +44,6 @@ import { workflowEventGroupIdStep2Mock, } from "../__fixtures__/workflow_event_group_id" import { createScheduled } from "../__fixtures__/workflow_scheduled" -import { container, MedusaContainer } from "@medusajs/framework" jest.setTimeout(60000) @@ -143,7 +143,7 @@ moduleIntegrationTestRunner({ }) describe("Cancel transaction", function () { - it("should cancel an ongoing execution with async unfinished yet step", async () => { + it("should cancel an ongoing execution with async unfinished yet step", (done) => { const transactionId = "transaction-to-cancel-id" const step1 = createStep("step1", async () => { return new StepResponse("step1") @@ -168,25 +168,39 @@ moduleIntegrationTestRunner({ return new WorkflowResponse("finished") }) - await workflowOrcModule.run(workflowId, { - input: {}, - transactionId, - }) - - await setTimeoutPromise(100) - - await workflowOrcModule.cancel(workflowId, { - transactionId, - }) - - await setTimeoutPromise(1000) - - const execution = await workflowOrcModule.listWorkflowExecutions({ - transaction_id: transactionId, - }) + workflowOrcModule + .run(workflowId, { + input: {}, + transactionId, + }) + .then(async () => { + await setTimeoutPromise(100) + + await workflowOrcModule.cancel(workflowId, { + transactionId, + }) + + workflowOrcModule.subscribe({ + workflowId, + transactionId, + subscriber: async (event) => { + if (event.eventType === "onFinish") { + const execution = + await workflowOrcModule.listWorkflowExecutions({ + transaction_id: transactionId, + }) + + expect(execution.length).toEqual(1) + expect(execution[0].state).toEqual( + TransactionState.REVERTED + ) + done() + } + }, + }) + }) - expect(execution.length).toEqual(1) - expect(execution[0].state).toEqual(TransactionState.REVERTED) + failTrap(done) }) it("should cancel a complete execution with a sync workflow running as async", async () => { @@ -898,7 +912,6 @@ moduleIntegrationTestRunner({ expect(spy).toHaveBeenCalledTimes(1) - console.log(spy.mock.results) expect(spy).toHaveReturnedWith( expect.objectContaining({ output: { testValue: "test" } }) ) @@ -944,6 +957,35 @@ moduleIntegrationTestRunner({ expect(executionsList).toHaveLength(1) expect(executionsListAfter).toHaveLength(1) }) + + it("should display error when multple async steps are running in parallel", (done) => { + void workflowOrcModule.run("workflow_parallel_async", { + input: {}, + throwOnError: false, + }) + + void workflowOrcModule.subscribe({ + workflowId: "workflow_parallel_async", + subscriber: (event) => { + if (event.eventType === "onFinish") { + done() + expect(event.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: "step_2", + handlerType: "invoke", + error: expect.objectContaining({ + message: "Error in parallel step", + }), + }), + ]) + ) + } + }, + }) + + failTrap(done) + }) }) describe("Cleaner job", function () { diff --git a/packages/modules/workflow-engine-inmemory/package.json b/packages/modules/workflow-engine-inmemory/package.json index 683eb371ea607..a20cb4353b132 100644 --- a/packages/modules/workflow-engine-inmemory/package.json +++ b/packages/modules/workflow-engine-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/workflow-engine-inmemory", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Workflow Orchestrator module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -54,7 +54,7 @@ "ulid": "^2.3.0" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts b/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts index b8455cad5cb0b..bbf3ca1565267 100644 --- a/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts +++ b/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts @@ -51,6 +51,7 @@ type NotifyOptions = { eventType: keyof DistributedTransactionEvents workflowId: string transactionId?: string + state?: TransactionState step?: TransactionStep response?: unknown result?: unknown @@ -212,6 +213,7 @@ export class WorkflowOrchestratorService { eventType: "onFinish", workflowId, transactionId: context.transactionId, + state: ret.transaction.getFlow().state as TransactionState, result, errors, }) @@ -269,9 +271,6 @@ export class WorkflowOrchestratorService { throw new Error(`Workflow with id "${workflowId}" not found.`) } - const originalOnFinishHandler = events.onFinish! - delete events.onFinish - const transaction = await this.getRunningTransaction( workflowId, transactionId, @@ -307,12 +306,11 @@ export class WorkflowOrchestratorService { const metadata = ret.transaction.getFlow().metadata const { parentStepIdempotencyKey } = metadata ?? {} - const hasFailed = [TransactionState.FAILED].includes( - ret.transaction.getFlow().state - ) + const transactionState = ret.transaction.getFlow().state + const hasFailed = [TransactionState.FAILED].includes(transactionState) const acknowledgement = { - transactionId: context.transactionId, + transactionId: transaction.transactionId, workflowId: workflowId, parentStepIdempotencyKey, hasFinished, @@ -323,8 +321,11 @@ export class WorkflowOrchestratorService { if (hasFinished) { const { result, errors } = ret - await originalOnFinishHandler({ - transaction: ret.transaction, + this.notify({ + eventType: "onFinish", + workflowId, + transactionId: transaction.transactionId, + state: transactionState as TransactionState, result, errors, }) @@ -423,6 +424,7 @@ export class WorkflowOrchestratorService { eventType: "onFinish", workflowId, transactionId, + state: ret.transaction.getFlow().state as TransactionState, result, errors, }) @@ -493,6 +495,7 @@ export class WorkflowOrchestratorService { eventType: "onFinish", workflowId, transactionId, + state: ret.transaction.getFlow().state as TransactionState, result, errors, }) @@ -521,14 +524,16 @@ export class WorkflowOrchestratorService { const subscribers = this.subscribers.get(workflowId) ?? new Map() const handlerIndex = (handlers) => { - return handlers.indexOf((s) => s === subscriber || s._id === subscriberId) + return handlers.findIndex( + (s) => s === subscriber || s._id === subscriberId + ) } if (transactionId) { const transactionSubscribers = subscribers.get(transactionId) ?? [] const subscriberIndex = handlerIndex(transactionSubscribers) if (subscriberIndex !== -1) { - transactionSubscribers.slice(subscriberIndex, 1) + transactionSubscribers.splice(subscriberIndex, 1) } transactionSubscribers.push(subscriber) @@ -540,7 +545,7 @@ export class WorkflowOrchestratorService { const workflowSubscribers = subscribers.get(AnySubscriber) ?? [] const subscriberIndex = handlerIndex(workflowSubscribers) if (subscriberIndex !== -1) { - workflowSubscribers.slice(subscriberIndex, 1) + workflowSubscribers.splice(subscriberIndex, 1) } workflowSubscribers.push(subscriber) @@ -568,14 +573,22 @@ export class WorkflowOrchestratorService { const newTransactionSubscribers = filterSubscribers( transactionSubscribers ) - subscribers.set(transactionId, newTransactionSubscribers) + if (newTransactionSubscribers.length) { + subscribers.set(transactionId, newTransactionSubscribers) + } else { + subscribers.delete(transactionId) + } this.subscribers.set(workflowId, subscribers) return } const workflowSubscribers = subscribers.get(AnySubscriber) ?? [] const newWorkflowSubscribers = filterSubscribers(workflowSubscribers) - subscribers.set(AnySubscriber, newWorkflowSubscribers) + if (newWorkflowSubscribers.length) { + subscribers.set(AnySubscriber, newWorkflowSubscribers) + } else { + subscribers.delete(AnySubscriber) + } this.subscribers.set(workflowId, subscribers) } @@ -588,6 +601,7 @@ export class WorkflowOrchestratorService { result, step, response, + state, } = options const subscribers: TransactionSubscribers = @@ -603,6 +617,7 @@ export class WorkflowOrchestratorService { response, result, errors, + state, }) }) } @@ -610,6 +625,10 @@ export class WorkflowOrchestratorService { if (transactionId) { const transactionSubscribers = subscribers.get(transactionId) ?? [] notifySubscribers(transactionSubscribers) + + if (options.eventType === "onFinish") { + subscribers.delete(transactionId) + } } const workflowSubscribers = subscribers.get(AnySubscriber) ?? [] @@ -627,12 +646,14 @@ export class WorkflowOrchestratorService { result, response, errors, + state, }: { eventType: keyof DistributedTransactionEvents step?: TransactionStep response?: unknown result?: unknown errors?: unknown[] + state?: TransactionState }) => { this.notify({ workflowId, @@ -642,6 +663,7 @@ export class WorkflowOrchestratorService { step, result, errors, + state, }) } @@ -664,7 +686,6 @@ export class WorkflowOrchestratorService { notify({ eventType: "onCompensateBegin" }) }, onFinish: ({ transaction, result, errors }) => { - // TODO: unsubscribe transaction handlers on finish customEventHandlers?.onFinish?.({ transaction, result, errors }) }, diff --git a/packages/modules/workflow-engine-redis/CHANGELOG.md b/packages/modules/workflow-engine-redis/CHANGELOG.md index 6c0af95595344..c7811479e3d65 100644 --- a/packages/modules/workflow-engine-redis/CHANGELOG.md +++ b/packages/modules/workflow-engine-redis/CHANGELOG.md @@ -1,5 +1,16 @@ # @medusajs/workflow-engine-redis +## 2.8.8 + +### Patch Changes + +- [#12976](https://github.com/medusajs/medusa/pull/12976) [`eb83954f23077c0714125b6f2f19fd0ef0f288f9`](https://github.com/medusajs/medusa/commit/eb83954f23077c0714125b6f2f19fd0ef0f288f9) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(workflow-engine-\*): Align event subscribers management + +- [#12982](https://github.com/medusajs/medusa/pull/12982) [`238e7d53c13a1c033886d7c33254919f8b5b22dc`](https://github.com/medusajs/medusa/commit/238e7d53c13a1c033886d7c33254919f8b5b22dc) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(wfe): should notify when finished + add state info + +- Updated dependencies [[`43fb06ecc2d87ea2a11e12524679c142eb890ec7`](https://github.com/medusajs/medusa/commit/43fb06ecc2d87ea2a11e12524679c142eb890ec7)]: + - @medusajs/framework@2.8.8 + ## 2.8.7 ### Patch Changes diff --git a/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts b/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts index f8c264c13f2c3..e0bc17bd1e162 100644 --- a/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts +++ b/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts @@ -151,7 +151,7 @@ moduleIntegrationTestRunner({ describe("Testing basic workflow", function () { describe("Cancel transaction", function () { - it("should cancel an ongoing execution with async unfinished yet step", async () => { + it("should cancel an ongoing execution with async unfinished yet step", (done) => { const transactionId = "transaction-to-cancel-id" const step1 = createStep("step1", async () => { return new StepResponse("step1") @@ -179,25 +179,42 @@ moduleIntegrationTestRunner({ } ) - await workflowOrcModule.run(workflowId, { - input: {}, - transactionId, - }) - - await setTimeout(100) - - await workflowOrcModule.cancel(workflowId, { - transactionId, - }) + workflowOrcModule + .run(workflowId, { + input: {}, + transactionId, + }) + .then(async () => { + await setTimeout(100) - await setTimeout(1000) + await workflowOrcModule.cancel(workflowId, { + transactionId, + }) - const execution = await workflowOrcModule.listWorkflowExecutions({ - transaction_id: transactionId, - }) + workflowOrcModule.subscribe({ + workflowId, + transactionId, + subscriber: async (event) => { + if (event.eventType === "onFinish") { + const execution = + await workflowOrcModule.listWorkflowExecutions({ + transaction_id: transactionId, + }) + + expect(execution.length).toEqual(1) + expect(execution[0].state).toEqual( + TransactionState.REVERTED + ) + done() + } + }, + }) + }) - expect(execution.length).toEqual(1) - expect(execution[0].state).toEqual(TransactionState.REVERTED) + failTrap( + done, + "should cancel an ongoing execution with async unfinished yet step" + ) }) it("should cancel a complete execution with a sync workflow running as async", async () => { @@ -550,7 +567,14 @@ moduleIntegrationTestRunner({ return e }) - expect(setStepError).toEqual({ uhuuuu: "yeaah!" }) + expect(setStepError).toEqual( + expect.objectContaining({ + message: JSON.stringify({ + uhuuuu: "yeaah!", + }), + stack: expect.any(String), + }) + ) ;({ data: executionsList } = await query.graph({ entity: "workflow_executions", fields: ["id", "state", "context"], @@ -817,6 +841,7 @@ moduleIntegrationTestRunner({ void workflowOrcModule.subscribe({ workflowId: "wf-when", + transactionId: "trx_123_when", subscriber: (event) => { if (event.eventType === "onFinish") { done() diff --git a/packages/modules/workflow-engine-redis/package.json b/packages/modules/workflow-engine-redis/package.json index d821a25adcdf6..677a04d40f426 100644 --- a/packages/modules/workflow-engine-redis/package.json +++ b/packages/modules/workflow-engine-redis/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/workflow-engine-redis", - "version": "2.8.7", + "version": "2.8.8", "description": "Medusa Workflow Orchestrator module using Redis to track workflows executions", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -36,8 +36,8 @@ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear" }, "devDependencies": { - "@medusajs/framework": "2.8.7", - "@medusajs/test-utils": "2.8.7", + "@medusajs/framework": "2.8.8", + "@medusajs/test-utils": "2.8.8", "@mikro-orm/cli": "6.4.3", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", @@ -55,7 +55,7 @@ "ulid": "^2.3.0" }, "peerDependencies": { - "@medusajs/framework": "2.8.7", + "@medusajs/framework": "2.8.8", "@mikro-orm/core": "6.4.3", "@mikro-orm/migrations": "6.4.3", "@mikro-orm/postgresql": "6.4.3", diff --git a/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts b/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts index f4b9c5b588c85..70025a67ce121 100644 --- a/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts +++ b/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts @@ -63,6 +63,7 @@ type NotifyOptions = { response?: unknown result?: unknown errors?: unknown[] + state?: TransactionState } type WorkflowId = string @@ -318,9 +319,6 @@ export class WorkflowOrchestratorService { throw new Error(`Workflow with id "${workflowId}" not found.`) } - const originalOnFinishHandler = events.onFinish! - delete events.onFinish - const transaction = await this.getRunningTransaction( workflowId, transactionId, @@ -352,12 +350,11 @@ export class WorkflowOrchestratorService { const metadata = ret.transaction.getFlow().metadata const { parentStepIdempotencyKey } = metadata ?? {} - const hasFailed = [TransactionState.FAILED].includes( - ret.transaction.getFlow().state - ) + const transactionState = ret.transaction.getFlow().state + const hasFailed = [TransactionState.FAILED].includes(transactionState) const acknowledgement = { - transactionId: context.transactionId, + transactionId: transaction.transactionId, workflowId: workflowId, parentStepIdempotencyKey, hasFinished, @@ -368,8 +365,11 @@ export class WorkflowOrchestratorService { if (hasFinished) { const { result, errors } = ret - await originalOnFinishHandler({ - transaction: ret.transaction, + this.notify({ + eventType: "onFinish", + workflowId, + transactionId: transaction.transactionId, + state: transactionState as TransactionState, result, errors, }) @@ -449,9 +449,6 @@ export class WorkflowOrchestratorService { workflowId, }) - const originalOnFinishHandler = events.onFinish! - delete events.onFinish - const ret = await exportedWorkflow.registerStepSuccess({ idempotencyKey: idempotencyKey_, context, @@ -466,8 +463,11 @@ export class WorkflowOrchestratorService { if (ret.transaction.hasFinished()) { const { result, errors } = ret - await originalOnFinishHandler({ - transaction: ret.transaction, + this.notify({ + eventType: "onFinish", + workflowId, + transactionId, + state: ret.transaction.getFlow().state as TransactionState, result, errors, }) @@ -520,9 +520,6 @@ export class WorkflowOrchestratorService { workflowId, }) - const originalOnFinishHandler = events.onFinish! - delete events.onFinish - const ret = await exportedWorkflow.registerStepFailure({ idempotencyKey: idempotencyKey_, context, @@ -537,8 +534,11 @@ export class WorkflowOrchestratorService { if (ret.transaction.hasFinished()) { const { result, errors } = ret - await originalOnFinishHandler({ - transaction: ret.transaction, + this.notify({ + eventType: "onFinish", + workflowId, + transactionId, + state: ret.transaction.getFlow().state as TransactionState, result, errors, }) @@ -572,14 +572,16 @@ export class WorkflowOrchestratorService { } const handlerIndex = (handlers) => { - return handlers.indexOf((s) => s === subscriber || s._id === subscriberId) + return handlers.findIndex( + (s) => s === subscriber || s._id === subscriberId + ) } if (transactionId) { const transactionSubscribers = subscribers.get(transactionId) ?? [] const subscriberIndex = handlerIndex(transactionSubscribers) if (subscriberIndex !== -1) { - transactionSubscribers.slice(subscriberIndex, 1) + transactionSubscribers.splice(subscriberIndex, 1) } transactionSubscribers.push(subscriber) @@ -591,7 +593,7 @@ export class WorkflowOrchestratorService { const workflowSubscribers = subscribers.get(AnySubscriber) ?? [] const subscriberIndex = handlerIndex(workflowSubscribers) if (subscriberIndex !== -1) { - workflowSubscribers.slice(subscriberIndex, 1) + workflowSubscribers.splice(subscriberIndex, 1) } workflowSubscribers.push(subscriber) @@ -604,7 +606,10 @@ export class WorkflowOrchestratorService { transactionId, subscriberOrId, }: UnsubscribeOptions) { - const subscribers = this.subscribers.get(workflowId) ?? new Map() + const subscribers = this.subscribers.get(workflowId) + if (!subscribers) { + return + } const filterSubscribers = (handlers: SubscriberHandler[]) => { return handlers.filter((handler) => { @@ -614,25 +619,36 @@ export class WorkflowOrchestratorService { }) } - // Unsubscribe instance - if (!this.subscribers.has(workflowId)) { - void this.redisSubscriber.unsubscribe(this.getChannelName(workflowId)) - } - if (transactionId) { - const transactionSubscribers = subscribers.get(transactionId) ?? [] - const newTransactionSubscribers = filterSubscribers( - transactionSubscribers - ) - subscribers.set(transactionId, newTransactionSubscribers) - this.subscribers.set(workflowId, subscribers) - return + const transactionSubscribers = subscribers.get(transactionId) + if (transactionSubscribers) { + const newTransactionSubscribers = filterSubscribers( + transactionSubscribers + ) + + if (newTransactionSubscribers.length) { + subscribers.set(transactionId, newTransactionSubscribers) + } else { + subscribers.delete(transactionId) + } + } + } else { + const workflowSubscribers = subscribers.get(AnySubscriber) + if (workflowSubscribers) { + const newWorkflowSubscribers = filterSubscribers(workflowSubscribers) + + if (newWorkflowSubscribers.length) { + subscribers.set(AnySubscriber, newWorkflowSubscribers) + } else { + subscribers.delete(AnySubscriber) + } + } } - const workflowSubscribers = subscribers.get(AnySubscriber) ?? [] - const newWorkflowSubscribers = filterSubscribers(workflowSubscribers) - subscribers.set(AnySubscriber, newWorkflowSubscribers) - this.subscribers.set(workflowId, subscribers) + if (subscribers.size === 0) { + this.subscribers.delete(workflowId) + void this.redisSubscriber.unsubscribe(this.getChannelName(workflowId)) + } } private async notify( @@ -661,6 +677,7 @@ export class WorkflowOrchestratorService { result, step, response, + state, } = options const subscribers: TransactionSubscribers = @@ -676,6 +693,7 @@ export class WorkflowOrchestratorService { response, result, errors, + state, } const isPromise = "then" in handler if (isPromise) { @@ -721,12 +739,14 @@ export class WorkflowOrchestratorService { result, response, errors, + state, }: { eventType: keyof DistributedTransactionEvents step?: TransactionStep response?: unknown result?: unknown errors?: unknown[] + state?: TransactionState }) => { await this.notify({ workflowId, @@ -736,6 +756,7 @@ export class WorkflowOrchestratorService { step, result, errors, + state, }) } diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx index 2d2169fe06458..2520de067ddc3 100644 --- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx +++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx @@ -108,9 +108,9 @@ const TagsOperationDescriptionSectionEvent = ({ ) : ( Deprecated ))} - {event.version && ( - - v{event.version} + {event.since && ( + + v{event.since} )}
diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx index 7715a766b0d9c..9fb48e734cab9 100644 --- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx +++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/index.tsx @@ -78,12 +78,12 @@ const TagsOperationDescriptionSection = ({ badgeClassName="ml-0.5" /> )} - {operation["x-version"] && ( + {operation["x-since"] && ( - v{operation["x-version"]} + v{operation["x-since"]} )} diff --git a/www/apps/api-reference/generated/generated-admin-sidebar.mjs b/www/apps/api-reference/generated/generated-admin-sidebar.mjs index 6b668773db6c2..85fa41cef361f 100644 --- a/www/apps/api-reference/generated/generated-admin-sidebar.mjs +++ b/www/apps/api-reference/generated/generated-admin-sidebar.mjs @@ -20,6 +20,12 @@ const generatedgeneratedAdminSidebarSidebar = { "path": "http-compression", "loaded": true }, + { + "type": "link", + "title": "Manage Metadata", + "path": "manage-metadata", + "loaded": true + }, { "type": "link", "title": "Select Fields and Relations", diff --git a/www/apps/api-reference/generated/generated-store-sidebar.mjs b/www/apps/api-reference/generated/generated-store-sidebar.mjs index 556852701d77d..94325bcb4b30e 100644 --- a/www/apps/api-reference/generated/generated-store-sidebar.mjs +++ b/www/apps/api-reference/generated/generated-store-sidebar.mjs @@ -26,6 +26,12 @@ const generatedgeneratedStoreSidebarSidebar = { "path": "http-compression", "loaded": true }, + { + "type": "link", + "title": "Manage Metadata", + "path": "manage-metadata", + "loaded": true + }, { "type": "link", "title": "Select Fields and Relations", diff --git a/www/apps/api-reference/markdown/admin.mdx b/www/apps/api-reference/markdown/admin.mdx index b866601c15be0..e26980bc4bdf1 100644 --- a/www/apps/api-reference/markdown/admin.mdx +++ b/www/apps/api-reference/markdown/admin.mdx @@ -483,6 +483,226 @@ x-no-compression: false +## Manage Metadata + +Many data models in Medusa, such as products and carts, have a `metadata` field that allows you to store custom information in key-value pairs. + +When setting or updating the `metadata` field using the relevant API routes, Medusa will merge the new metadata with the existing metadata. + + + +The instructions in this section apply to any [JSON property in a data model](!docs!/learn/fundamentals/data-models/json-properties). + + + + + + + + + + + +### Accepted Values in Metadata + +The `metadata` is an object of key-value pairs, where the keys are strings and the values can be one of the following types: + +- String + - An empty string deletes the property from the metadata. +- Number +- Boolean +- Date +- Object +- Arrays of any of the above types + +The `metadata` is not validated, so you can store any custom data in it. + + + + + +```ts title="Metadata Example" +{ + "metadata": { + "category": "electronics", + "views": 1500, + "is_featured": true, + "tags": ["new", "sale"], + "details": { + "warranty": "2 years", + "origin": "USA" + } + } +} +``` + + + + + + + + + +### Add or Update New Property in Metadata + +To add or update a property in the `metadata`, pass the property in the request body as a key-value pair. This won't affect existing properties in the metadata. + + + + + + + + + +```js title="Add new metadata property" +sdk.admin.product.update("prod_123", { + metadata: { + new_property: "value" + } +}) +``` + + + + + +```json title="Result" +{ + "id": "prod_123", + "metadata": { + "new_property": "value", + "old_property": "value" + } +} +``` + + + + + + + + + + + + + +### Update Nested Objects in Metadata + +When updating a nested object in the `metadata`, you must pass the entire object in the request body. + +Medusa doesn't merge nested objects, so if you pass a partial object, the existing properties in the nested object will be removed. + + + + + + + + + +```js title="Update nested object in metadata" +sdk.admin.product.update("prod_123", { + metadata: { + nested_object: { + property1: "value1", + property2: "value2" + } + } +}) +``` + + + + + +```json title="Result" +{ + "id": "prod_123", + "metadata": { + "nested_object": { + "property1": "value1", + "property2": "value2" + } + } +} +``` + + + + + + + + + + + + + +### Remove Property from Metadata + +To remove a property from the `metadata`, pass the property in the request body with an empty string value. This will remove the property from the `metadata` without affecting other properties. + + + + + + + + + + + +```js title="Remove metadata property" +sdk.admin.product.update("prod_123", { + metadata: { + property_to_remove: "" + } +}) +``` + + + + + +```json title="Result" +{ + "id": "prod_123", + "metadata": { + "other_property": "value" + } +} +``` + + + + + + + + + + + + + + + + + ## Select Fields and Relations diff --git a/www/apps/api-reference/markdown/store.mdx b/www/apps/api-reference/markdown/store.mdx index 6c5b8f58abd3f..401a52f0b8390 100644 --- a/www/apps/api-reference/markdown/store.mdx +++ b/www/apps/api-reference/markdown/store.mdx @@ -441,6 +441,253 @@ x-no-compression: false +## Manage Metadata + +Many data models in Medusa, such as products and carts, have a `metadata` field that allows you to store custom information in key-value pairs. + +When setting or updating the `metadata` field using the relevant API routes, Medusa will merge the new metadata with the existing metadata. + + + +The instructions in this section apply to any [JSON property in a data model](!docs!/learn/fundamentals/data-models/json-properties). + + + + + + + + + + + +### Accepted Values in Metadata + +The `metadata` is an object of key-value pairs, where the keys are strings and the values can be one of the following types: + +- String + - An empty string deletes the property from the metadata. +- Number +- Boolean +- Date +- Object +- Arrays of any of the above types + +The `metadata` is not validated, so you can store any custom data in it. + + + + + +```ts title="Metadata Example" +{ + "metadata": { + "category": "electronics", + "views": 1500, + "is_featured": true, + "tags": ["new", "sale"], + "details": { + "warranty": "2 years", + "origin": "USA" + } + } +} +``` + + + + + + + + + +### Add or Update New Property in Metadata + +To add or update a property in the `metadata`, pass the property in the request body as a key-value pair. This won't affect existing properties in the metadata. + + + + + + + + + +```js title="Add new metadata property" +sdk.store.cart.updateLineItem( + "cart_123", + "li_123", + { + metadata: { + new_property: "value" + } + } +) +``` + + + + + +```json title="Result" +{ + "id": "cart_123", + "items": [ + { + "id": "li_123", + "metadata": { + "new_property": "value", + "old_property": "value" + } + } + ] +} +``` + + + + + + + + + + + + + +### Update Nested Objects in Metadata + +When updating a nested object in the `metadata`, you must pass the entire object in the request body. + +Medusa doesn't merge nested objects, so if you pass a partial object, the existing properties in the nested object will be removed. + + + + + + + + + +```js title="Update nested object in metadata" +sdk.store.cart.updateLineItem( + "cart_123", + "li_123", + { + metadata: { + nested_object: { + property1: "value1", + property2: "value2" + } + } + } +) +``` + + + + + +```json title="Result" +{ + "id": "cart_123", + "items": [ + { + "id": "li_123", + "metadata": { + "nested_object": { + "property1": "value1", + "property2": "value2" + } + } + } + ] +} +``` + + + + + + + + + + + + + +### Remove Property from Metadata + +To remove a property from the `metadata`, pass the property in the request body with an empty string value. This will remove the property from the `metadata` without affecting other properties. + + + + + + + + + + + +```js title="Remove metadata property" +sdk.store.cart.updateLineItem( + "cart_123", + "li_123", + { + metadata: { + property_to_remove: "" + } + } +) +``` + + + + + +```json title="Result" +{ + "id": "cart_123", + "items": [ + { + "id": "li_123", + "metadata": { + "other_property": "value" + } + } + ] +} +``` + + + + + + + + + + + + + + + + + ## Select Fields and Relations @@ -1167,6 +1414,10 @@ This sorts the products by their `created_at` field in the descending order. + + + + @@ -1184,10 +1435,10 @@ Refer to [this guide](!docs!/learn/customization/extend-features/extend-create-p diff --git a/www/apps/api-reference/package.json b/www/apps/api-reference/package.json index 675cc98449f5e..ba2a63abbd476 100644 --- a/www/apps/api-reference/package.json +++ b/www/apps/api-reference/package.json @@ -16,9 +16,9 @@ "dependencies": { "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@medusajs/icons": "2.8.5", - "@medusajs/ui": "4.0.15", - "@next/mdx": "15.3.1", + "@medusajs/icons": "2.8.8", + "@medusajs/ui": "4.0.18", + "@next/mdx": "15.3.5", "@react-hook/resize-observer": "^2.0.2", "@readme/openapi-parser": "^2.5.0", "algoliasearch": "4", @@ -29,7 +29,7 @@ "jsdom": "^22.1.0", "json-schema": "^0.4.0", "json-stringify-pretty-compact": "^4.0.0", - "next": "15.3.1", + "next": "15.3.5", "next-mdx-remote": "5.0.0", "openapi-sampler": "^1.3.1", "pluralize": "^8.0.0", @@ -51,7 +51,7 @@ "yaml": "^2.3.1" }, "devDependencies": { - "@next/bundle-analyzer": "15.3.1", + "@next/bundle-analyzer": "15.3.5", "@types/jsdom": "^21.1.1", "@types/mapbox__rehype-prism": "^0.8.0", "@types/mdx": "^2.0.13", diff --git a/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_archive/post.js b/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_archive/post.js new file mode 100644 index 0000000000000..1456f49bd458b --- /dev/null +++ b/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_archive/post.js @@ -0,0 +1,14 @@ +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", + }, +}) + +sdk.admin.order.archive("order_123") +.then(({ order }) => { + console.log(order) +}) \ No newline at end of file diff --git a/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_complete/post.js b/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_complete/post.js new file mode 100644 index 0000000000000..cdce8f7c9827f --- /dev/null +++ b/www/apps/api-reference/specs/admin/code_samples/JavaScript/admin_orders_{id}_complete/post.js @@ -0,0 +1,14 @@ +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", + }, +}) + +sdk.admin.order.complete("order_123") +.then(({ order }) => { + console.log(order) +}) \ No newline at end of file diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderItems.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderItems.yaml index fc172864f445d..4e62ee3b227d8 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderItems.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderItems.yaml @@ -42,3 +42,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderShippingMethod.yaml index 443413665ea8b..b2e96363930ef 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminAddDraftOrderShippingMethod.yaml @@ -25,3 +25,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProduct.yaml index 0af27aa4755d6..c59675a8e7175 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProduct.yaml @@ -147,6 +147,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProductVariant.yaml index 82a68abb60700..bd4043cb70be5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminBatchUpdateProductVariant.yaml @@ -71,6 +71,9 @@ properties: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminClaim.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminClaim.yaml index d6104bdcebdb2..c4bf2d32df1bc 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminClaim.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminClaim.yaml @@ -68,6 +68,9 @@ properties: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCollection.yaml index 572a94f4f68c8..68b150b1ba570 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateCustomerGroup.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateCustomerGroup.yaml index 4dd642b053256..1f528e4cc8dda 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateCustomerGroup.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateCustomerGroup.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillment.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillment.yaml index 6359d75d4c3ff..31eec6b3efa3d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillment.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillment.yaml @@ -2,10 +2,10 @@ type: object description: The filfillment's details. x-schemaName: AdminCreateFulfillment required: - - data - items - metadata - order_id + - data - location_id - provider_id - delivery_address @@ -70,6 +70,9 @@ properties: metadata: type: object description: The delivery address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata items: type: array description: The items to fulfill. @@ -167,3 +170,6 @@ properties: metadata: type: object description: The fulfillment's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml new file mode 100644 index 0000000000000..4d3d7c2ae0efb --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml @@ -0,0 +1,144 @@ +type: object +description: The service zone's details. +required: + - name +properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - metadata + - country_code + - type + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: country + - type: object + description: A province geo zone. + required: + - metadata + - country_code + - type + - province_code + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: province + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + - type: object + description: A city geo zone + required: + - metadata + - country_code + - type + - province_code + - city + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: city + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + - type: object + description: A ZIP geo zone. + required: + - metadata + - country_code + - type + - province_code + - city + - postal_expression + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: zip + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. +x-schemaName: AdminCreateFulfillmentSetServiceZones diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateGiftCardParams.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateGiftCardParams.yaml index 98f5b3fc7844d..df9577984cf37 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateGiftCardParams.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateGiftCardParams.yaml @@ -49,3 +49,6 @@ properties: metadata: type: object description: The gift card's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryItem.yaml index 21a7ed1cd95c1..db8552d937889 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryItem.yaml @@ -57,3 +57,6 @@ properties: metadata: type: object description: The inventory item's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryLocationLevel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryLocationLevel.yaml new file mode 100644 index 0000000000000..6578c122f4a8c --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateInventoryLocationLevel.yaml @@ -0,0 +1,18 @@ +type: object +description: The inventory level's details. +required: + - location_id +properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. +x-schemaName: AdminCreateInventoryLocationLevel diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateOrderCreditLines.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateOrderCreditLines.yaml index a5d094df4d45b..edb07be15c9a7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateOrderCreditLines.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateOrderCreditLines.yaml @@ -24,3 +24,6 @@ properties: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCapture.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCapture.yaml new file mode 100644 index 0000000000000..0064576c86290 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCapture.yaml @@ -0,0 +1,8 @@ +type: object +description: The payment's details. +properties: + amount: + type: number + title: amount + description: The amount to capture. +x-schemaName: AdminCreatePaymentCapture diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCollection.yaml new file mode 100644 index 0000000000000..9d1f8bef7bc4a --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentCollection.yaml @@ -0,0 +1,15 @@ +type: object +description: The payment collection's details. +required: + - order_id + - amount +properties: + order_id: + type: string + title: order_id + description: The ID of the associated order. + amount: + type: number + title: amount + description: The amount to be paid. +x-schemaName: AdminCreatePaymentCollection diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentRefund.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentRefund.yaml new file mode 100644 index 0000000000000..b5be8ea589b70 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreatePaymentRefund.yaml @@ -0,0 +1,16 @@ +type: object +description: The refund's details. +properties: + amount: + type: number + title: amount + description: The amount to refund. + refund_reason_id: + type: string + title: refund_reason_id + description: The ID of a refund reason. + note: + type: string + title: note + description: A note to attach to the refund. +x-schemaName: AdminCreatePaymentRefund diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProduct.yaml index 0d83da3c0051a..e463da6f69226 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProduct.yaml @@ -146,6 +146,9 @@ properties: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductCategory.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductCategory.yaml index 5273e97f2bd2c..b4eebd83f25bc 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductCategory.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductCategory.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The product category's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductTag.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductTag.yaml index 88b118a73fe08..453f7815fef0d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductTag.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductTag.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The product tag's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductType.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductType.yaml index 92179c70669bc..ec0a42d6a9516 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductType.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductType.yaml @@ -7,6 +7,9 @@ properties: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata value: type: string title: value diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductVariant.yaml index 5cbbf93868f35..29b6784abaf57 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateProductVariant.yaml @@ -74,6 +74,9 @@ properties: metadata: type: object description: The variant's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The variant's prices. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateRegion.yaml index 6028eaf5d3831..b50a259f29132 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateRegion.yaml @@ -41,3 +41,6 @@ properties: metadata: type: object description: The region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReservation.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReservation.yaml index 1f78aa26dd015..a025ab2c17316 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReservation.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReservation.yaml @@ -29,3 +29,6 @@ properties: metadata: type: object description: The reservation's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReturnReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReturnReason.yaml index adac302423d2a..4be2cf28d12cb 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReturnReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateReturnReason.yaml @@ -24,3 +24,6 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateSalesChannel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateSalesChannel.yaml index 69ac7d89c6f3d..cd673e32859bd 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateSalesChannel.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateSalesChannel.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The sales channel's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateShippingProfile.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateShippingProfile.yaml index 80a6b3ae524d4..679f3438f9e10 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateShippingProfile.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateShippingProfile.yaml @@ -16,3 +16,6 @@ properties: metadata: type: object description: The shipping profile's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocation.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocation.yaml index 4e52d487ce02a..bfa63d63d0327 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocation.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocation.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The stock location's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml new file mode 100644 index 0000000000000..f09930060123f --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml @@ -0,0 +1,15 @@ +type: object +description: The fulfillment set to create. +required: + - type + - name +properties: + name: + type: string + title: name + description: The fulfillment set's name. + type: + type: string + title: type + description: The fulfillment set's type. +x-schemaName: AdminCreateStockLocationFulfillmentSet diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStoreCreditAccount.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStoreCreditAccount.yaml index 56c12f053632e..387063e6c9d24 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateStoreCreditAccount.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRate.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRate.yaml index 8aa3b751deccb..474c362dbc3ec 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRate.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRate.yaml @@ -43,3 +43,6 @@ properties: metadata: type: object description: The tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRegion.yaml index f2343d568f239..f882c430527c3 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCreateTaxRegion.yaml @@ -51,9 +51,15 @@ properties: metadata: type: object description: The default tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The tax region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomer.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomer.yaml index 7a8ef94f4fc1f..7c6d09c86a6d7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomer.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomer.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The customer's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerAddress.yaml index 9cce8725ae34e..9cfc816b85e4c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerAddress.yaml @@ -89,6 +89,9 @@ properties: metadata: type: object description: The address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerGroup.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerGroup.yaml index ddef197fa058e..cfc0b7ae4c80a 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerGroup.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminCustomerGroup.yaml @@ -25,6 +25,9 @@ properties: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrder.yaml index d0807a7018891..7a40e1caf05e4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrder.yaml @@ -144,6 +144,9 @@ properties: metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrderPreview.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrderPreview.yaml index 992e94feb296d..8f3a96628c3e5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrderPreview.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminDraftOrderPreview.yaml @@ -215,6 +215,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -352,6 +355,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -541,6 +547,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminExchange.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminExchange.yaml index 0123920e3f91e..b693ed23331e2 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminExchange.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminExchange.yaml @@ -66,6 +66,9 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillment.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillment.yaml index a459be7817a01..140d1f4809693 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillment.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillment.yaml @@ -77,6 +77,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillmentAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillmentAddress.yaml index 67f9793026b6b..c207f520f1279 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillmentAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminFulfillmentAddress.yaml @@ -74,6 +74,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminInventoryLevel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminInventoryLevel.yaml index d7802818c2b7b..b0f6bb9c256a9 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminInventoryLevel.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminInventoryLevel.yaml @@ -56,6 +56,9 @@ properties: metadata: type: object description: The location level's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_item: type: object available_quantity: diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminInvite.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminInvite.yaml index 566d068b14a79..500fa0a27fd97 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminInvite.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminInvite.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The invite's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminInviteAccept.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminInviteAccept.yaml new file mode 100644 index 0000000000000..9b853e7621725 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminInviteAccept.yaml @@ -0,0 +1,17 @@ +type: object +description: The details of the user to be created. +properties: + email: + type: string + title: email + description: The user's email. + format: email + first_name: + type: string + title: first_name + description: The user's first name. + last_name: + type: string + title: last_name + description: The user's last name. +x-schemaName: AdminInviteAccept diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminMarkPaymentCollectionPaid.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminMarkPaymentCollectionPaid.yaml new file mode 100644 index 0000000000000..eec135671047f --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminMarkPaymentCollectionPaid.yaml @@ -0,0 +1,10 @@ +type: object +description: The payment details. +required: + - order_id +properties: + order_id: + type: string + title: order_id + description: The ID of the order associated with the payment collection. +x-schemaName: AdminMarkPaymentCollectionPaid diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrder.yaml index ac1c9a048912e..f0d0784c760cb 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrder.yaml @@ -139,6 +139,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderAddress.yaml index fad010e48baaa..fb80941ffa009 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderAddress.yaml @@ -65,6 +65,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderChange.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderChange.yaml index 195bd76d5e903..36005625f3107 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderChange.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderChange.yaml @@ -112,6 +112,9 @@ properties: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderFulfillment.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderFulfillment.yaml index e1312673f4fc0..7ae1b3f61d526 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderLineItem.yaml index a6b3ba9b68fbb..c590d05144d0f 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderLineItem.yaml @@ -160,6 +160,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderPreview.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderPreview.yaml index 030a16e48c1d2..98cf4090b772c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderPreview.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderPreview.yaml @@ -217,6 +217,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -354,6 +357,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -543,6 +549,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderShippingMethod.yaml index ab93242120a98..1b64bc87e6901 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPaymentCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPaymentCollection.yaml index 1af31ef86411a..a67ab43e575b8 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPaymentCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml index c9ce2644120bf..71c3bc5ac6d92 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml @@ -31,3 +31,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml index 8d6fe6cdad202..d16214b606e31 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingReqSchema.yaml index 34e2356020b13..ec629bd78ae35 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostClaimsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml index bc0234b0bdbfc..33f64855b338f 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml index 666ccbcfe538a..b54a811facc57 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml index da3d011473f9d..0149dfd44a0ed 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml index 9564ca8aa45c2..4fa5f2d3351dd 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingReqSchema.yaml index 4bff8e27eb9e0..577bcd7bc7b28 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostExchangesShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderClaimsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderClaimsReqSchema.yaml index e3dc6b300ef8a..b828e16fa2fde 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderClaimsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderClaimsReqSchema.yaml @@ -30,3 +30,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml index d477bfb83846d..91614c5e78cda 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata compare_at_unit_price: type: number title: compare_at_unit_price diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsReqSchema.yaml index 23a158dae9887..bffbcdd2c22ae 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsReqSchema.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml index 3bad2acc19bbe..a7c70033520fe 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml index 3327fc1442a9b..4aa99ea642bbf 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderExchangesReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderExchangesReqSchema.yaml index 4472a42d37093..f6f32aad33d55 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderExchangesReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostOrderExchangesReqSchema.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReceiveReturnsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReceiveReturnsReqSchema.yaml index 12d20a347b64e..f4b1f18cb737e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReceiveReturnsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReceiveReturnsReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReqSchema.yaml index 0fbd5801592d2..137c180ca949e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReqSchema.yaml @@ -27,3 +27,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml index 0d0c6e633cc59..0ae505d7fbd76 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml index d47549d7ef8b5..b64ffbe8d0ee4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReturnReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReturnReqSchema.yaml index bdb5f9b00d8a1..638346da8c60c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReturnReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsReturnReqSchema.yaml @@ -15,3 +15,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml index 2fb4607a740a5..0ad7fb99be735 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingReqSchema.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingReqSchema.yaml index 861fa03fc7c5f..d7ab099fe3c0f 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminPostReturnsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProduct.yaml index 304df0f157c2a..6674fcc3e6bbe 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProduct.yaml @@ -86,6 +86,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductCategory.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductCategory.yaml index 3c5286bc3436a..b14093d05dd76 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductCategory.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductCategory.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductImage.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductImage.yaml index d1561e5a2fcf4..1238fdf516f32 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductImage.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductImage.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductOption.yaml index 415115c4163d5..ab5b7196de03d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductOption.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductOption.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductOptionValue.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductOptionValue.yaml index 88175464a4be2..28ec39ea38dac 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductOptionValue.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductTag.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductTag.yaml index 7fc320de89716..8fb2c7567ec8d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductTag.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductTag.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductType.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductType.yaml index ef5f2ece56c12..6cef7208c7e8a 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductType.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductType.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminProductVariant.yaml index 5696575641f96..b6c4ca8752b64 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminProductVariant.yaml @@ -136,6 +136,9 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_items: type: array description: The variant's inventory items. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminRefundReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminRefundReason.yaml index 919bb56bfa28d..a1f49a55fafd5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminRefundReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminRefundReason.yaml @@ -23,6 +23,9 @@ properties: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminRegion.yaml index 96b6ff979c1fb..f8126c6286672 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminRegion.yaml @@ -36,6 +36,9 @@ properties: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminReservation.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminReservation.yaml index 2b75d1102d26b..652eb2043756d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminReservation.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminReservation.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The reservation's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminReturnItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminReturnItem.yaml index 96691fc7caa16..0d4d8cb24930f 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminReturnItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminReturnItem.yaml @@ -48,3 +48,6 @@ properties: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminReturnReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminReturnReason.yaml index 7de3166b718f4..fe647c7656362 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminReturnReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminReturnReason.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminSalesChannel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminSalesChannel.yaml index 2311a9b68784f..8ed84c4f28f1d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminSalesChannel.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminSalesChannel.yaml @@ -30,6 +30,9 @@ properties: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOption.yaml index 7cb57ee3897ed..116a1b720e38c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOption.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOption.yaml @@ -88,6 +88,9 @@ properties: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOptionRule.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOptionRule.yaml index 2676706b8ee9d..7f25523307731 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOptionRule.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingOptionRule.yaml @@ -33,8 +33,18 @@ properties: - gte - nin value: - type: string - title: value + oneOf: + - type: string + title: value + description: The shipping option rule's value. + example: 'true' + - type: array + description: The shipping option rule's values. + items: + type: string + title: value + description: A value of the shipping option rule. + example: 'true' shipping_option_id: type: string title: shipping_option_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingProfile.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingProfile.yaml index 1dc31c1b5b70d..9a29de8f6cd90 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminShippingProfile.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminShippingProfile.yaml @@ -17,6 +17,9 @@ properties: metadata: type: object description: The shipping profile's metadata, holds custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminStore.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminStore.yaml index 32f483dc82209..7dbecee439f49 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminStore.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminStore.yaml @@ -40,6 +40,9 @@ properties: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminStoreCreditAccount.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminStoreCreditAccount.yaml index 0e54b944b40a4..4c19a0069c3b8 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminStoreCreditAccount.yaml @@ -49,6 +49,9 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRate.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRate.yaml index 7972123ffef44..19c88dbd9bb70 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRate.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRate.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_region_id: type: string title: tax_region_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRegion.yaml index f75bb9af92ef8..7595bc5aba2ae 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminTaxRegion.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata parent_id: type: string title: parent_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminTransaction.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminTransaction.yaml index 0a488a5fd2715..fae0c25d1cea3 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminTransaction.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminTransaction.yaml @@ -51,6 +51,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminTransactionGroup.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminTransactionGroup.yaml index bf509bb167394..40e5f640f0048 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminTransactionGroup.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminTransactionGroup.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateCustomerGroup.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateCustomerGroup.yaml index 57fb90e7151e6..930e67c47e748 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateCustomerGroup.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateCustomerGroup.yaml @@ -9,3 +9,6 @@ properties: metadata: type: object description: The customer group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrder.yaml index e4e0c8a5ee963..8396b38deb6b6 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrder.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The shipping address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The draft order's billing address. @@ -111,9 +114,15 @@ properties: metadata: type: object description: The billing address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata customer_id: type: string title: customer_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml index 8b2d7422db30a..48b76c24ec6ca 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml @@ -25,3 +25,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml new file mode 100644 index 0000000000000..a319aa1491ce3 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml @@ -0,0 +1,158 @@ +type: object +description: The service zone's details. +properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's associated geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - type + - metadata + - country_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: country + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A province geo zone. + required: + - type + - metadata + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: province + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A city geo zone + required: + - type + - metadata + - city + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: city + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A ZIP geo zone. + required: + - type + - metadata + - city + - country_code + - province_code + - postal_expression + properties: + type: + type: string + title: type + description: The geo zone's type. + default: zip + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + id: + type: string + title: id + description: The ID of an existing geo zone. +x-schemaName: AdminUpdateFulfillmentSetServiceZones diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateGiftCardParams.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateGiftCardParams.yaml index f53e7e972823f..108d2cde1ea56 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateGiftCardParams.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateGiftCardParams.yaml @@ -32,3 +32,6 @@ properties: metadata: type: object description: The gift card's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryItem.yaml new file mode 100644 index 0000000000000..c7a6b674506a8 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryItem.yaml @@ -0,0 +1,64 @@ +type: object +description: The properties to update in the inventory item. +properties: + sku: + type: string + title: sku + description: The inventory item's SKU. + hs_code: + type: string + title: hs_code + description: The inventory item's HS code. + weight: + type: number + title: weight + description: The inventory item's weight. + length: + type: number + title: length + description: The inventory item's length. + height: + type: number + title: height + description: The inventory item's height. + width: + type: number + title: width + description: The inventory item's width. + origin_country: + type: string + title: origin_country + description: The inventory item's origin country. + mid_code: + type: string + title: mid_code + description: The inventory item's MID code. + material: + type: string + title: material + description: The inventory item's material. + title: + type: string + title: title + description: The inventory item's title. + description: + type: string + title: description + description: The inventory item's description. + requires_shipping: + type: boolean + title: requires_shipping + description: Whether the inventory item requires shipping. + thumbnail: + type: string + title: thumbnail + description: >- + The URL of an image to be used as the inventory item's thumbnail. You can + use the Upload API routes to upload an image and get its URL. + metadata: + type: object + description: The inventory item's metadata. Can be custom data in key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateInventoryItem diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryLocationLevel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryLocationLevel.yaml new file mode 100644 index 0000000000000..94fba3e700154 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateInventoryLocationLevel.yaml @@ -0,0 +1,12 @@ +type: object +description: The properties to update in the inventory level. +properties: + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. +x-schemaName: AdminUpdateInventoryLocationLevel diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateOrder.yaml index 78280d8e7652c..cd3ef369d9dff 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateOrder.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The order's billing address. @@ -111,6 +114,12 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdatePaymentRefundReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdatePaymentRefundReason.yaml new file mode 100644 index 0000000000000..1573aa64b7003 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdatePaymentRefundReason.yaml @@ -0,0 +1,12 @@ +type: object +description: The properties to update in the refund reason. +properties: + label: + type: string + title: label + description: The refund reason's label. + description: + type: string + title: description + description: The refund reason's description. +x-schemaName: AdminUpdatePaymentRefundReason diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProduct.yaml index 1f220e671fcae..c0d35a78920e7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProduct.yaml @@ -147,6 +147,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductCategory.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductCategory.yaml new file mode 100644 index 0000000000000..2f8962e6516fe --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductCategory.yaml @@ -0,0 +1,40 @@ +type: object +description: The properties to update in the product category. +properties: + name: + type: string + title: name + description: The product category's name. + description: + type: string + title: description + description: The product category's description. + handle: + type: string + title: handle + description: The product category's handle. Must be a unique value. + is_internal: + type: boolean + title: is_internal + description: >- + Whether the product category is only used for internal purposes and + shouldn't be shown the customer. + is_active: + type: boolean + title: is_active + description: Whether the product category is active. + parent_category_id: + type: string + title: parent_category_id + description: The ID of a parent category. + metadata: + type: object + description: The product category's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + rank: + type: number + title: rank + description: The product category's rank among other categories. +x-schemaName: AdminUpdateProductCategory diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductTag.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductTag.yaml new file mode 100644 index 0000000000000..50b569184ec65 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductTag.yaml @@ -0,0 +1,14 @@ +type: object +description: The properties to update in the product tag. +properties: + value: + type: string + title: value + description: The product tag's value. + metadata: + type: object + description: The product tag's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateProductTag diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductType.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductType.yaml new file mode 100644 index 0000000000000..6c60edd1a6179 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductType.yaml @@ -0,0 +1,14 @@ +type: object +description: The properties to update in the product type. +properties: + value: + type: string + title: value + description: The product type's value. + metadata: + type: object + description: The product type's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateProductType diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductVariant.yaml index 59f99cff83c1d..93e6b702666c6 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateProductVariant.yaml @@ -71,6 +71,9 @@ properties: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateRegion.yaml new file mode 100644 index 0000000000000..740456f51c0fb --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateRegion.yaml @@ -0,0 +1,40 @@ +type: object +description: The propeties to update in the region. +properties: + name: + type: string + title: name + description: The region's name. + currency_code: + type: string + title: currency_code + description: The region's currency code. + countries: + type: array + description: The region's countries. + items: + type: string + title: countries + description: A country code. + automatic_taxes: + type: boolean + title: automatic_taxes + description: Whether taxes are calculated automatically for carts in the region. + payment_providers: + type: array + description: The payment providers enabled in the region. + items: + type: string + title: payment_providers + description: A payment provider's ID. + metadata: + type: object + description: The region's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + is_tax_inclusive: + type: boolean + title: is_tax_inclusive + description: Whether the prices in the region are tax inclusive. +x-schemaName: AdminUpdateRegion diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReservation.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReservation.yaml new file mode 100644 index 0000000000000..e2931ef4b8a2b --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReservation.yaml @@ -0,0 +1,22 @@ +type: object +description: The properties to update in the reservation. +properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + quantity: + type: number + title: quantity + description: The reserved quantity. + description: + type: string + title: description + description: The reservation's description. + metadata: + type: object + description: The reservation's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateReservation diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReturnReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReturnReason.yaml index 3299988196d04..9f1309e182f63 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReturnReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateReturnReason.yaml @@ -17,6 +17,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata required: - value - label diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateSalesChannel.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateSalesChannel.yaml index db5e76e560aa0..d1e459242792e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateSalesChannel.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateSalesChannel.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingOption.yaml new file mode 100644 index 0000000000000..8ea9eac5bc0a6 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingOption.yaml @@ -0,0 +1,174 @@ +type: object +description: The properties to update in the shipping option. +properties: + name: + type: string + title: name + description: The shipping option's name. + data: + type: object + description: The shipping option's data that is useful for third-party providers. + externalDocs: + url: >- + https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property + price_type: + type: string + description: > + The type of the shipping option's price. If `calculated`, its price is + retrieved by the associated fulfillment provider during checkout. If + `flat`, its price is set in the `prices` property. + enum: + - calculated + - flat + provider_id: + type: string + title: provider_id + description: >- + The ID of the associated fulfillment provider that is used to process the + option. + shipping_profile_id: + type: string + title: shipping_profile_id + description: The ID of the shipping profile this shipping option belongs to. + type: + type: object + description: The shipping option's type. + required: + - code + - description + - label + properties: + label: + type: string + title: label + description: The type's label. + description: + type: string + title: description + description: The type's description. + code: + type: string + title: code + description: The type's code. + prices: + type: array + description: >- + The shipping option's prices. If the `price_type` is `calculated`, pass an + empty array. + items: + oneOf: + - type: object + description: The shipping option's price for a currency code. + properties: + id: + type: string + title: id + description: The ID of an existing price. + currency_code: + type: string + title: currency_code + description: The price's currency code. + amount: + type: number + title: amount + description: The price's amount. + - type: object + description: The shipping option's price for a region. + properties: + id: + type: string + title: id + description: The ID of an existing price. + region_id: + type: string + title: region_id + description: The ID of the associated region. + amount: + type: number + title: amount + description: The price's amount. + rules: + type: array + description: The shipping option's rules. + items: + oneOf: + - type: object + description: The details of a new shipping option rule. + required: + - operator + - attribute + - value + properties: + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: object + description: Update the properties of an existing rule. + required: + - id + - operator + - attribute + - value + properties: + id: + type: string + title: id + description: The rule's ID. + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 +x-schemaName: AdminUpdateShippingOption diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingProfile.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingProfile.yaml new file mode 100644 index 0000000000000..4102792ddf546 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateShippingProfile.yaml @@ -0,0 +1,18 @@ +type: object +description: The properties to update in the shipping profile. +properties: + name: + type: string + title: name + description: The shipping profile's name. + type: + type: string + title: type + description: The shipping profile's type. + metadata: + type: object + description: The shipping profile's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateShippingProfile diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStockLocation.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStockLocation.yaml index 9d47328396ace..ed608616691d4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStockLocation.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStockLocation.yaml @@ -61,3 +61,6 @@ properties: metadata: type: object description: The stock location's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStore.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStore.yaml index e8c4463b52cdc..734283353bd90 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStore.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateStore.yaml @@ -43,3 +43,6 @@ properties: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRate.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRate.yaml index 8000de42a91c3..e79271cd68c10 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRate.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRate.yaml @@ -48,3 +48,6 @@ properties: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRegion.yaml index 942070922146a..ee6a84b52e812 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateTaxRegion.yaml @@ -13,6 +13,9 @@ properties: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateUser.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateUser.yaml index 32e881a213587..c6011259d72ed 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateUser.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUpdateUser.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/AdminUser.yaml b/www/apps/api-reference/specs/admin/components/schemas/AdminUser.yaml index c3bca505cb895..223368c4c72f0 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/AdminUser.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/AdminUser.yaml @@ -36,6 +36,9 @@ properties: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseCart.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseCart.yaml index 5ba46f61aa6af..c3dd5d86215d5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseCart.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseCart.yaml @@ -73,6 +73,9 @@ properties: metadata: type: object description: The cart's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseCartLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseCartLineItem.yaml index b2e59d962110c..eca6c74f2364e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseCartLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseCartLineItem.yaml @@ -133,6 +133,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseCartShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseCartShippingMethod.yaml index 0a23a60cdc4c2..2601c6fffd7a2 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseCartShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseCartShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseClaimItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseClaimItem.yaml index fe407754bed6c..470709cfa67df 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseClaimItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseClaimItem.yaml @@ -55,6 +55,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseCollection.yaml index 58ab3baab3f94..a038a2eab7753 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseExchangeItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseExchangeItem.yaml index 82c98c6dbf2f4..8fa987469289c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseExchangeItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseExchangeItem.yaml @@ -33,6 +33,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrder.yaml index daa6b8cc669fa..4322ff6393ab8 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrder.yaml @@ -134,6 +134,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderAddress.yaml index 87e754907729f..0b2e3f17d74f6 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderAddress.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderFulfillment.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderFulfillment.yaml index eba0b4539b212..824eb98ba3a74 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderLineItem.yaml index 115a58c2dbe82..279d023e9ca37 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderLineItem.yaml @@ -160,6 +160,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderShippingMethod.yaml index 930352a943340..54df7640adeb8 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderTransaction.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderTransaction.yaml index f822566df95f8..542c5349d2849 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseOrderTransaction.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseOrderTransaction.yaml @@ -46,6 +46,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BasePaymentCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/BasePaymentCollection.yaml index e5c542339e375..47c61f8a0ca03 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BasePaymentCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BasePaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProduct.yaml index 51b5946075787..6b2f40b707bcc 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProduct.yaml @@ -80,6 +80,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductCategory.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductCategory.yaml index b7311e22133fc..589d8220ee1fe 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductCategory.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductCategory.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductImage.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductImage.yaml index 25637c69e893d..cb6694260cecc 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductImage.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductImage.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductOption.yaml index 7964d121fed58..69e0e5a487c03 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductOption.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductOption.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductOptionValue.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductOptionValue.yaml index ba0a4dfd87809..83b476b9eb9e3 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductOptionValue.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductTag.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductTag.yaml index 2f9a58b017962..f39f147ccca62 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductTag.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductTag.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductType.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductType.yaml index 70c15ce3ff314..34600a475fde7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductType.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductType.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseProductVariant.yaml index e27ee0ea8c77c..74d80a6209576 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseProductVariant.yaml @@ -130,3 +130,6 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/BaseRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/BaseRegion.yaml index bbf5758714afc..6e64976df1c67 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/BaseRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/BaseRegion.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The region's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/CreateAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/CreateAddress.yaml index a610a042befa1..238fe6942820e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/CreateAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/CreateAddress.yaml @@ -54,3 +54,6 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/InventoryLevel.yaml b/www/apps/api-reference/specs/admin/components/schemas/InventoryLevel.yaml index aa13bb7243bb5..6107f2104a444 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/InventoryLevel.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/InventoryLevel.yaml @@ -41,3 +41,6 @@ properties: metadata: type: object description: The inventory level's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/Order.yaml b/www/apps/api-reference/specs/admin/components/schemas/Order.yaml index a933e4e94d298..6c331fd686fb2 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/Order.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/Order.yaml @@ -177,6 +177,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata canceled_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderAddress.yaml index f2ca629923d9d..8160d47381cf3 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderAddress.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderChange.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderChange.yaml index 58d07e2b68c64..72b1f74a7b37c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderChange.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderChange.yaml @@ -112,6 +112,9 @@ properties: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderClaim.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderClaim.yaml index a68218b780718..9acfe0de87700 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderClaim.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderClaim.yaml @@ -64,6 +64,9 @@ properties: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderCreditLine.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderCreditLine.yaml index 17b199333be26..e2769840affeb 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderCreditLine.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderCreditLine.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderExchange.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderExchange.yaml index 857b9ed634090..e211bb4ba0e76 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderExchange.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderExchange.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderItem.yaml index ab12ebb4d28b5..98fe9e4853dc9 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderItem.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderLineItem.yaml index 1a0cf29b96a5e..3bf42aa2b2b35 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderLineItem.yaml @@ -141,6 +141,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderReturnItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderReturnItem.yaml index 41fd512de05e7..465be4ee1f7b1 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderReturnItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderReturnItem.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata order_id: type: string title: order_id diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderShippingMethod.yaml index 3aa794be00473..b052eacde5fbf 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/admin/components/schemas/OrderTransaction.yaml b/www/apps/api-reference/specs/admin/components/schemas/OrderTransaction.yaml index d497d2305cd4f..76e5da6dfc612 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/OrderTransaction.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/OrderTransaction.yaml @@ -48,6 +48,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/RefundReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/RefundReason.yaml index 5c681747978e2..fad95d0cdefb3 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/RefundReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/RefundReason.yaml @@ -23,6 +23,9 @@ properties: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/Return.yaml b/www/apps/api-reference/specs/admin/components/schemas/Return.yaml index cfefdef18c517..f0933b5d2c960 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/Return.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/Return.yaml @@ -47,6 +47,9 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartLineItem.yaml index 8ce18622d407c..b967a3087e4de 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartLineItem.yaml @@ -16,3 +16,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartShippingMethods.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartShippingMethods.yaml new file mode 100644 index 0000000000000..494a059031399 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreAddCartShippingMethods.yaml @@ -0,0 +1,19 @@ +type: object +description: The shipping method's details. +required: + - option_id +properties: + option_id: + type: string + title: option_id + description: The ID of the shipping option this method is created from. + data: + type: object + description: >- + Any additional data relevant for the third-party fulfillment provider to + process the shipment. + externalDocs: + url: >- + https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter + description: Learn more about the data parameter. +x-schemaName: StoreAddCartShippingMethods diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCart.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCart.yaml index 6cd3dc8a0d96b..2ff9e3c53a2a7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCart.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCart.yaml @@ -78,6 +78,9 @@ properties: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCartAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCartAddress.yaml index 89963e7b4c39e..6f1f560076403 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCartAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCartAddress.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCartLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCartLineItem.yaml index 824b9623cbf3c..cbb841f1d0ef1 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCartLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCartLineItem.yaml @@ -277,6 +277,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCartShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCartShippingMethod.yaml index 818a670856c64..4cd0151100296 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCartShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCartShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCollection.yaml index 121ef729ec06b..b5f66d0361958 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCart.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCart.yaml index 69fb5ff088f3a..608d8bbea2083 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCart.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCart.yaml @@ -39,3 +39,6 @@ properties: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomer.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomer.yaml index 6821b053399fc..2685212918b71 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomer.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomer.yaml @@ -28,3 +28,6 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomerAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomerAddress.yaml new file mode 100644 index 0000000000000..a68cce7becd25 --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCreateCustomerAddress.yaml @@ -0,0 +1,63 @@ +type: object +description: The address's details. +properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. +x-schemaName: StoreCreateCustomerAddress diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCustomer.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCustomer.yaml index 0c0997ff3f1d5..d079b0a0e7283 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCustomer.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCustomer.yaml @@ -52,6 +52,9 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreCustomerAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreCustomerAddress.yaml index 7c24514a35f8e..1153236620ab7 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreCustomerAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreCustomerAddress.yaml @@ -89,6 +89,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreGiftCardInvitation.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreGiftCardInvitation.yaml deleted file mode 100644 index 8ad2557f70183..0000000000000 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreGiftCardInvitation.yaml +++ /dev/null @@ -1,24 +0,0 @@ -type: object -description: The gift card invitation's details. -x-schemaName: StoreGiftCardInvitation -required: - - id - - email - - status - - gift_card -properties: - id: - type: string - title: id - description: The gift card invitation's ID. - email: - type: string - title: email - description: The gift card invitation's email. - format: email - status: - type: string - title: status - description: The gift card invitation's status. - gift_card: - $ref: ./StoreGiftCard.yaml diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreOrder.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreOrder.yaml index 8235fd5e24746..c4013d4431839 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreOrder.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreOrder.yaml @@ -125,6 +125,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderAddress.yaml index 8c24ea8e12f2a..a960f1c7fbcb4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderAddress.yaml @@ -65,6 +65,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderFulfillment.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderFulfillment.yaml index 4c9e22c359173..ed2f535f34664 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderLineItem.yaml index 556ab17106765..0925dc3195178 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderLineItem.yaml @@ -349,6 +349,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -530,6 +533,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -794,6 +800,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -817,6 +826,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -1016,6 +1028,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1201,6 +1216,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1494,6 +1512,10 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: >- + https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1524,6 +1546,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1878,6 +1903,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -2059,6 +2087,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -2323,6 +2354,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -2346,6 +2380,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -2541,6 +2578,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -2726,6 +2766,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3019,6 +3062,10 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: >- + https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3049,6 +3096,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3295,6 +3345,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3480,6 +3533,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3773,6 +3829,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3803,6 +3862,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3958,6 +4020,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderShippingMethod.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderShippingMethod.yaml index 513ce23b454c2..b2994b98df5ac 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -137,6 +140,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -668,6 +674,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -786,6 +795,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -1313,6 +1325,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -1669,6 +1684,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/admin/components/schemas/StorePaymentCollection.yaml b/www/apps/api-reference/specs/admin/components/schemas/StorePaymentCollection.yaml index b1f572ce80333..bd11eb1da264f 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StorePaymentCollection.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StorePaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProduct.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProduct.yaml index 137cd220c4578..b985c4eef9ed4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProduct.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProduct.yaml @@ -68,6 +68,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductCategory.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductCategory.yaml index 4df135c98f3bf..b1e3dafb316aa 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductCategory.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductCategory.yaml @@ -48,6 +48,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductImage.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductImage.yaml index a3b3b1c72afd1..21e551b763b1e 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductImage.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductImage.yaml @@ -32,6 +32,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductOption.yaml index 40ba71a5d9d22..06b88c40d23c1 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductOption.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductOption.yaml @@ -24,6 +24,9 @@ properties: metadata: type: object description: The option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductOptionValue.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductOptionValue.yaml index 9a754c37c21b0..5a4a747f0a41b 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductOptionValue.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductTag.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductTag.yaml index 1e31fdb2fbde8..cce1492ca6a1a 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductTag.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductTag.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata required: - id - value diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductType.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductType.yaml index ed951b4c1301d..96a52be903b3c 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductType.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductType.yaml @@ -14,6 +14,9 @@ properties: metadata: type: object description: The product type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreProductVariant.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreProductVariant.yaml index 677ad64a10b29..5ee38cc8775d5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreProductVariant.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreProductVariant.yaml @@ -20,6 +20,9 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata id: type: string title: id diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreRegion.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreRegion.yaml index 4f92f9c220030..cd8f654d031b6 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreRegion.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreRegion.yaml @@ -38,6 +38,9 @@ properties: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreReturnItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreReturnItem.yaml index bd4223318cb7d..5b681b1ca4bf1 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreReturnItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreReturnItem.yaml @@ -44,3 +44,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreReturnReason.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreReturnReason.yaml index b08051fb32484..c7b6e9ed0f899 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreReturnReason.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreReturnReason.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreShippingOption.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreShippingOption.yaml index c1e75ff2426a6..c57f31d6d8cf6 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreShippingOption.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreShippingOption.yaml @@ -70,3 +70,6 @@ properties: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreStoreCreditAccount.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreStoreCreditAccount.yaml index 26cf2ddc5b258..44d636dcb555d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreStoreCreditAccount.yaml @@ -49,6 +49,9 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreTransactionGroup.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreTransactionGroup.yaml index 0a7fe807c1252..6ddf0d8bb727d 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreTransactionGroup.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreTransactionGroup.yaml @@ -34,4 +34,7 @@ properties: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata x-schemaName: StoreTransactionGroup diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCartLineItem.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCartLineItem.yaml index 844c2958cea64..dc31e201cc9a5 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCartLineItem.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCartLineItem.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomer.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomer.yaml index 700e28826a85e..8e1fb612468fd 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomer.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomer.yaml @@ -21,3 +21,6 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomerAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomerAddress.yaml new file mode 100644 index 0000000000000..83d98b908bb9b --- /dev/null +++ b/www/apps/api-reference/specs/admin/components/schemas/StoreUpdateCustomerAddress.yaml @@ -0,0 +1,63 @@ +type: object +description: The properties to update in the address. +properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. +x-schemaName: StoreUpdateCustomerAddress diff --git a/www/apps/api-reference/specs/admin/components/schemas/UpdateAddress.yaml b/www/apps/api-reference/specs/admin/components/schemas/UpdateAddress.yaml index 6968ed3b46b90..08c8e679b52f4 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/UpdateAddress.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/UpdateAddress.yaml @@ -60,3 +60,6 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/components/schemas/UpdateCartData.yaml b/www/apps/api-reference/specs/admin/components/schemas/UpdateCartData.yaml index 32376f2c2f892..e7f19ea4968bc 100644 --- a/www/apps/api-reference/specs/admin/components/schemas/UpdateCartData.yaml +++ b/www/apps/api-reference/specs/admin/components/schemas/UpdateCartData.yaml @@ -47,3 +47,6 @@ properties: metadata: type: object description: The cart's metadata, ca hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/admin/openapi.full.yaml b/www/apps/api-reference/specs/admin/openapi.full.yaml index 562339250e7a8..f26ec2a2de70a 100644 --- a/www/apps/api-reference/specs/admin/openapi.full.yaml +++ b/www/apps/api-reference/specs/admin/openapi.full.yaml @@ -15024,7 +15024,7 @@ paths: $ref: '#/components/responses/500_error' x-workflow: deleteDraftOrdersWorkflow x-events: [] - x-version: 2.8.4 + x-since: 2.8.4 /admin/draft-orders/{id}/convert-to-order: post: operationId: PostDraftOrdersIdConvertToOrder @@ -18672,137 +18672,7 @@ paths: content: application/json: schema: - type: object - description: The service zone's details. - required: - - name - properties: - name: - type: string - title: name - description: The service zone's name. - geo_zones: - type: array - description: The service zone's geo zones. - items: - oneOf: - - type: object - description: A country geo zone. - required: - - metadata - - country_code - - type - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: country - - type: object - description: A province geo zone. - required: - - metadata - - country_code - - type - - province_code - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: province - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - - type: object - description: A city geo zone - required: - - metadata - - country_code - - type - - province_code - - city - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: city - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - city: - type: string - title: city - description: The geo zone's city. - - type: object - description: A ZIP geo zone. - required: - - metadata - - country_code - - type - - province_code - - city - - postal_expression - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: zip - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - city: - type: string - title: city - description: The geo zone's city. - postal_expression: - type: object - description: The geo zone's postal expression or ZIP code. + $ref: '#/components/schemas/AdminCreateFulfillmentSetServiceZones' x-codeSamples: - lang: JavaScript label: JS SDK @@ -18977,151 +18847,7 @@ paths: content: application/json: schema: - type: object - description: The service zone's details. - properties: - name: - type: string - title: name - description: The service zone's name. - geo_zones: - type: array - description: The service zone's associated geo zones. - items: - oneOf: - - type: object - description: A country geo zone. - required: - - type - - metadata - - country_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: country - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A province geo zone. - required: - - type - - metadata - - country_code - - province_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: province - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A city geo zone - required: - - type - - metadata - - city - - country_code - - province_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: city - metadata: - type: object - description: The geo zone's metadata. - city: - type: string - title: city - description: The geo zone's city. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A ZIP geo zone. - required: - - type - - metadata - - city - - country_code - - province_code - - postal_expression - properties: - type: - type: string - title: type - description: The geo zone's type. - default: zip - metadata: - type: object - description: The geo zone's metadata. - city: - type: string - title: city - description: The geo zone's city. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: The geo zone's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_expression: - type: object - description: The geo zone's postal expression or ZIP code. - id: - type: string - title: id - description: The ID of an existing geo zone. + $ref: '#/components/schemas/AdminUpdateFulfillmentSetServiceZones' x-codeSamples: - lang: JavaScript label: JS SDK @@ -20998,64 +20724,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the inventory item. - properties: - sku: - type: string - title: sku - description: The inventory item's SKU. - hs_code: - type: string - title: hs_code - description: The inventory item's HS code. - weight: - type: number - title: weight - description: The inventory item's weight. - length: - type: number - title: length - description: The inventory item's length. - height: - type: number - title: height - description: The inventory item's height. - width: - type: number - title: width - description: The inventory item's width. - origin_country: - type: string - title: origin_country - description: The inventory item's origin country. - mid_code: - type: string - title: mid_code - description: The inventory item's MID code. - material: - type: string - title: material - description: The inventory item's material. - title: - type: string - title: title - description: The inventory item's title. - description: - type: string - title: description - description: The inventory item's description. - requires_shipping: - type: boolean - title: requires_shipping - description: Whether the inventory item requires shipping. - thumbnail: - type: string - title: thumbnail - description: The URL of an image to be used as the inventory item's thumbnail. You can use the Upload API routes to upload an image and get its URL. - metadata: - type: object - description: The inventory item's metadata. Can be custom data in key-value pairs. + $ref: '#/components/schemas/AdminUpdateInventoryItem' x-codeSamples: - lang: JavaScript label: JS SDK @@ -21386,23 +21055,7 @@ paths: content: application/json: schema: - type: object - description: The inventory level's details. - required: - - location_id - properties: - location_id: - type: string - title: location_id - description: The ID of the associated location. - stocked_quantity: - type: number - title: stocked_quantity - description: The inventory level's stocked quantity. - incoming_quantity: - type: number - title: incoming_quantity - description: The inventory level's incoming quantity. + $ref: '#/components/schemas/AdminCreateInventoryLocationLevel' x-codeSamples: - lang: Shell label: cURL @@ -21545,17 +21198,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the inventory level. - properties: - stocked_quantity: - type: number - title: stocked_quantity - description: The inventory level's stocked quantity. - incoming_quantity: - type: number - title: incoming_quantity - description: The inventory level's incoming quantity. + $ref: '#/components/schemas/AdminUpdateInventoryLocationLevel' x-codeSamples: - lang: JavaScript label: JS SDK @@ -22368,22 +22011,7 @@ paths: content: application/json: schema: - type: object - description: The details of the user to be created. - properties: - email: - type: string - title: email - description: The user's email. - format: email - first_name: - type: string - title: first_name - description: The user's first name. - last_name: - type: string - title: last_name - description: The user's last name. + $ref: '#/components/schemas/AdminInviteAccept' x-codeSamples: - lang: JavaScript label: JS SDK @@ -23110,7 +22738,7 @@ paths: ``` description: Emitted when an order edit request is canceled. deprecated: false - version: 2.8.0 + since: 2.8.0 /admin/order-edits/{id}/confirm: post: operationId: PostOrderEditsIdConfirm @@ -23184,7 +22812,7 @@ paths: ``` description: Emitted when an order edit request is confirmed. deprecated: false - version: 2.8.0 + since: 2.8.0 /admin/order-edits/{id}/items: post: operationId: PostOrderEditsIdItems @@ -23266,7 +22894,9 @@ paths: operationId: PostOrderEditsIdItemsItemItem_id summary: Update Order Item Quantity of Order Edit x-sidebar-summary: Update Item Quantity - description: Update an existing order item's quantity of an order edit. + description: | + Update an existing order item's quantity of an order edit. + You can also use this API route to remove an item from an order by setting its quantity to `0`. x-authenticated: true parameters: - name: id @@ -23578,7 +23208,7 @@ paths: ``` description: Emitted when an order edit is requested. deprecated: false - version: 2.8.0 + since: 2.8.0 /admin/order-edits/{id}/shipping-method: post: operationId: PostOrderEditsIdShippingMethod @@ -24691,6 +24321,23 @@ paths: - cookie_auth: [] - jwt_token: [] x-codeSamples: + - lang: JavaScript + label: JS SDK + source: |- + 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", + }, + }) + + sdk.admin.order.archive("order_123") + .then(({ order }) => { + console.log(order) + }) - lang: Shell label: cURL source: |- @@ -24939,6 +24586,23 @@ paths: type: object description: Pass additional custom data to the API route. This data is passed to the underlying workflow under the `additional_data` parameter. x-codeSamples: + - lang: JavaScript + label: JS SDK + source: |- + 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", + }, + }) + + sdk.admin.order.complete("order_123") + .then(({ order }) => { + console.log(order) + }) - lang: Shell label: cURL source: |- @@ -26044,20 +25708,7 @@ paths: content: application/json: schema: - type: object - description: The payment collection's details. - required: - - order_id - - amount - properties: - order_id: - type: string - title: order_id - description: The ID of the associated order. - amount: - type: number - title: amount - description: The amount to be paid. + $ref: '#/components/schemas/AdminCreatePaymentCollection' x-codeSamples: - lang: JavaScript label: JS SDK @@ -26205,15 +25856,7 @@ paths: content: application/json: schema: - type: object - description: The payment details. - required: - - order_id - properties: - order_id: - type: string - title: order_id - description: The ID of the order associated with the payment collection. + $ref: '#/components/schemas/AdminMarkPaymentCollectionPaid' x-codeSamples: - lang: JavaScript label: JS SDK @@ -27088,13 +26731,7 @@ paths: content: application/json: schema: - type: object - description: The payment's details. - properties: - amount: - type: number - title: amount - description: The amount to capture. + $ref: '#/components/schemas/AdminCreatePaymentCapture' x-codeSamples: - lang: JavaScript label: JS SDK @@ -27181,21 +26818,7 @@ paths: content: application/json: schema: - type: object - description: The refund's details. - properties: - amount: - type: number - title: amount - description: The amount to refund. - refund_reason_id: - type: string - title: refund_reason_id - description: The ID of a refund reason. - note: - type: string - title: note - description: A note to attach to the refund. + $ref: '#/components/schemas/AdminCreatePaymentRefund' x-codeSamples: - lang: JavaScript label: JS SDK @@ -29517,40 +29140,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the product category. - properties: - name: - type: string - title: name - description: The product category's name. - description: - type: string - title: description - description: The product category's description. - handle: - type: string - title: handle - description: The product category's handle. Must be a unique value. - is_internal: - type: boolean - title: is_internal - description: Whether the product category is only used for internal purposes and shouldn't be shown the customer. - is_active: - type: boolean - title: is_active - description: Whether the product category is active. - parent_category_id: - type: string - title: parent_category_id - description: The ID of a parent category. - metadata: - type: object - description: The product category's metadata. Can hold custom key-value pairs. - rank: - type: number - title: rank - description: The product category's rank among other categories. + $ref: '#/components/schemas/AdminUpdateProductCategory' x-codeSamples: - lang: JavaScript label: JS SDK @@ -30482,16 +30072,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the product tag. - properties: - value: - type: string - title: value - description: The product tag's value. - metadata: - type: object - description: The product tag's metadata. Can hold custom key-value pairs. + $ref: '#/components/schemas/AdminUpdateProductTag' x-codeSamples: - lang: JavaScript label: JS SDK @@ -31324,16 +30905,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the product type. - properties: - value: - type: string - title: value - description: The product type's value. - metadata: - type: object - description: The product type's metadata. Can hold custom key-value pairs. + $ref: '#/components/schemas/AdminUpdateProductType' x-codeSamples: - lang: JavaScript label: JS SDK @@ -33705,7 +33277,7 @@ paths: $ref: '#/components/responses/500_error' x-workflow: importProductsAsChunksWorkflow x-events: [] - x-version: 2.8.5 + x-since: 2.8.5 /admin/products/imports/{transaction_id}/confirm: post: operationId: PostProductsImportsTransaction_idConfirm @@ -33763,7 +33335,7 @@ paths: $ref: '#/components/responses/invalid_request_error' '500': $ref: '#/components/responses/500_error' - x-version: 2.8.5 + x-since: 2.8.5 /admin/products/{id}: get: operationId: GetProductsId @@ -38394,17 +37966,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the refund reason. - properties: - label: - type: string - title: label - description: The refund reason's label. - description: - type: string - title: description - description: The refund reason's description. + $ref: '#/components/schemas/AdminUpdatePaymentRefundReason' x-codeSamples: - lang: Shell label: cURL @@ -39247,42 +38809,7 @@ paths: content: application/json: schema: - type: object - description: The propeties to update in the region. - properties: - name: - type: string - title: name - description: The region's name. - currency_code: - type: string - title: currency_code - description: The region's currency code. - countries: - type: array - description: The region's countries. - items: - type: string - title: countries - description: A country code. - automatic_taxes: - type: boolean - title: automatic_taxes - description: Whether taxes are calculated automatically for carts in the region. - payment_providers: - type: array - description: The payment providers enabled in the region. - items: - type: string - title: payment_providers - description: A payment provider's ID. - metadata: - type: object - description: The region's metadata. Can hold custom key-value pairs. - is_tax_inclusive: - type: boolean - title: is_tax_inclusive - description: Whether the prices in the region are tax inclusive. + $ref: '#/components/schemas/AdminUpdateRegion' x-codeSamples: - lang: JavaScript label: JS SDK @@ -40215,24 +39742,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the reservation. - properties: - location_id: - type: string - title: location_id - description: The ID of the associated location. - quantity: - type: number - title: quantity - description: The reserved quantity. - description: - type: string - title: description - description: The reservation's description. - metadata: - type: object - description: The reservation's metadata. Can hold custom key-value pairs. + $ref: '#/components/schemas/AdminUpdateReservation' x-codeSamples: - lang: JavaScript label: JS SDK @@ -45470,172 +44980,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the shipping option. - properties: - name: - type: string - title: name - description: The shipping option's name. - data: - type: object - description: The shipping option's data that is useful for third-party providers. - externalDocs: - url: https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property - price_type: - type: string - description: | - The type of the shipping option's price. If `calculated`, its price is retrieved by the associated fulfillment provider during checkout. If `flat`, its price is set in the `prices` property. - enum: - - calculated - - flat - provider_id: - type: string - title: provider_id - description: The ID of the associated fulfillment provider that is used to process the option. - shipping_profile_id: - type: string - title: shipping_profile_id - description: The ID of the shipping profile this shipping option belongs to. - type: - type: object - description: The shipping option's type. - required: - - code - - description - - label - properties: - label: - type: string - title: label - description: The type's label. - description: - type: string - title: description - description: The type's description. - code: - type: string - title: code - description: The type's code. - prices: - type: array - description: The shipping option's prices. If the `price_type` is `calculated`, pass an empty array. - items: - oneOf: - - type: object - description: The shipping option's price for a currency code. - properties: - id: - type: string - title: id - description: The ID of an existing price. - currency_code: - type: string - title: currency_code - description: The price's currency code. - amount: - type: number - title: amount - description: The price's amount. - - type: object - description: The shipping option's price for a region. - properties: - id: - type: string - title: id - description: The ID of an existing price. - region_id: - type: string - title: region_id - description: The ID of the associated region. - amount: - type: number - title: amount - description: The price's amount. - rules: - type: array - description: The shipping option's rules. - items: - oneOf: - - type: object - description: The details of a new shipping option rule. - required: - - operator - - attribute - - value - properties: - operator: - type: string - description: The operator used to check whether a rule applies. - enum: - - in - - eq - - ne - - gt - - gte - - lt - - lte - - nin - attribute: - type: string - title: attribute - description: The name of a property or table that the rule applies to. - example: customer_group - value: - oneOf: - - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: array - description: Values of the attribute that enable this rule. - items: - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: object - description: Update the properties of an existing rule. - required: - - id - - operator - - attribute - - value - properties: - id: - type: string - title: id - description: The rule's ID. - operator: - type: string - description: The operator used to check whether a rule applies. - enum: - - in - - eq - - ne - - gt - - gte - - lt - - lte - - nin - attribute: - type: string - title: attribute - description: The name of a property or table that the rule applies to. - example: customer_group - value: - oneOf: - - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: array - description: Values of the attribute that enable this rule. - items: - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 + $ref: '#/components/schemas/AdminUpdateShippingOption' x-codeSamples: - lang: JavaScript label: JS SDK @@ -46609,20 +45954,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the shipping profile. - properties: - name: - type: string - title: name - description: The shipping profile's name. - type: - type: string - title: type - description: The shipping profile's type. - metadata: - type: object - description: The shipping profile's metadata. + $ref: '#/components/schemas/AdminUpdateShippingProfile' x-codeSamples: - lang: JavaScript label: JS SDK @@ -47704,20 +47036,7 @@ paths: content: application/json: schema: - type: object - description: The fulfillment set to create. - required: - - type - - name - properties: - name: - type: string - title: name - description: The fulfillment set's name. - type: - type: string - title: type - description: The fulfillment set's type. + $ref: '#/components/schemas/AdminCreateStockLocationFulfillmentSet' x-codeSamples: - lang: JavaScript label: JS SDK @@ -49637,7 +48956,7 @@ paths: $ref: '#/components/responses/invalid_request_error' '500': $ref: '#/components/responses/500_error' - x-version: 2.8.0 + x-since: 2.8.0 /admin/tax-rates: get: operationId: GetTaxRates @@ -54881,6 +54200,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminAddDraftOrderPromotions: type: object description: The details of the promotions to add to a draft order. @@ -54921,6 +54243,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminApiKey: type: object description: The API key's details. @@ -55511,6 +54836,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id @@ -55597,6 +54925,9 @@ components: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. @@ -55784,6 +55115,9 @@ components: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -55982,6 +55316,9 @@ components: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCollectionDeleteResponse: type: object description: The details of the deleted collection. @@ -56077,15 +55414,18 @@ components: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateFulfillment: type: object description: The filfillment's details. x-schemaName: AdminCreateFulfillment required: - - data - items - metadata - order_id + - data - location_id - provider_id - delivery_address @@ -56150,6 +55490,9 @@ components: metadata: type: object description: The delivery address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata items: type: array description: The items to fulfill. @@ -56246,6 +55589,154 @@ components: metadata: type: object description: The fulfillment's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateFulfillmentSetServiceZones: + type: object + description: The service zone's details. + required: + - name + properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - metadata + - country_code + - type + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: country + - type: object + description: A province geo zone. + required: + - metadata + - country_code + - type + - province_code + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: province + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + - type: object + description: A city geo zone + required: + - metadata + - country_code + - type + - province_code + - city + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: city + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + - type: object + description: A ZIP geo zone. + required: + - metadata + - country_code + - type + - province_code + - city + - postal_expression + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: zip + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + x-schemaName: AdminCreateFulfillmentSetServiceZones AdminCreateGiftCardParams: type: object description: The details of the gift card to create. @@ -56298,6 +55789,9 @@ components: metadata: type: object description: The gift card's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateInventoryItem: type: object description: The inventory item's details. @@ -56358,6 +55852,28 @@ components: metadata: type: object description: The inventory item's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateInventoryLocationLevel: + type: object + description: The inventory level's details. + required: + - location_id + properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. + x-schemaName: AdminCreateInventoryLocationLevel AdminCreateOrderCreditLines: type: object description: The details of a credit line to add to an order. @@ -56385,6 +55901,51 @@ components: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreatePaymentCapture: + type: object + description: The payment's details. + properties: + amount: + type: number + title: amount + description: The amount to capture. + x-schemaName: AdminCreatePaymentCapture + AdminCreatePaymentCollection: + type: object + description: The payment collection's details. + required: + - order_id + - amount + properties: + order_id: + type: string + title: order_id + description: The ID of the associated order. + amount: + type: number + title: amount + description: The amount to be paid. + x-schemaName: AdminCreatePaymentCollection + AdminCreatePaymentRefund: + type: object + description: The refund's details. + properties: + amount: + type: number + title: amount + description: The amount to refund. + refund_reason_id: + type: string + title: refund_reason_id + description: The ID of a refund reason. + note: + type: string + title: note + description: A note to attach to the refund. + x-schemaName: AdminCreatePaymentRefund AdminCreatePriceList: type: object description: The price list's details. @@ -56632,6 +56193,9 @@ components: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id @@ -56678,6 +56242,9 @@ components: metadata: type: object description: The product category's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateProductOption: type: object description: The product option's details. @@ -56711,6 +56278,9 @@ components: metadata: type: object description: The product tag's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateProductType: type: object description: The details of the product type to create. @@ -56721,6 +56291,9 @@ components: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata value: type: string title: value @@ -56800,6 +56373,9 @@ components: metadata: type: object description: The variant's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The variant's prices. @@ -56967,6 +56543,9 @@ components: metadata: type: object description: The region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateReservation: type: object description: The reservation's details. @@ -56999,6 +56578,9 @@ components: metadata: type: object description: The reservation's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateReturnReason: type: object description: The details of the return reason to create. @@ -57026,6 +56608,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateSalesChannel: type: object description: The sales channel's details. @@ -57048,6 +56633,9 @@ components: metadata: type: object description: The sales channel's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateShipment: type: object description: The shipment's details. @@ -57240,6 +56828,9 @@ components: metadata: type: object description: The shipping profile's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateStockLocation: type: object description: The stock location's details. @@ -57260,6 +56851,25 @@ components: metadata: type: object description: The stock location's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateStockLocationFulfillmentSet: + type: object + description: The fulfillment set to create. + required: + - type + - name + properties: + name: + type: string + title: name + description: The fulfillment set's name. + type: + type: string + title: type + description: The fulfillment set's type. + x-schemaName: AdminCreateStockLocationFulfillmentSet AdminCreateStoreCreditAccount: type: object description: The details of the store credit account to create. @@ -57280,6 +56890,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateTaxRate: type: object description: The tax rate's details. @@ -57325,6 +56938,9 @@ components: metadata: type: object description: The tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateTaxRateRule: type: object description: The tax rate rule's details. @@ -57396,9 +57012,15 @@ components: metadata: type: object description: The default tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The tax region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id @@ -57619,6 +57241,9 @@ components: metadata: type: object description: The customer's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by @@ -57730,6 +57355,9 @@ components: metadata: type: object description: The address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -57777,6 +57405,9 @@ components: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -58528,6 +58159,9 @@ components: metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -58884,6 +58518,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -59010,6 +58647,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -59187,6 +58827,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -59387,6 +59030,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -59628,6 +59274,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -59720,6 +59369,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -60425,6 +60077,9 @@ components: metadata: type: object description: The location level's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_item: type: object available_quantity: @@ -60469,6 +60124,9 @@ components: metadata: type: object description: The invite's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -60479,6 +60137,24 @@ components: format: date-time title: updated_at description: The date the invite was updated. + AdminInviteAccept: + type: object + description: The details of the user to be created. + properties: + email: + type: string + title: email + description: The user's email. + format: email + first_name: + type: string + title: first_name + description: The user's first name. + last_name: + type: string + title: last_name + description: The user's last name. + x-schemaName: AdminInviteAccept AdminInviteResponse: type: object description: The invite's details. @@ -60500,6 +60176,17 @@ components: title: remove description: The ID of a product. x-schemaName: AdminLinkPriceListProducts + AdminMarkPaymentCollectionPaid: + type: object + description: The payment details. + required: + - order_id + properties: + order_id: + type: string + title: order_id + description: The ID of the order associated with the payment collection. + x-schemaName: AdminMarkPaymentCollectionPaid AdminNotification: type: object description: The notification's details. @@ -60750,6 +60437,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -60931,6 +60621,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -61054,6 +60747,9 @@ components: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at @@ -61272,6 +60968,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -61495,6 +61194,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -61766,6 +61468,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -61892,6 +61597,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -62069,6 +61777,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -62276,6 +61987,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -62460,6 +62174,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -62700,6 +62417,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostClaimsItemsActionReqSchema: type: object description: The details to update in the item. @@ -62733,6 +62453,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostClaimsShippingReqSchema: type: object description: The details of the shipping method used to ship outbound items. @@ -62759,6 +62482,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesAddItemsReqSchema: type: object description: The details of outbound items. @@ -62797,6 +62523,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesItemsActionReqSchema: type: object description: The details to update in an outbound item. @@ -62830,6 +62559,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesReturnRequestItemsReqSchema: type: object description: The details of the inbound (return) items. @@ -62868,6 +62600,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesShippingActionReqSchema: type: object description: The details of the shipping method to update. @@ -62884,6 +62619,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesShippingReqSchema: type: object description: The outbound shipping method's details. @@ -62910,6 +62648,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderClaimsReqSchema: type: object description: The claim's details. @@ -62943,6 +62684,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsAddItemsReqSchema: type: object description: The details of items to be edited. @@ -62981,6 +62725,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata compare_at_unit_price: type: number title: compare_at_unit_price @@ -63028,6 +62775,9 @@ components: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsShippingActionReqSchema: type: object description: The shipping method's details. @@ -63044,6 +62794,9 @@ components: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsShippingReqSchema: type: object description: The shipping method's details. @@ -63070,6 +62823,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsUpdateItemQuantityReqSchema: type: object description: The order item's details to update. @@ -63115,6 +62871,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReceiveReturnsReqSchema: type: object description: The return receival details. @@ -63131,6 +62890,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsConfirmRequestReqSchema: type: object description: The confirmation's details. @@ -63227,6 +62989,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsRequestItemsActionReqSchema: type: object description: The details to update in the item. @@ -63247,6 +63012,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsRequestItemsReqSchema: type: object description: The items' details. @@ -63285,6 +63053,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsReturnReqSchema: type: object description: The return's details. @@ -63301,6 +63072,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsShippingActionReqSchema: type: object description: The shipping method's details. @@ -63317,6 +63091,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsShippingReqSchema: type: object description: The shipping method's details. @@ -63343,6 +63120,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPrice: type: object description: The price's details. @@ -63857,6 +63637,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -63985,6 +63768,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -64136,6 +63922,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -64174,6 +63963,9 @@ components: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -64247,6 +64039,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -64307,6 +64102,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminProductTagDeleteResponse: type: object description: The details of the product tag deletion. @@ -64406,6 +64204,9 @@ components: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminProductTypeDeleteResponse: type: object description: The details of the product type deletion. @@ -64604,6 +64405,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_items: type: array description: The variant's inventory items. @@ -65018,6 +64822,9 @@ components: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -65067,6 +64874,9 @@ components: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -65180,6 +64990,9 @@ components: metadata: type: object description: The reservation's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by @@ -65332,6 +65145,9 @@ components: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminReturnPreviewResponse: type: object description: The details of a return and a preview of the order once the return is applied. @@ -65374,6 +65190,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -65546,6 +65365,9 @@ components: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -65759,6 +65581,9 @@ components: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -65983,8 +65808,18 @@ components: - gte - nin value: - type: string - title: value + oneOf: + - type: string + title: value + description: The shipping option rule's value. + example: 'true' + - type: array + description: The shipping option rule's values. + items: + type: string + title: value + description: A value of the shipping option rule. + example: 'true' shipping_option_id: type: string title: shipping_option_id @@ -66073,6 +65908,9 @@ components: metadata: type: object description: The shipping profile's metadata, holds custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -66327,6 +66165,9 @@ components: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -66389,6 +66230,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -66586,6 +66430,9 @@ components: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_region_id: type: string title: tax_region_id @@ -66713,6 +66560,9 @@ components: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata parent_id: type: string title: parent_id @@ -66833,6 +66683,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -66881,6 +66734,9 @@ components: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminTransactionGroupsResponse: type: object description: The paginated list of transaction groups. @@ -66987,6 +66843,9 @@ components: metadata: type: object description: The customer group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateDraftOrder: type: object description: The data to update in the draft order. @@ -67049,6 +66908,9 @@ components: metadata: type: object description: The shipping address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The draft order's billing address. @@ -67101,9 +66963,15 @@ components: metadata: type: object description: The billing address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata customer_id: type: string title: customer_id @@ -67138,6 +67006,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateDraftOrderItem: type: object description: The updates to make on a draft order's item. @@ -67178,6 +67049,165 @@ components: type: string title: internal_note description: A note viewed only by admin users about the shipping method. + AdminUpdateFulfillmentSetServiceZones: + type: object + description: The service zone's details. + properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's associated geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - type + - metadata + - country_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: country + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A province geo zone. + required: + - type + - metadata + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: province + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A city geo zone + required: + - type + - metadata + - city + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: city + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A ZIP geo zone. + required: + - type + - metadata + - city + - country_code + - province_code + - postal_expression + properties: + type: + type: string + title: type + description: The geo zone's type. + default: zip + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + id: + type: string + title: id + description: The ID of an existing geo zone. + x-schemaName: AdminUpdateFulfillmentSetServiceZones AdminUpdateGiftCardParams: type: object description: The details to update in the gift card. @@ -67213,6 +67243,85 @@ components: metadata: type: object description: The gift card's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdateInventoryItem: + type: object + description: The properties to update in the inventory item. + properties: + sku: + type: string + title: sku + description: The inventory item's SKU. + hs_code: + type: string + title: hs_code + description: The inventory item's HS code. + weight: + type: number + title: weight + description: The inventory item's weight. + length: + type: number + title: length + description: The inventory item's length. + height: + type: number + title: height + description: The inventory item's height. + width: + type: number + title: width + description: The inventory item's width. + origin_country: + type: string + title: origin_country + description: The inventory item's origin country. + mid_code: + type: string + title: mid_code + description: The inventory item's MID code. + material: + type: string + title: material + description: The inventory item's material. + title: + type: string + title: title + description: The inventory item's title. + description: + type: string + title: description + description: The inventory item's description. + requires_shipping: + type: boolean + title: requires_shipping + description: Whether the inventory item requires shipping. + thumbnail: + type: string + title: thumbnail + description: The URL of an image to be used as the inventory item's thumbnail. You can use the Upload API routes to upload an image and get its URL. + metadata: + type: object + description: The inventory item's metadata. Can be custom data in key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateInventoryItem + AdminUpdateInventoryLocationLevel: + type: object + description: The properties to update in the inventory level. + properties: + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. + x-schemaName: AdminUpdateInventoryLocationLevel AdminUpdateOrder: type: object description: The details to update in the order. @@ -67275,6 +67384,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The order's billing address. @@ -67327,9 +67439,28 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdatePaymentRefundReason: + type: object + description: The properties to update in the refund reason. + properties: + label: + type: string + title: label + description: The refund reason's label. + description: + type: string + title: description + description: The refund reason's description. + x-schemaName: AdminUpdatePaymentRefundReason AdminUpdatePriceList: type: object description: the details to update in a price list. @@ -67536,10 +67667,52 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id description: The ID of the product in an external or third-party system. + AdminUpdateProductCategory: + type: object + description: The properties to update in the product category. + properties: + name: + type: string + title: name + description: The product category's name. + description: + type: string + title: description + description: The product category's description. + handle: + type: string + title: handle + description: The product category's handle. Must be a unique value. + is_internal: + type: boolean + title: is_internal + description: Whether the product category is only used for internal purposes and shouldn't be shown the customer. + is_active: + type: boolean + title: is_active + description: Whether the product category is active. + parent_category_id: + type: string + title: parent_category_id + description: The ID of a parent category. + metadata: + type: object + description: The product category's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + rank: + type: number + title: rank + description: The product category's rank among other categories. + x-schemaName: AdminUpdateProductCategory AdminUpdateProductOption: type: object description: The details to update in a product option. @@ -67556,6 +67729,36 @@ components: type: string title: values description: An option value. + AdminUpdateProductTag: + type: object + description: The properties to update in the product tag. + properties: + value: + type: string + title: value + description: The product tag's value. + metadata: + type: object + description: The product tag's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateProductTag + AdminUpdateProductType: + type: object + description: The properties to update in the product type. + properties: + value: + type: string + title: value + description: The product type's value. + metadata: + type: object + description: The product type's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateProductType AdminUpdateProductVariant: type: object description: The properties to update of a product variant. @@ -67628,6 +67831,9 @@ components: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. @@ -67681,6 +67887,70 @@ components: description: An attribute value. example: prod_123 x-schemaName: AdminUpdatePromotionRule + AdminUpdateRegion: + type: object + description: The propeties to update in the region. + properties: + name: + type: string + title: name + description: The region's name. + currency_code: + type: string + title: currency_code + description: The region's currency code. + countries: + type: array + description: The region's countries. + items: + type: string + title: countries + description: A country code. + automatic_taxes: + type: boolean + title: automatic_taxes + description: Whether taxes are calculated automatically for carts in the region. + payment_providers: + type: array + description: The payment providers enabled in the region. + items: + type: string + title: payment_providers + description: A payment provider's ID. + metadata: + type: object + description: The region's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + is_tax_inclusive: + type: boolean + title: is_tax_inclusive + description: Whether the prices in the region are tax inclusive. + x-schemaName: AdminUpdateRegion + AdminUpdateReservation: + type: object + description: The properties to update in the reservation. + properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + quantity: + type: number + title: quantity + description: The reserved quantity. + description: + type: string + title: description + description: The reservation's description. + metadata: + type: object + description: The reservation's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateReservation AdminUpdateReturnReason: type: object description: The details to update in a return reason. @@ -67701,6 +67971,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata required: - value - label @@ -67724,6 +67997,177 @@ components: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdateShippingOption: + type: object + description: The properties to update in the shipping option. + properties: + name: + type: string + title: name + description: The shipping option's name. + data: + type: object + description: The shipping option's data that is useful for third-party providers. + externalDocs: + url: https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property + price_type: + type: string + description: | + The type of the shipping option's price. If `calculated`, its price is retrieved by the associated fulfillment provider during checkout. If `flat`, its price is set in the `prices` property. + enum: + - calculated + - flat + provider_id: + type: string + title: provider_id + description: The ID of the associated fulfillment provider that is used to process the option. + shipping_profile_id: + type: string + title: shipping_profile_id + description: The ID of the shipping profile this shipping option belongs to. + type: + type: object + description: The shipping option's type. + required: + - code + - description + - label + properties: + label: + type: string + title: label + description: The type's label. + description: + type: string + title: description + description: The type's description. + code: + type: string + title: code + description: The type's code. + prices: + type: array + description: The shipping option's prices. If the `price_type` is `calculated`, pass an empty array. + items: + oneOf: + - type: object + description: The shipping option's price for a currency code. + properties: + id: + type: string + title: id + description: The ID of an existing price. + currency_code: + type: string + title: currency_code + description: The price's currency code. + amount: + type: number + title: amount + description: The price's amount. + - type: object + description: The shipping option's price for a region. + properties: + id: + type: string + title: id + description: The ID of an existing price. + region_id: + type: string + title: region_id + description: The ID of the associated region. + amount: + type: number + title: amount + description: The price's amount. + rules: + type: array + description: The shipping option's rules. + items: + oneOf: + - type: object + description: The details of a new shipping option rule. + required: + - operator + - attribute + - value + properties: + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: object + description: Update the properties of an existing rule. + required: + - id + - operator + - attribute + - value + properties: + id: + type: string + title: id + description: The rule's ID. + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + x-schemaName: AdminUpdateShippingOption AdminUpdateShippingOptionRule: type: object description: The properties to update in the shipping option rule. @@ -67768,6 +68212,25 @@ components: description: A value of the attribute that enables this rule. example: cusgroup_123 x-schemaName: AdminUpdateShippingOptionRule + AdminUpdateShippingProfile: + type: object + description: The properties to update in the shipping profile. + properties: + name: + type: string + title: name + description: The shipping profile's name. + type: + type: string + title: type + description: The shipping profile's type. + metadata: + type: object + description: The shipping profile's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateShippingProfile AdminUpdateStockLocation: type: object description: The properties to update in a stock location. @@ -67828,6 +68291,9 @@ components: metadata: type: object description: The stock location's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateStore: type: object description: The properties to update in a store. @@ -67874,6 +68340,9 @@ components: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateTaxRate: type: object description: The properties to update in the tax rate. @@ -67924,6 +68393,9 @@ components: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateTaxRegion: type: object description: The details to update in a tax region. @@ -67940,6 +68412,9 @@ components: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id @@ -67964,6 +68439,9 @@ components: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateVariantInventoryItem: type: object description: The properties to update of the variant's inventory item association. @@ -68125,6 +68603,9 @@ components: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -68693,6 +69174,9 @@ components: metadata: type: object description: The cart's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -68931,6 +69415,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -69047,6 +69534,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -69157,6 +69647,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -69215,6 +69708,9 @@ components: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseExchangeItem: type: object description: The item's details. @@ -69251,6 +69747,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -69534,6 +70033,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -69706,6 +70208,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -69779,6 +70284,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -70032,6 +70540,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -70311,6 +70822,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -70572,6 +71086,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -70712,6 +71229,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -70891,6 +71411,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -71017,6 +71540,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -71083,6 +71609,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -71121,6 +71650,9 @@ components: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -71161,6 +71693,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -71212,6 +71747,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseProductType: type: object description: The product type's details. @@ -71248,6 +71786,9 @@ components: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseProductVariant: type: object description: The product variant's details. @@ -71377,6 +71918,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BasePromotionRuleValue: type: object description: The rule value's details. @@ -71469,6 +72013,9 @@ components: metadata: type: object description: The region's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -71787,6 +72334,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata CustomerGroupInCustomerFilters: type: object description: Filter by customer groups to get their associated customers. @@ -72243,6 +72793,9 @@ components: metadata: type: object description: The inventory level's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata Order: type: object description: The order change's order. @@ -72421,6 +72974,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata canceled_at: type: string format: date-time @@ -72615,6 +73171,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -72738,6 +73297,9 @@ components: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at @@ -72929,6 +73491,9 @@ components: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73015,6 +73580,9 @@ components: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73087,6 +73655,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73200,6 +73771,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73354,6 +73928,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -73572,6 +74149,9 @@ components: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata order_id: type: string title: order_id @@ -73647,6 +74227,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -73867,6 +74450,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73909,6 +74495,9 @@ components: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -73978,6 +74567,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -74082,6 +74674,26 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreAddCartShippingMethods: + type: object + description: The shipping method's details. + required: + - option_id + properties: + option_id: + type: string + title: option_id + description: The ID of the shipping option this method is created from. + data: + type: object + description: Any additional data relevant for the third-party fulfillment provider to process the shipment. + externalDocs: + url: https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter + description: Learn more about the data parameter. + x-schemaName: StoreAddCartShippingMethods StoreAddGiftCardToCart: type: object description: The details to add a gift card to the cart. @@ -74306,6 +74918,9 @@ components: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -74488,6 +75103,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at @@ -74778,6 +75396,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at @@ -74958,6 +75579,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -75301,6 +75925,9 @@ components: metadata: type: object description: The collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreCollectionResponse: type: object description: The collection's details. @@ -75345,6 +75972,9 @@ components: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreCreateCustomer: type: object description: The details of the customer to create. @@ -75376,6 +76006,73 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreCreateCustomerAddress: + type: object + description: The address's details. + properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. + x-schemaName: StoreCreateCustomerAddress StoreCreatePaymentCollection: type: object description: The details of the payment collection to create. @@ -75609,6 +76306,9 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -75716,6 +76416,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -75865,31 +76568,6 @@ components: format: date-time title: updated_at description: The date the gift card was updated. - StoreGiftCardInvitation: - type: object - description: The gift card invitation's details. - x-schemaName: StoreGiftCardInvitation - required: - - id - - email - - status - - gift_card - properties: - id: - type: string - title: id - description: The gift card invitation's ID. - email: - type: string - title: email - description: The gift card invitation's email. - format: email - status: - type: string - title: status - description: The gift card invitation's status. - gift_card: - $ref: '#/components/schemas/StoreGiftCard' StoreGiftCardResponse: type: object description: The gift card's details. @@ -76045,6 +76723,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -76226,6 +76907,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -76299,6 +76983,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -76665,6 +77352,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -76846,6 +77536,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -77110,6 +77803,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -77133,6 +77829,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -77332,6 +78031,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -77517,6 +78219,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -77810,6 +78515,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -77840,6 +78548,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -78194,6 +78905,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -78375,6 +79089,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -78639,6 +79356,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -78662,6 +79382,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -78857,6 +79580,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -79042,6 +79768,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -79335,6 +80064,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -79365,6 +80097,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -79611,6 +80346,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -79796,6 +80534,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -80089,6 +80830,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -80119,6 +80863,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -80274,6 +81021,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -80399,6 +81149,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -80476,6 +81229,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -81007,6 +81763,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -81125,6 +81884,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -81652,6 +82414,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -82008,6 +82773,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -82104,6 +82872,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -82346,6 +83117,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -82489,6 +83263,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -82585,6 +83362,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -82616,6 +83396,9 @@ components: metadata: type: object description: The option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -82659,6 +83442,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -82714,6 +83500,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata required: - id - value @@ -82777,6 +83566,9 @@ components: metadata: type: object description: The product type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -82860,6 +83652,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata id: type: string title: id @@ -83011,6 +83806,9 @@ components: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -83190,6 +83988,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreReturnReason: type: object description: The return reason's details. @@ -83220,6 +84021,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -83317,6 +84121,9 @@ components: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreShippingOptionListResponse: type: object description: The shipping option's details. @@ -83439,6 +84246,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -83527,6 +84337,9 @@ components: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata x-schemaName: StoreTransactionGroup StoreUpdateCartLineItem: type: object @@ -83542,6 +84355,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreUpdateCustomer: type: object description: The details to update in the customer. @@ -83566,6 +84382,73 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreUpdateCustomerAddress: + type: object + description: The properties to update in the address. + properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. + x-schemaName: StoreUpdateCustomerAddress UpdateAddress: type: object description: The details to update in the address. @@ -83629,6 +84512,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata UpdateCartData: type: object description: The details to update in a cart. @@ -83675,6 +84561,9 @@ components: metadata: type: object description: The cart's metadata, ca hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata WorkflowExecutionContext: type: object description: The workflow execution's context. diff --git a/www/apps/api-reference/specs/admin/paths/admin_draft-orders_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_draft-orders_{id}.yaml index 4ebfa8884d5e8..54a4c6573c5db 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_draft-orders_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_draft-orders_{id}.yaml @@ -199,4 +199,4 @@ delete: $ref: ../components/responses/500_error.yaml x-workflow: deleteDraftOrdersWorkflow x-events: [] - x-version: 2.8.4 + x-since: 2.8.4 diff --git a/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones.yaml b/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones.yaml index 3da6aa1e26290..3d99e4647c97b 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones.yaml @@ -37,143 +37,7 @@ post: content: application/json: schema: - type: object - description: The service zone's details. - required: - - name - properties: - name: - type: string - title: name - description: The service zone's name. - geo_zones: - type: array - description: The service zone's geo zones. - items: - oneOf: - - type: object - description: A country geo zone. - required: - - metadata - - country_code - - type - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: country - - type: object - description: A province geo zone. - required: - - metadata - - country_code - - type - - province_code - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: province - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - - type: object - description: A city geo zone - required: - - metadata - - country_code - - type - - province_code - - city - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: city - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - city: - type: string - title: city - description: The geo zone's city. - - type: object - description: A ZIP geo zone. - required: - - metadata - - country_code - - type - - province_code - - city - - postal_expression - properties: - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - type: - type: string - title: type - description: The geo zone's type. - default: zip - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - city: - type: string - title: city - description: The geo zone's city. - postal_expression: - type: object - description: The geo zone's postal expression or ZIP code. + $ref: ../components/schemas/AdminCreateFulfillmentSetServiceZones.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones_{zone_id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones_{zone_id}.yaml index 019ccd54fd7aa..2bf0c07070859 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones_{zone_id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_fulfillment-sets_{id}_service-zones_{zone_id}.yaml @@ -116,157 +116,7 @@ post: content: application/json: schema: - type: object - description: The service zone's details. - properties: - name: - type: string - title: name - description: The service zone's name. - geo_zones: - type: array - description: The service zone's associated geo zones. - items: - oneOf: - - type: object - description: A country geo zone. - required: - - type - - metadata - - country_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: country - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A province geo zone. - required: - - type - - metadata - - country_code - - province_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: province - metadata: - type: object - description: The geo zone's metadata. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A city geo zone - required: - - type - - metadata - - city - - country_code - - province_code - properties: - type: - type: string - title: type - description: The geo zone's type. - default: city - metadata: - type: object - description: The geo zone's metadata. - city: - type: string - title: city - description: The geo zone's city. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - id: - type: string - title: id - description: The ID of an existing geo zone. - - type: object - description: A ZIP geo zone. - required: - - type - - metadata - - city - - country_code - - province_code - - postal_expression - properties: - type: - type: string - title: type - description: The geo zone's type. - default: zip - metadata: - type: object - description: The geo zone's metadata. - city: - type: string - title: city - description: The geo zone's city. - country_code: - type: string - title: country_code - description: The geo zone's country code. - province_code: - type: string - title: province_code - description: >- - The geo zone's ISO 3166-2 province code. Must be - lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_expression: - type: object - description: The geo zone's postal expression or ZIP code. - id: - type: string - title: id - description: The ID of an existing geo zone. + $ref: ../components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}.yaml index 8d83535eeb41f..31cae6980ceb7 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}.yaml @@ -102,69 +102,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the inventory item. - properties: - sku: - type: string - title: sku - description: The inventory item's SKU. - hs_code: - type: string - title: hs_code - description: The inventory item's HS code. - weight: - type: number - title: weight - description: The inventory item's weight. - length: - type: number - title: length - description: The inventory item's length. - height: - type: number - title: height - description: The inventory item's height. - width: - type: number - title: width - description: The inventory item's width. - origin_country: - type: string - title: origin_country - description: The inventory item's origin country. - mid_code: - type: string - title: mid_code - description: The inventory item's MID code. - material: - type: string - title: material - description: The inventory item's material. - title: - type: string - title: title - description: The inventory item's title. - description: - type: string - title: description - description: The inventory item's description. - requires_shipping: - type: boolean - title: requires_shipping - description: Whether the inventory item requires shipping. - thumbnail: - type: string - title: thumbnail - description: >- - The URL of an image to be used as the inventory item's - thumbnail. You can use the Upload API routes to upload an image - and get its URL. - metadata: - type: object - description: >- - The inventory item's metadata. Can be custom data in key-value - pairs. + $ref: ../components/schemas/AdminUpdateInventoryItem.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels.yaml b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels.yaml index 98286d0eb8e04..515f588c7a16a 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels.yaml @@ -200,23 +200,7 @@ post: content: application/json: schema: - type: object - description: The inventory level's details. - required: - - location_id - properties: - location_id: - type: string - title: location_id - description: The ID of the associated location. - stocked_quantity: - type: number - title: stocked_quantity - description: The inventory level's stocked quantity. - incoming_quantity: - type: number - title: incoming_quantity - description: The inventory level's incoming quantity. + $ref: ../components/schemas/AdminCreateInventoryLocationLevel.yaml x-codeSamples: - lang: Shell label: cURL diff --git a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels_{location_id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels_{location_id}.yaml index bd364f4e4549e..69ebdf89c3c7d 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels_{location_id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_inventory-items_{id}_location-levels_{location_id}.yaml @@ -45,17 +45,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the inventory level. - properties: - stocked_quantity: - type: number - title: stocked_quantity - description: The inventory level's stocked quantity. - incoming_quantity: - type: number - title: incoming_quantity - description: The inventory level's incoming quantity. + $ref: ../components/schemas/AdminUpdateInventoryLocationLevel.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_invites_accept.yaml b/www/apps/api-reference/specs/admin/paths/admin_invites_accept.yaml index 135ff939397ea..6424a2372edf3 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_invites_accept.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_invites_accept.yaml @@ -13,22 +13,7 @@ post: content: application/json: schema: - type: object - description: The details of the user to be created. - properties: - email: - type: string - title: email - description: The user's email. - format: email - first_name: - type: string - title: first_name - description: The user's first name. - last_name: - type: string - title: last_name - description: The user's last name. + $ref: ../components/schemas/AdminInviteAccept.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}.yaml index 15b2c1bbec284..c87f143eaf5f3 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}.yaml @@ -75,4 +75,4 @@ delete: ``` description: Emitted when an order edit request is canceled. deprecated: false - version: 2.8.0 + since: 2.8.0 diff --git a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_confirm.yaml b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_confirm.yaml index ab4354f3f172d..9e3fbf4eed652 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_confirm.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_confirm.yaml @@ -56,4 +56,4 @@ post: ``` description: Emitted when an order edit request is confirmed. deprecated: false - version: 2.8.0 + since: 2.8.0 diff --git a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_items_item_{item_id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_items_item_{item_id}.yaml index 6babc1f0c9b1f..6fafe9dc0cf7b 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_items_item_{item_id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_items_item_{item_id}.yaml @@ -2,7 +2,11 @@ post: operationId: PostOrderEditsIdItemsItemItem_id summary: Update Order Item Quantity of Order Edit x-sidebar-summary: Update Item Quantity - description: Update an existing order item's quantity of an order edit. + description: > + Update an existing order item's quantity of an order edit. + + You can also use this API route to remove an item from an order by setting + its quantity to `0`. x-authenticated: true parameters: - name: id diff --git a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_request.yaml b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_request.yaml index 3cd6900e90537..4bd840e6429bb 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_request.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_order-edits_{id}_request.yaml @@ -56,4 +56,4 @@ post: ``` description: Emitted when an order edit is requested. deprecated: false - version: 2.8.0 + since: 2.8.0 diff --git a/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_archive.yaml b/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_archive.yaml index 0c08fae18a448..d12e658360893 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_archive.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_archive.yaml @@ -33,6 +33,10 @@ post: - cookie_auth: [] - jwt_token: [] x-codeSamples: + - lang: JavaScript + label: JS SDK + source: + $ref: ../code_samples/JavaScript/admin_orders_{id}_archive/post.js - lang: Shell label: cURL source: diff --git a/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_complete.yaml b/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_complete.yaml index 6725b18ee838c..a316bd20ef711 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_complete.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_orders_{id}_complete.yaml @@ -48,6 +48,10 @@ post: passed to the underlying workflow under the `additional_data` parameter. x-codeSamples: + - lang: JavaScript + label: JS SDK + source: + $ref: ../code_samples/JavaScript/admin_orders_{id}_complete/post.js - lang: Shell label: cURL source: diff --git a/www/apps/api-reference/specs/admin/paths/admin_payment-collections.yaml b/www/apps/api-reference/specs/admin/paths/admin_payment-collections.yaml index f74971b754524..427f23e368f68 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_payment-collections.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_payment-collections.yaml @@ -30,20 +30,7 @@ post: content: application/json: schema: - type: object - description: The payment collection's details. - required: - - order_id - - amount - properties: - order_id: - type: string - title: order_id - description: The ID of the associated order. - amount: - type: number - title: amount - description: The amount to be paid. + $ref: ../components/schemas/AdminCreatePaymentCollection.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_payment-collections_{id}_mark-as-paid.yaml b/www/apps/api-reference/specs/admin/paths/admin_payment-collections_{id}_mark-as-paid.yaml index f332a861585e4..9665f256ffea1 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_payment-collections_{id}_mark-as-paid.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_payment-collections_{id}_mark-as-paid.yaml @@ -39,15 +39,7 @@ post: content: application/json: schema: - type: object - description: The payment details. - required: - - order_id - properties: - order_id: - type: string - title: order_id - description: The ID of the order associated with the payment collection. + $ref: ../components/schemas/AdminMarkPaymentCollectionPaid.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_capture.yaml b/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_capture.yaml index 5453b7d0e4842..7bf7b71e8db5a 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_capture.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_capture.yaml @@ -38,13 +38,7 @@ post: content: application/json: schema: - type: object - description: The payment's details. - properties: - amount: - type: number - title: amount - description: The amount to capture. + $ref: ../components/schemas/AdminCreatePaymentCapture.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_refund.yaml b/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_refund.yaml index 517a84f5865b1..58c7d685b7b95 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_refund.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_payments_{id}_refund.yaml @@ -38,21 +38,7 @@ post: content: application/json: schema: - type: object - description: The refund's details. - properties: - amount: - type: number - title: amount - description: The amount to refund. - refund_reason_id: - type: string - title: refund_reason_id - description: The ID of a refund reason. - note: - type: string - title: note - description: A note to attach to the refund. + $ref: ../components/schemas/AdminCreatePaymentRefund.yaml x-codeSamples: - lang: JavaScript label: JS SDK 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 637ef1e5dcbb3..bc54617716289 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 @@ -158,44 +158,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the product category. - properties: - name: - type: string - title: name - description: The product category's name. - description: - type: string - title: description - description: The product category's description. - handle: - type: string - title: handle - description: The product category's handle. Must be a unique value. - is_internal: - type: boolean - title: is_internal - description: >- - Whether the product category is only used for internal purposes - and shouldn't be shown the customer. - is_active: - type: boolean - title: is_active - description: Whether the product category is active. - parent_category_id: - type: string - title: parent_category_id - description: The ID of a parent category. - metadata: - type: object - description: >- - The product category's metadata. Can hold custom key-value - pairs. - rank: - type: number - title: rank - description: The product category's rank among other categories. + $ref: ../components/schemas/AdminUpdateProductCategory.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_product-tags_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_product-tags_{id}.yaml index 2fdce9541eb84..c09775153b6a3 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_product-tags_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_product-tags_{id}.yaml @@ -102,16 +102,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the product tag. - properties: - value: - type: string - title: value - description: The product tag's value. - metadata: - type: object - description: The product tag's metadata. Can hold custom key-value pairs. + $ref: ../components/schemas/AdminUpdateProductTag.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_product-types_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_product-types_{id}.yaml index ec42e03631cf9..118ff77434ee5 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_product-types_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_product-types_{id}.yaml @@ -102,16 +102,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the product type. - properties: - value: - type: string - title: value - description: The product type's value. - metadata: - type: object - description: The product type's metadata. Can hold custom key-value pairs. + $ref: ../components/schemas/AdminUpdateProductType.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_products_imports.yaml b/www/apps/api-reference/specs/admin/paths/admin_products_imports.yaml index 334d557533cca..df513bcd925fb 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_products_imports.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_products_imports.yaml @@ -47,4 +47,4 @@ post: $ref: ../components/responses/500_error.yaml x-workflow: importProductsAsChunksWorkflow x-events: [] - x-version: 2.8.5 + x-since: 2.8.5 diff --git a/www/apps/api-reference/specs/admin/paths/admin_products_imports_{transaction_id}_confirm.yaml b/www/apps/api-reference/specs/admin/paths/admin_products_imports_{transaction_id}_confirm.yaml index 31519e0b05495..41c936e2369f7 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_products_imports_{transaction_id}_confirm.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_products_imports_{transaction_id}_confirm.yaml @@ -44,4 +44,4 @@ post: $ref: ../components/responses/invalid_request_error.yaml '500': $ref: ../components/responses/500_error.yaml - x-version: 2.8.5 + x-since: 2.8.5 diff --git a/www/apps/api-reference/specs/admin/paths/admin_refund-reasons_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_refund-reasons_{id}.yaml index d216aa377d201..8b6034cd94b70 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_refund-reasons_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_refund-reasons_{id}.yaml @@ -98,17 +98,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the refund reason. - properties: - label: - type: string - title: label - description: The refund reason's label. - description: - type: string - title: description - description: The refund reason's description. + $ref: ../components/schemas/AdminUpdatePaymentRefundReason.yaml x-codeSamples: - lang: Shell label: cURL diff --git a/www/apps/api-reference/specs/admin/paths/admin_regions_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_regions_{id}.yaml index 6f0c64fd47ba9..1e69b5b7c0da9 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_regions_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_regions_{id}.yaml @@ -102,44 +102,7 @@ post: content: application/json: schema: - type: object - description: The propeties to update in the region. - properties: - name: - type: string - title: name - description: The region's name. - currency_code: - type: string - title: currency_code - description: The region's currency code. - countries: - type: array - description: The region's countries. - items: - type: string - title: countries - description: A country code. - automatic_taxes: - type: boolean - title: automatic_taxes - description: >- - Whether taxes are calculated automatically for carts in the - region. - payment_providers: - type: array - description: The payment providers enabled in the region. - items: - type: string - title: payment_providers - description: A payment provider's ID. - metadata: - type: object - description: The region's metadata. Can hold custom key-value pairs. - is_tax_inclusive: - type: boolean - title: is_tax_inclusive - description: Whether the prices in the region are tax inclusive. + $ref: ../components/schemas/AdminUpdateRegion.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_reservations_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_reservations_{id}.yaml index 6d2ae3d7ceba4..69e108ecef487 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_reservations_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_reservations_{id}.yaml @@ -102,24 +102,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the reservation. - properties: - location_id: - type: string - title: location_id - description: The ID of the associated location. - quantity: - type: number - title: quantity - description: The reserved quantity. - description: - type: string - title: description - description: The reservation's description. - metadata: - type: object - description: The reservation's metadata. Can hold custom key-value pairs. + $ref: ../components/schemas/AdminUpdateReservation.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_shipping-options_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_shipping-options_{id}.yaml index e7ffa03c096e2..aed6317bea50b 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_shipping-options_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_shipping-options_{id}.yaml @@ -104,186 +104,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the shipping option. - properties: - name: - type: string - title: name - description: The shipping option's name. - data: - type: object - description: >- - The shipping option's data that is useful for third-party - providers. - externalDocs: - url: >- - https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property - price_type: - type: string - description: > - The type of the shipping option's price. If `calculated`, its - price is retrieved by the associated fulfillment provider - during checkout. If `flat`, its price is set in the `prices` - property. - enum: - - calculated - - flat - provider_id: - type: string - title: provider_id - description: >- - The ID of the associated fulfillment provider that is used to - process the option. - shipping_profile_id: - type: string - title: shipping_profile_id - description: The ID of the shipping profile this shipping option belongs to. - type: - type: object - description: The shipping option's type. - required: - - code - - description - - label - properties: - label: - type: string - title: label - description: The type's label. - description: - type: string - title: description - description: The type's description. - code: - type: string - title: code - description: The type's code. - prices: - type: array - description: >- - The shipping option's prices. If the `price_type` is - `calculated`, pass an empty array. - items: - oneOf: - - type: object - description: The shipping option's price for a currency code. - properties: - id: - type: string - title: id - description: The ID of an existing price. - currency_code: - type: string - title: currency_code - description: The price's currency code. - amount: - type: number - title: amount - description: The price's amount. - - type: object - description: The shipping option's price for a region. - properties: - id: - type: string - title: id - description: The ID of an existing price. - region_id: - type: string - title: region_id - description: The ID of the associated region. - amount: - type: number - title: amount - description: The price's amount. - rules: - type: array - description: The shipping option's rules. - items: - oneOf: - - type: object - description: The details of a new shipping option rule. - required: - - operator - - attribute - - value - properties: - operator: - type: string - description: The operator used to check whether a rule applies. - enum: - - in - - eq - - ne - - gt - - gte - - lt - - lte - - nin - attribute: - type: string - title: attribute - description: >- - The name of a property or table that the rule applies - to. - example: customer_group - value: - oneOf: - - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: array - description: Values of the attribute that enable this rule. - items: - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: object - description: Update the properties of an existing rule. - required: - - id - - operator - - attribute - - value - properties: - id: - type: string - title: id - description: The rule's ID. - operator: - type: string - description: The operator used to check whether a rule applies. - enum: - - in - - eq - - ne - - gt - - gte - - lt - - lte - - nin - attribute: - type: string - title: attribute - description: >- - The name of a property or table that the rule applies - to. - example: customer_group - value: - oneOf: - - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 - - type: array - description: Values of the attribute that enable this rule. - items: - type: string - title: value - description: A value of the attribute that enables this rule. - example: cusgroup_123 + $ref: ../components/schemas/AdminUpdateShippingOption.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_shipping-profiles_{id}.yaml b/www/apps/api-reference/specs/admin/paths/admin_shipping-profiles_{id}.yaml index 41f131b448018..555af65a2ff87 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_shipping-profiles_{id}.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_shipping-profiles_{id}.yaml @@ -102,20 +102,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the shipping profile. - properties: - name: - type: string - title: name - description: The shipping profile's name. - type: - type: string - title: type - description: The shipping profile's type. - metadata: - type: object - description: The shipping profile's metadata. + $ref: ../components/schemas/AdminUpdateShippingProfile.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_stock-locations_{id}_fulfillment-sets.yaml b/www/apps/api-reference/specs/admin/paths/admin_stock-locations_{id}_fulfillment-sets.yaml index 0345a07c993e4..0de25993b65ce 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_stock-locations_{id}_fulfillment-sets.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_stock-locations_{id}_fulfillment-sets.yaml @@ -37,20 +37,7 @@ post: content: application/json: schema: - type: object - description: The fulfillment set to create. - required: - - type - - name - properties: - name: - type: string - title: name - description: The fulfillment set's name. - type: - type: string - title: type - description: The fulfillment set's type. + $ref: ../components/schemas/AdminCreateStockLocationFulfillmentSet.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/admin/paths/admin_tax-providers.yaml b/www/apps/api-reference/specs/admin/paths/admin_tax-providers.yaml index afd261f02e441..67263a9bf67af 100644 --- a/www/apps/api-reference/specs/admin/paths/admin_tax-providers.yaml +++ b/www/apps/api-reference/specs/admin/paths/admin_tax-providers.yaml @@ -192,4 +192,4 @@ get: $ref: ../components/responses/invalid_request_error.yaml '500': $ref: ../components/responses/500_error.yaml - x-version: 2.8.0 + x-since: 2.8.0 diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderItems.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderItems.yaml index fc172864f445d..4e62ee3b227d8 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderItems.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderItems.yaml @@ -42,3 +42,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderShippingMethod.yaml index 443413665ea8b..b2e96363930ef 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminAddDraftOrderShippingMethod.yaml @@ -25,3 +25,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProduct.yaml index 0af27aa4755d6..c59675a8e7175 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProduct.yaml @@ -147,6 +147,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProductVariant.yaml index 82a68abb60700..bd4043cb70be5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminBatchUpdateProductVariant.yaml @@ -71,6 +71,9 @@ properties: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminClaim.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminClaim.yaml index d6104bdcebdb2..c4bf2d32df1bc 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminClaim.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminClaim.yaml @@ -68,6 +68,9 @@ properties: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCollection.yaml index 572a94f4f68c8..68b150b1ba570 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateCustomerGroup.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateCustomerGroup.yaml index 4dd642b053256..1f528e4cc8dda 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateCustomerGroup.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateCustomerGroup.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillment.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillment.yaml index 6359d75d4c3ff..31eec6b3efa3d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillment.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillment.yaml @@ -2,10 +2,10 @@ type: object description: The filfillment's details. x-schemaName: AdminCreateFulfillment required: - - data - items - metadata - order_id + - data - location_id - provider_id - delivery_address @@ -70,6 +70,9 @@ properties: metadata: type: object description: The delivery address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata items: type: array description: The items to fulfill. @@ -167,3 +170,6 @@ properties: metadata: type: object description: The fulfillment's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml new file mode 100644 index 0000000000000..4d3d7c2ae0efb --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateFulfillmentSetServiceZones.yaml @@ -0,0 +1,144 @@ +type: object +description: The service zone's details. +required: + - name +properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - metadata + - country_code + - type + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: country + - type: object + description: A province geo zone. + required: + - metadata + - country_code + - type + - province_code + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: province + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + - type: object + description: A city geo zone + required: + - metadata + - country_code + - type + - province_code + - city + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: city + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + - type: object + description: A ZIP geo zone. + required: + - metadata + - country_code + - type + - province_code + - city + - postal_expression + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: zip + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. +x-schemaName: AdminCreateFulfillmentSetServiceZones diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateGiftCardParams.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateGiftCardParams.yaml index 98f5b3fc7844d..df9577984cf37 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateGiftCardParams.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateGiftCardParams.yaml @@ -49,3 +49,6 @@ properties: metadata: type: object description: The gift card's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryItem.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryItem.yaml index 21a7ed1cd95c1..db8552d937889 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryItem.yaml @@ -57,3 +57,6 @@ properties: metadata: type: object description: The inventory item's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryLocationLevel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryLocationLevel.yaml new file mode 100644 index 0000000000000..6578c122f4a8c --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateInventoryLocationLevel.yaml @@ -0,0 +1,18 @@ +type: object +description: The inventory level's details. +required: + - location_id +properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. +x-schemaName: AdminCreateInventoryLocationLevel diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateOrderCreditLines.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateOrderCreditLines.yaml index a5d094df4d45b..edb07be15c9a7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateOrderCreditLines.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateOrderCreditLines.yaml @@ -24,3 +24,6 @@ properties: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCapture.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCapture.yaml new file mode 100644 index 0000000000000..0064576c86290 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCapture.yaml @@ -0,0 +1,8 @@ +type: object +description: The payment's details. +properties: + amount: + type: number + title: amount + description: The amount to capture. +x-schemaName: AdminCreatePaymentCapture diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCollection.yaml new file mode 100644 index 0000000000000..9d1f8bef7bc4a --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentCollection.yaml @@ -0,0 +1,15 @@ +type: object +description: The payment collection's details. +required: + - order_id + - amount +properties: + order_id: + type: string + title: order_id + description: The ID of the associated order. + amount: + type: number + title: amount + description: The amount to be paid. +x-schemaName: AdminCreatePaymentCollection diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentRefund.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentRefund.yaml new file mode 100644 index 0000000000000..b5be8ea589b70 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreatePaymentRefund.yaml @@ -0,0 +1,16 @@ +type: object +description: The refund's details. +properties: + amount: + type: number + title: amount + description: The amount to refund. + refund_reason_id: + type: string + title: refund_reason_id + description: The ID of a refund reason. + note: + type: string + title: note + description: A note to attach to the refund. +x-schemaName: AdminCreatePaymentRefund diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProduct.yaml index 0d83da3c0051a..e463da6f69226 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProduct.yaml @@ -146,6 +146,9 @@ properties: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductCategory.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductCategory.yaml index 5273e97f2bd2c..b4eebd83f25bc 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductCategory.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductCategory.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The product category's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductTag.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductTag.yaml index 88b118a73fe08..453f7815fef0d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductTag.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductTag.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The product tag's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductType.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductType.yaml index 92179c70669bc..ec0a42d6a9516 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductType.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductType.yaml @@ -7,6 +7,9 @@ properties: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata value: type: string title: value diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductVariant.yaml index 5cbbf93868f35..29b6784abaf57 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateProductVariant.yaml @@ -74,6 +74,9 @@ properties: metadata: type: object description: The variant's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The variant's prices. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateRegion.yaml index 6028eaf5d3831..b50a259f29132 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateRegion.yaml @@ -41,3 +41,6 @@ properties: metadata: type: object description: The region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateReservation.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateReservation.yaml index 1f78aa26dd015..a025ab2c17316 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateReservation.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateReservation.yaml @@ -29,3 +29,6 @@ properties: metadata: type: object description: The reservation's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateReturnReason.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateReturnReason.yaml index adac302423d2a..4be2cf28d12cb 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateReturnReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateReturnReason.yaml @@ -24,3 +24,6 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateSalesChannel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateSalesChannel.yaml index 69ac7d89c6f3d..cd673e32859bd 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateSalesChannel.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateSalesChannel.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The sales channel's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateShippingProfile.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateShippingProfile.yaml index 80a6b3ae524d4..679f3438f9e10 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateShippingProfile.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateShippingProfile.yaml @@ -16,3 +16,6 @@ properties: metadata: type: object description: The shipping profile's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocation.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocation.yaml index 4e52d487ce02a..bfa63d63d0327 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocation.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocation.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The stock location's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml new file mode 100644 index 0000000000000..f09930060123f --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStockLocationFulfillmentSet.yaml @@ -0,0 +1,15 @@ +type: object +description: The fulfillment set to create. +required: + - type + - name +properties: + name: + type: string + title: name + description: The fulfillment set's name. + type: + type: string + title: type + description: The fulfillment set's type. +x-schemaName: AdminCreateStockLocationFulfillmentSet diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateStoreCreditAccount.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStoreCreditAccount.yaml index 56c12f053632e..387063e6c9d24 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateStoreCreditAccount.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRate.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRate.yaml index 8aa3b751deccb..474c362dbc3ec 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRate.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRate.yaml @@ -43,3 +43,6 @@ properties: metadata: type: object description: The tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRegion.yaml index f2343d568f239..f882c430527c3 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCreateTaxRegion.yaml @@ -51,9 +51,15 @@ properties: metadata: type: object description: The default tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The tax region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCustomer.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCustomer.yaml index 7a8ef94f4fc1f..7c6d09c86a6d7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCustomer.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCustomer.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The customer's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCustomerAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCustomerAddress.yaml index 9cce8725ae34e..9cfc816b85e4c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCustomerAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCustomerAddress.yaml @@ -89,6 +89,9 @@ properties: metadata: type: object description: The address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminCustomerGroup.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminCustomerGroup.yaml index ddef197fa058e..cfc0b7ae4c80a 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminCustomerGroup.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminCustomerGroup.yaml @@ -25,6 +25,9 @@ properties: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrder.yaml index d0807a7018891..7a40e1caf05e4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrder.yaml @@ -144,6 +144,9 @@ properties: metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrderPreview.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrderPreview.yaml index 992e94feb296d..8f3a96628c3e5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrderPreview.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminDraftOrderPreview.yaml @@ -215,6 +215,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -352,6 +355,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -541,6 +547,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminExchange.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminExchange.yaml index 0123920e3f91e..b693ed23331e2 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminExchange.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminExchange.yaml @@ -66,6 +66,9 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminFulfillment.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminFulfillment.yaml index a459be7817a01..140d1f4809693 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminFulfillment.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminFulfillment.yaml @@ -77,6 +77,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminFulfillmentAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminFulfillmentAddress.yaml index 67f9793026b6b..c207f520f1279 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminFulfillmentAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminFulfillmentAddress.yaml @@ -74,6 +74,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminInventoryLevel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminInventoryLevel.yaml index d7802818c2b7b..b0f6bb9c256a9 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminInventoryLevel.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminInventoryLevel.yaml @@ -56,6 +56,9 @@ properties: metadata: type: object description: The location level's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_item: type: object available_quantity: diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminInvite.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminInvite.yaml index 566d068b14a79..500fa0a27fd97 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminInvite.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminInvite.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The invite's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminInviteAccept.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminInviteAccept.yaml new file mode 100644 index 0000000000000..9b853e7621725 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminInviteAccept.yaml @@ -0,0 +1,17 @@ +type: object +description: The details of the user to be created. +properties: + email: + type: string + title: email + description: The user's email. + format: email + first_name: + type: string + title: first_name + description: The user's first name. + last_name: + type: string + title: last_name + description: The user's last name. +x-schemaName: AdminInviteAccept diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminMarkPaymentCollectionPaid.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminMarkPaymentCollectionPaid.yaml new file mode 100644 index 0000000000000..eec135671047f --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminMarkPaymentCollectionPaid.yaml @@ -0,0 +1,10 @@ +type: object +description: The payment details. +required: + - order_id +properties: + order_id: + type: string + title: order_id + description: The ID of the order associated with the payment collection. +x-schemaName: AdminMarkPaymentCollectionPaid diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrder.yaml index ac1c9a048912e..f0d0784c760cb 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrder.yaml @@ -139,6 +139,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderAddress.yaml index fad010e48baaa..fb80941ffa009 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderAddress.yaml @@ -65,6 +65,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderChange.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderChange.yaml index 195bd76d5e903..36005625f3107 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderChange.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderChange.yaml @@ -112,6 +112,9 @@ properties: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderFulfillment.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderFulfillment.yaml index e1312673f4fc0..7ae1b3f61d526 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderLineItem.yaml index a6b3ba9b68fbb..c590d05144d0f 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderLineItem.yaml @@ -160,6 +160,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderPreview.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderPreview.yaml index 030a16e48c1d2..98cf4090b772c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderPreview.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderPreview.yaml @@ -217,6 +217,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -354,6 +357,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -543,6 +549,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminOrderShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminOrderShippingMethod.yaml index ab93242120a98..1b64bc87e6901 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPaymentCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPaymentCollection.yaml index 1af31ef86411a..a67ab43e575b8 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPaymentCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml index c9ce2644120bf..71c3bc5ac6d92 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsAddItemsReqSchema.yaml @@ -31,3 +31,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml index 8d6fe6cdad202..d16214b606e31 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingReqSchema.yaml index 34e2356020b13..ec629bd78ae35 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostClaimsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml index bc0234b0bdbfc..33f64855b338f 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesAddItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml index 666ccbcfe538a..b54a811facc57 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesRequestItemsReturnActionReqSchema.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml index da3d011473f9d..0149dfd44a0ed 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesReturnRequestItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml index 9564ca8aa45c2..4fa5f2d3351dd 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingReqSchema.yaml index 4bff8e27eb9e0..577bcd7bc7b28 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostExchangesShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderClaimsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderClaimsReqSchema.yaml index e3dc6b300ef8a..b828e16fa2fde 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderClaimsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderClaimsReqSchema.yaml @@ -30,3 +30,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml index d477bfb83846d..91614c5e78cda 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsAddItemsReqSchema.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata compare_at_unit_price: type: number title: compare_at_unit_price diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsReqSchema.yaml index 23a158dae9887..bffbcdd2c22ae 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsReqSchema.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml index 3bad2acc19bbe..a7c70033520fe 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml index 3327fc1442a9b..4aa99ea642bbf 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderEditsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderExchangesReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderExchangesReqSchema.yaml index 4472a42d37093..f6f32aad33d55 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderExchangesReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostOrderExchangesReqSchema.yaml @@ -19,3 +19,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReceiveReturnsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReceiveReturnsReqSchema.yaml index 12d20a347b64e..f4b1f18cb737e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReceiveReturnsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReceiveReturnsReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReqSchema.yaml index 0fbd5801592d2..137c180ca949e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReqSchema.yaml @@ -27,3 +27,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml index 0d0c6e633cc59..0ae505d7fbd76 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsActionReqSchema.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml index d47549d7ef8b5..b64ffbe8d0ee4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsRequestItemsReqSchema.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReturnReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReturnReqSchema.yaml index bdb5f9b00d8a1..638346da8c60c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReturnReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsReturnReqSchema.yaml @@ -15,3 +15,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml index 2fb4607a740a5..0ad7fb99be735 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingActionReqSchema.yaml @@ -13,3 +13,6 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingReqSchema.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingReqSchema.yaml index 861fa03fc7c5f..d7ab099fe3c0f 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingReqSchema.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminPostReturnsShippingReqSchema.yaml @@ -23,3 +23,6 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProduct.yaml index 304df0f157c2a..6674fcc3e6bbe 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProduct.yaml @@ -86,6 +86,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductCategory.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductCategory.yaml index 3c5286bc3436a..b14093d05dd76 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductCategory.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductCategory.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductImage.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductImage.yaml index d1561e5a2fcf4..1238fdf516f32 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductImage.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductImage.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductOption.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductOption.yaml index 415115c4163d5..ab5b7196de03d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductOption.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductOption.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductOptionValue.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductOptionValue.yaml index 88175464a4be2..28ec39ea38dac 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductOptionValue.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductTag.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductTag.yaml index 7fc320de89716..8fb2c7567ec8d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductTag.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductTag.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductType.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductType.yaml index ef5f2ece56c12..6cef7208c7e8a 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductType.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductType.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminProductVariant.yaml index 5696575641f96..b6c4ca8752b64 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminProductVariant.yaml @@ -136,6 +136,9 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_items: type: array description: The variant's inventory items. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminRefundReason.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminRefundReason.yaml index 919bb56bfa28d..a1f49a55fafd5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminRefundReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminRefundReason.yaml @@ -23,6 +23,9 @@ properties: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminRegion.yaml index 96b6ff979c1fb..f8126c6286672 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminRegion.yaml @@ -36,6 +36,9 @@ properties: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminReservation.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminReservation.yaml index 2b75d1102d26b..652eb2043756d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminReservation.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminReservation.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The reservation's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminReturnItem.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminReturnItem.yaml index 96691fc7caa16..0d4d8cb24930f 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminReturnItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminReturnItem.yaml @@ -48,3 +48,6 @@ properties: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminReturnReason.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminReturnReason.yaml index 7de3166b718f4..fe647c7656362 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminReturnReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminReturnReason.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminSalesChannel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminSalesChannel.yaml index 2311a9b68784f..8ed84c4f28f1d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminSalesChannel.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminSalesChannel.yaml @@ -30,6 +30,9 @@ properties: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminShippingOption.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminShippingOption.yaml index 7cb57ee3897ed..116a1b720e38c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminShippingOption.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminShippingOption.yaml @@ -88,6 +88,9 @@ properties: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminShippingOptionRule.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminShippingOptionRule.yaml index 2676706b8ee9d..7f25523307731 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminShippingOptionRule.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminShippingOptionRule.yaml @@ -33,8 +33,18 @@ properties: - gte - nin value: - type: string - title: value + oneOf: + - type: string + title: value + description: The shipping option rule's value. + example: 'true' + - type: array + description: The shipping option rule's values. + items: + type: string + title: value + description: A value of the shipping option rule. + example: 'true' shipping_option_id: type: string title: shipping_option_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminShippingProfile.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminShippingProfile.yaml index 1dc31c1b5b70d..9a29de8f6cd90 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminShippingProfile.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminShippingProfile.yaml @@ -17,6 +17,9 @@ properties: metadata: type: object description: The shipping profile's metadata, holds custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminStore.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminStore.yaml index 32f483dc82209..7dbecee439f49 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminStore.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminStore.yaml @@ -40,6 +40,9 @@ properties: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminStoreCreditAccount.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminStoreCreditAccount.yaml index 0e54b944b40a4..4c19a0069c3b8 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminStoreCreditAccount.yaml @@ -49,6 +49,9 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminTaxRate.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminTaxRate.yaml index 7972123ffef44..19c88dbd9bb70 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminTaxRate.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminTaxRate.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_region_id: type: string title: tax_region_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminTaxRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminTaxRegion.yaml index f75bb9af92ef8..7595bc5aba2ae 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminTaxRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminTaxRegion.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata parent_id: type: string title: parent_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminTransaction.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminTransaction.yaml index 0a488a5fd2715..fae0c25d1cea3 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminTransaction.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminTransaction.yaml @@ -51,6 +51,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminTransactionGroup.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminTransactionGroup.yaml index bf509bb167394..40e5f640f0048 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminTransactionGroup.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminTransactionGroup.yaml @@ -35,3 +35,6 @@ properties: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateCustomerGroup.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateCustomerGroup.yaml index 57fb90e7151e6..930e67c47e748 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateCustomerGroup.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateCustomerGroup.yaml @@ -9,3 +9,6 @@ properties: metadata: type: object description: The customer group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrder.yaml index e4e0c8a5ee963..8396b38deb6b6 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrder.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The shipping address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The draft order's billing address. @@ -111,9 +114,15 @@ properties: metadata: type: object description: The billing address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata customer_id: type: string title: customer_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml index 8b2d7422db30a..48b76c24ec6ca 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateDraftOrderActionShippingMethod.yaml @@ -25,3 +25,6 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml new file mode 100644 index 0000000000000..a319aa1491ce3 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateFulfillmentSetServiceZones.yaml @@ -0,0 +1,158 @@ +type: object +description: The service zone's details. +properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's associated geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - type + - metadata + - country_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: country + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A province geo zone. + required: + - type + - metadata + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: province + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A city geo zone + required: + - type + - metadata + - city + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: city + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A ZIP geo zone. + required: + - type + - metadata + - city + - country_code + - province_code + - postal_expression + properties: + type: + type: string + title: type + description: The geo zone's type. + default: zip + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + id: + type: string + title: id + description: The ID of an existing geo zone. +x-schemaName: AdminUpdateFulfillmentSetServiceZones diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateGiftCardParams.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateGiftCardParams.yaml index f53e7e972823f..108d2cde1ea56 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateGiftCardParams.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateGiftCardParams.yaml @@ -32,3 +32,6 @@ properties: metadata: type: object description: The gift card's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryItem.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryItem.yaml new file mode 100644 index 0000000000000..c7a6b674506a8 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryItem.yaml @@ -0,0 +1,64 @@ +type: object +description: The properties to update in the inventory item. +properties: + sku: + type: string + title: sku + description: The inventory item's SKU. + hs_code: + type: string + title: hs_code + description: The inventory item's HS code. + weight: + type: number + title: weight + description: The inventory item's weight. + length: + type: number + title: length + description: The inventory item's length. + height: + type: number + title: height + description: The inventory item's height. + width: + type: number + title: width + description: The inventory item's width. + origin_country: + type: string + title: origin_country + description: The inventory item's origin country. + mid_code: + type: string + title: mid_code + description: The inventory item's MID code. + material: + type: string + title: material + description: The inventory item's material. + title: + type: string + title: title + description: The inventory item's title. + description: + type: string + title: description + description: The inventory item's description. + requires_shipping: + type: boolean + title: requires_shipping + description: Whether the inventory item requires shipping. + thumbnail: + type: string + title: thumbnail + description: >- + The URL of an image to be used as the inventory item's thumbnail. You can + use the Upload API routes to upload an image and get its URL. + metadata: + type: object + description: The inventory item's metadata. Can be custom data in key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateInventoryItem diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryLocationLevel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryLocationLevel.yaml new file mode 100644 index 0000000000000..94fba3e700154 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateInventoryLocationLevel.yaml @@ -0,0 +1,12 @@ +type: object +description: The properties to update in the inventory level. +properties: + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. +x-schemaName: AdminUpdateInventoryLocationLevel diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateOrder.yaml index 78280d8e7652c..cd3ef369d9dff 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateOrder.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The order's billing address. @@ -111,6 +114,12 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdatePaymentRefundReason.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdatePaymentRefundReason.yaml new file mode 100644 index 0000000000000..1573aa64b7003 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdatePaymentRefundReason.yaml @@ -0,0 +1,12 @@ +type: object +description: The properties to update in the refund reason. +properties: + label: + type: string + title: label + description: The refund reason's label. + description: + type: string + title: description + description: The refund reason's description. +x-schemaName: AdminUpdatePaymentRefundReason diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProduct.yaml index 1f220e671fcae..c0d35a78920e7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProduct.yaml @@ -147,6 +147,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductCategory.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductCategory.yaml new file mode 100644 index 0000000000000..2f8962e6516fe --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductCategory.yaml @@ -0,0 +1,40 @@ +type: object +description: The properties to update in the product category. +properties: + name: + type: string + title: name + description: The product category's name. + description: + type: string + title: description + description: The product category's description. + handle: + type: string + title: handle + description: The product category's handle. Must be a unique value. + is_internal: + type: boolean + title: is_internal + description: >- + Whether the product category is only used for internal purposes and + shouldn't be shown the customer. + is_active: + type: boolean + title: is_active + description: Whether the product category is active. + parent_category_id: + type: string + title: parent_category_id + description: The ID of a parent category. + metadata: + type: object + description: The product category's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + rank: + type: number + title: rank + description: The product category's rank among other categories. +x-schemaName: AdminUpdateProductCategory diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductTag.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductTag.yaml new file mode 100644 index 0000000000000..50b569184ec65 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductTag.yaml @@ -0,0 +1,14 @@ +type: object +description: The properties to update in the product tag. +properties: + value: + type: string + title: value + description: The product tag's value. + metadata: + type: object + description: The product tag's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateProductTag diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductType.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductType.yaml new file mode 100644 index 0000000000000..6c60edd1a6179 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductType.yaml @@ -0,0 +1,14 @@ +type: object +description: The properties to update in the product type. +properties: + value: + type: string + title: value + description: The product type's value. + metadata: + type: object + description: The product type's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateProductType diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductVariant.yaml index 59f99cff83c1d..93e6b702666c6 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateProductVariant.yaml @@ -71,6 +71,9 @@ properties: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateRegion.yaml new file mode 100644 index 0000000000000..740456f51c0fb --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateRegion.yaml @@ -0,0 +1,40 @@ +type: object +description: The propeties to update in the region. +properties: + name: + type: string + title: name + description: The region's name. + currency_code: + type: string + title: currency_code + description: The region's currency code. + countries: + type: array + description: The region's countries. + items: + type: string + title: countries + description: A country code. + automatic_taxes: + type: boolean + title: automatic_taxes + description: Whether taxes are calculated automatically for carts in the region. + payment_providers: + type: array + description: The payment providers enabled in the region. + items: + type: string + title: payment_providers + description: A payment provider's ID. + metadata: + type: object + description: The region's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + is_tax_inclusive: + type: boolean + title: is_tax_inclusive + description: Whether the prices in the region are tax inclusive. +x-schemaName: AdminUpdateRegion diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReservation.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReservation.yaml new file mode 100644 index 0000000000000..e2931ef4b8a2b --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReservation.yaml @@ -0,0 +1,22 @@ +type: object +description: The properties to update in the reservation. +properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + quantity: + type: number + title: quantity + description: The reserved quantity. + description: + type: string + title: description + description: The reservation's description. + metadata: + type: object + description: The reservation's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateReservation diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReturnReason.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReturnReason.yaml index 3299988196d04..9f1309e182f63 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReturnReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateReturnReason.yaml @@ -17,6 +17,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata required: - value - label diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateSalesChannel.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateSalesChannel.yaml index db5e76e560aa0..d1e459242792e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateSalesChannel.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateSalesChannel.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingOption.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingOption.yaml new file mode 100644 index 0000000000000..8ea9eac5bc0a6 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingOption.yaml @@ -0,0 +1,174 @@ +type: object +description: The properties to update in the shipping option. +properties: + name: + type: string + title: name + description: The shipping option's name. + data: + type: object + description: The shipping option's data that is useful for third-party providers. + externalDocs: + url: >- + https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property + price_type: + type: string + description: > + The type of the shipping option's price. If `calculated`, its price is + retrieved by the associated fulfillment provider during checkout. If + `flat`, its price is set in the `prices` property. + enum: + - calculated + - flat + provider_id: + type: string + title: provider_id + description: >- + The ID of the associated fulfillment provider that is used to process the + option. + shipping_profile_id: + type: string + title: shipping_profile_id + description: The ID of the shipping profile this shipping option belongs to. + type: + type: object + description: The shipping option's type. + required: + - code + - description + - label + properties: + label: + type: string + title: label + description: The type's label. + description: + type: string + title: description + description: The type's description. + code: + type: string + title: code + description: The type's code. + prices: + type: array + description: >- + The shipping option's prices. If the `price_type` is `calculated`, pass an + empty array. + items: + oneOf: + - type: object + description: The shipping option's price for a currency code. + properties: + id: + type: string + title: id + description: The ID of an existing price. + currency_code: + type: string + title: currency_code + description: The price's currency code. + amount: + type: number + title: amount + description: The price's amount. + - type: object + description: The shipping option's price for a region. + properties: + id: + type: string + title: id + description: The ID of an existing price. + region_id: + type: string + title: region_id + description: The ID of the associated region. + amount: + type: number + title: amount + description: The price's amount. + rules: + type: array + description: The shipping option's rules. + items: + oneOf: + - type: object + description: The details of a new shipping option rule. + required: + - operator + - attribute + - value + properties: + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: object + description: Update the properties of an existing rule. + required: + - id + - operator + - attribute + - value + properties: + id: + type: string + title: id + description: The rule's ID. + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 +x-schemaName: AdminUpdateShippingOption diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingProfile.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingProfile.yaml new file mode 100644 index 0000000000000..4102792ddf546 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateShippingProfile.yaml @@ -0,0 +1,18 @@ +type: object +description: The properties to update in the shipping profile. +properties: + name: + type: string + title: name + description: The shipping profile's name. + type: + type: string + title: type + description: The shipping profile's type. + metadata: + type: object + description: The shipping profile's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata +x-schemaName: AdminUpdateShippingProfile diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStockLocation.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStockLocation.yaml index 9d47328396ace..ed608616691d4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStockLocation.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStockLocation.yaml @@ -61,3 +61,6 @@ properties: metadata: type: object description: The stock location's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStore.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStore.yaml index e8c4463b52cdc..734283353bd90 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStore.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateStore.yaml @@ -43,3 +43,6 @@ properties: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRate.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRate.yaml index 8000de42a91c3..e79271cd68c10 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRate.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRate.yaml @@ -48,3 +48,6 @@ properties: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRegion.yaml index 942070922146a..ee6a84b52e812 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateTaxRegion.yaml @@ -13,6 +13,9 @@ properties: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateUser.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateUser.yaml index 32e881a213587..c6011259d72ed 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUpdateUser.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUpdateUser.yaml @@ -17,3 +17,6 @@ properties: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/AdminUser.yaml b/www/apps/api-reference/specs/store/components/schemas/AdminUser.yaml index c3bca505cb895..223368c4c72f0 100644 --- a/www/apps/api-reference/specs/store/components/schemas/AdminUser.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/AdminUser.yaml @@ -36,6 +36,9 @@ properties: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseCart.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseCart.yaml index 5ba46f61aa6af..c3dd5d86215d5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseCart.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseCart.yaml @@ -73,6 +73,9 @@ properties: metadata: type: object description: The cart's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseCartLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseCartLineItem.yaml index b2e59d962110c..eca6c74f2364e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseCartLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseCartLineItem.yaml @@ -133,6 +133,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseCartShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseCartShippingMethod.yaml index 0a23a60cdc4c2..2601c6fffd7a2 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseCartShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseCartShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseClaimItem.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseClaimItem.yaml index fe407754bed6c..470709cfa67df 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseClaimItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseClaimItem.yaml @@ -55,6 +55,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseCollection.yaml index 58ab3baab3f94..a038a2eab7753 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseExchangeItem.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseExchangeItem.yaml index 82c98c6dbf2f4..8fa987469289c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseExchangeItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseExchangeItem.yaml @@ -33,6 +33,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrder.yaml index daa6b8cc669fa..4322ff6393ab8 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrder.yaml @@ -134,6 +134,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrderAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrderAddress.yaml index 87e754907729f..0b2e3f17d74f6 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrderAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrderAddress.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrderFulfillment.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrderFulfillment.yaml index eba0b4539b212..824eb98ba3a74 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrderLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrderLineItem.yaml index 115a58c2dbe82..279d023e9ca37 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrderLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrderLineItem.yaml @@ -160,6 +160,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrderShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrderShippingMethod.yaml index 930352a943340..54df7640adeb8 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseOrderTransaction.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseOrderTransaction.yaml index f822566df95f8..542c5349d2849 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseOrderTransaction.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseOrderTransaction.yaml @@ -46,6 +46,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BasePaymentCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/BasePaymentCollection.yaml index e5c542339e375..47c61f8a0ca03 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BasePaymentCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BasePaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProduct.yaml index 51b5946075787..6b2f40b707bcc 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProduct.yaml @@ -80,6 +80,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductCategory.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductCategory.yaml index b7311e22133fc..589d8220ee1fe 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductCategory.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductCategory.yaml @@ -43,6 +43,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductImage.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductImage.yaml index 25637c69e893d..cb6694260cecc 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductImage.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductImage.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductOption.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductOption.yaml index 7964d121fed58..69e0e5a487c03 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductOption.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductOption.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductOptionValue.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductOptionValue.yaml index ba0a4dfd87809..83b476b9eb9e3 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductOptionValue.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductTag.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductTag.yaml index 2f9a58b017962..f39f147ccca62 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductTag.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductTag.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductType.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductType.yaml index 70c15ce3ff314..34600a475fde7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductType.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductType.yaml @@ -33,3 +33,6 @@ properties: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseProductVariant.yaml index e27ee0ea8c77c..74d80a6209576 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseProductVariant.yaml @@ -130,3 +130,6 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/BaseRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/BaseRegion.yaml index bbf5758714afc..6e64976df1c67 100644 --- a/www/apps/api-reference/specs/store/components/schemas/BaseRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/BaseRegion.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The region's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/CreateAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/CreateAddress.yaml index a610a042befa1..238fe6942820e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/CreateAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/CreateAddress.yaml @@ -54,3 +54,6 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/InventoryLevel.yaml b/www/apps/api-reference/specs/store/components/schemas/InventoryLevel.yaml index aa13bb7243bb5..6107f2104a444 100644 --- a/www/apps/api-reference/specs/store/components/schemas/InventoryLevel.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/InventoryLevel.yaml @@ -41,3 +41,6 @@ properties: metadata: type: object description: The inventory level's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/Order.yaml b/www/apps/api-reference/specs/store/components/schemas/Order.yaml index a933e4e94d298..6c331fd686fb2 100644 --- a/www/apps/api-reference/specs/store/components/schemas/Order.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/Order.yaml @@ -177,6 +177,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata canceled_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderAddress.yaml index f2ca629923d9d..8160d47381cf3 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderAddress.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderChange.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderChange.yaml index 58d07e2b68c64..72b1f74a7b37c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderChange.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderChange.yaml @@ -112,6 +112,9 @@ properties: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderClaim.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderClaim.yaml index a68218b780718..9acfe0de87700 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderClaim.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderClaim.yaml @@ -64,6 +64,9 @@ properties: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderCreditLine.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderCreditLine.yaml index 17b199333be26..e2769840affeb 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderCreditLine.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderCreditLine.yaml @@ -35,6 +35,9 @@ properties: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderExchange.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderExchange.yaml index 857b9ed634090..e211bb4ba0e76 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderExchange.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderExchange.yaml @@ -59,6 +59,9 @@ properties: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderItem.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderItem.yaml index ab12ebb4d28b5..98fe9e4853dc9 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderItem.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderLineItem.yaml index 1a0cf29b96a5e..3bf42aa2b2b35 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderLineItem.yaml @@ -141,6 +141,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderReturnItem.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderReturnItem.yaml index 41fd512de05e7..465be4ee1f7b1 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderReturnItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderReturnItem.yaml @@ -37,6 +37,9 @@ properties: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata order_id: type: string title: order_id diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderShippingMethod.yaml index 3aa794be00473..b052eacde5fbf 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/store/components/schemas/OrderTransaction.yaml b/www/apps/api-reference/specs/store/components/schemas/OrderTransaction.yaml index d497d2305cd4f..76e5da6dfc612 100644 --- a/www/apps/api-reference/specs/store/components/schemas/OrderTransaction.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/OrderTransaction.yaml @@ -48,6 +48,9 @@ properties: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/RefundReason.yaml b/www/apps/api-reference/specs/store/components/schemas/RefundReason.yaml index 5c681747978e2..fad95d0cdefb3 100644 --- a/www/apps/api-reference/specs/store/components/schemas/RefundReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/RefundReason.yaml @@ -23,6 +23,9 @@ properties: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/Return.yaml b/www/apps/api-reference/specs/store/components/schemas/Return.yaml index cfefdef18c517..f0933b5d2c960 100644 --- a/www/apps/api-reference/specs/store/components/schemas/Return.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/Return.yaml @@ -47,6 +47,9 @@ properties: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreAddCartLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreAddCartLineItem.yaml index 8ce18622d407c..b967a3087e4de 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreAddCartLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreAddCartLineItem.yaml @@ -16,3 +16,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreAddCartShippingMethods.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreAddCartShippingMethods.yaml new file mode 100644 index 0000000000000..494a059031399 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/StoreAddCartShippingMethods.yaml @@ -0,0 +1,19 @@ +type: object +description: The shipping method's details. +required: + - option_id +properties: + option_id: + type: string + title: option_id + description: The ID of the shipping option this method is created from. + data: + type: object + description: >- + Any additional data relevant for the third-party fulfillment provider to + process the shipment. + externalDocs: + url: >- + https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter + description: Learn more about the data parameter. +x-schemaName: StoreAddCartShippingMethods diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCart.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCart.yaml index 6cd3dc8a0d96b..2ff9e3c53a2a7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCart.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCart.yaml @@ -78,6 +78,9 @@ properties: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCartAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCartAddress.yaml index 89963e7b4c39e..6f1f560076403 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCartAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCartAddress.yaml @@ -62,6 +62,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCartLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCartLineItem.yaml index 824b9623cbf3c..cbb841f1d0ef1 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCartLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCartLineItem.yaml @@ -277,6 +277,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCartShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCartShippingMethod.yaml index 818a670856c64..4cd0151100296 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCartShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCartShippingMethod.yaml @@ -57,6 +57,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCollection.yaml index 121ef729ec06b..b5f66d0361958 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCollection.yaml @@ -45,3 +45,6 @@ properties: metadata: type: object description: The collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCreateCart.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCart.yaml index 69fb5ff088f3a..608d8bbea2083 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCreateCart.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCart.yaml @@ -39,3 +39,6 @@ properties: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomer.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomer.yaml index 6821b053399fc..2685212918b71 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomer.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomer.yaml @@ -28,3 +28,6 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomerAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomerAddress.yaml new file mode 100644 index 0000000000000..a68cce7becd25 --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCreateCustomerAddress.yaml @@ -0,0 +1,63 @@ +type: object +description: The address's details. +properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. +x-schemaName: StoreCreateCustomerAddress diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCustomer.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCustomer.yaml index 0c0997ff3f1d5..d079b0a0e7283 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCustomer.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCustomer.yaml @@ -52,6 +52,9 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreCustomerAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreCustomerAddress.yaml index 7c24514a35f8e..1153236620ab7 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreCustomerAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreCustomerAddress.yaml @@ -89,6 +89,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreGiftCardInvitation.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreGiftCardInvitation.yaml deleted file mode 100644 index 8ad2557f70183..0000000000000 --- a/www/apps/api-reference/specs/store/components/schemas/StoreGiftCardInvitation.yaml +++ /dev/null @@ -1,24 +0,0 @@ -type: object -description: The gift card invitation's details. -x-schemaName: StoreGiftCardInvitation -required: - - id - - email - - status - - gift_card -properties: - id: - type: string - title: id - description: The gift card invitation's ID. - email: - type: string - title: email - description: The gift card invitation's email. - format: email - status: - type: string - title: status - description: The gift card invitation's status. - gift_card: - $ref: ./StoreGiftCard.yaml diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreOrder.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreOrder.yaml index 8235fd5e24746..c4013d4431839 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreOrder.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreOrder.yaml @@ -125,6 +125,9 @@ properties: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreOrderAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreOrderAddress.yaml index 8c24ea8e12f2a..a960f1c7fbcb4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreOrderAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreOrderAddress.yaml @@ -65,6 +65,9 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreOrderFulfillment.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreOrderFulfillment.yaml index 4c9e22c359173..ed2f535f34664 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreOrderFulfillment.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreOrderFulfillment.yaml @@ -63,6 +63,9 @@ properties: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreOrderLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreOrderLineItem.yaml index 556ab17106765..0925dc3195178 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreOrderLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreOrderLineItem.yaml @@ -349,6 +349,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -530,6 +533,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -794,6 +800,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -817,6 +826,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -1016,6 +1028,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1201,6 +1216,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1494,6 +1512,10 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: >- + https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1524,6 +1546,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -1878,6 +1903,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -2059,6 +2087,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -2323,6 +2354,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -2346,6 +2380,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -2541,6 +2578,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -2726,6 +2766,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3019,6 +3062,10 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: >- + https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3049,6 +3096,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3295,6 +3345,9 @@ properties: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3480,6 +3533,9 @@ properties: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3773,6 +3829,9 @@ properties: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3803,6 +3862,9 @@ properties: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -3958,6 +4020,9 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreOrderShippingMethod.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreOrderShippingMethod.yaml index 513ce23b454c2..b2994b98df5ac 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreOrderShippingMethod.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreOrderShippingMethod.yaml @@ -60,6 +60,9 @@ properties: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -137,6 +140,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -668,6 +674,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -786,6 +795,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -1313,6 +1325,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -1669,6 +1684,9 @@ properties: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total diff --git a/www/apps/api-reference/specs/store/components/schemas/StorePaymentCollection.yaml b/www/apps/api-reference/specs/store/components/schemas/StorePaymentCollection.yaml index b1f572ce80333..bd11eb1da264f 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StorePaymentCollection.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StorePaymentCollection.yaml @@ -50,6 +50,9 @@ properties: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProduct.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProduct.yaml index 137cd220c4578..b985c4eef9ed4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProduct.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProduct.yaml @@ -68,6 +68,9 @@ properties: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductCategory.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductCategory.yaml index 4df135c98f3bf..b1e3dafb316aa 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductCategory.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductCategory.yaml @@ -48,6 +48,9 @@ properties: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductImage.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductImage.yaml index a3b3b1c72afd1..21e551b763b1e 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductImage.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductImage.yaml @@ -32,6 +32,9 @@ properties: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductOption.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductOption.yaml index 40ba71a5d9d22..06b88c40d23c1 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductOption.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductOption.yaml @@ -24,6 +24,9 @@ properties: metadata: type: object description: The option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductOptionValue.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductOptionValue.yaml index 9a754c37c21b0..5a4a747f0a41b 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductOptionValue.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductOptionValue.yaml @@ -22,6 +22,9 @@ properties: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductTag.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductTag.yaml index 1e31fdb2fbde8..cce1492ca6a1a 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductTag.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductTag.yaml @@ -28,6 +28,9 @@ properties: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata required: - id - value diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductType.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductType.yaml index ed951b4c1301d..96a52be903b3c 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductType.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductType.yaml @@ -14,6 +14,9 @@ properties: metadata: type: object description: The product type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreProductVariant.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreProductVariant.yaml index 677ad64a10b29..5ee38cc8775d5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreProductVariant.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreProductVariant.yaml @@ -20,6 +20,9 @@ properties: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata id: type: string title: id diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreRegion.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreRegion.yaml index 4f92f9c220030..cd8f654d031b6 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreRegion.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreRegion.yaml @@ -38,6 +38,9 @@ properties: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreReturnItem.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreReturnItem.yaml index bd4223318cb7d..5b681b1ca4bf1 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreReturnItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreReturnItem.yaml @@ -44,3 +44,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreReturnReason.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreReturnReason.yaml index b08051fb32484..c7b6e9ed0f899 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreReturnReason.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreReturnReason.yaml @@ -27,6 +27,9 @@ properties: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreShippingOption.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreShippingOption.yaml index c1e75ff2426a6..c57f31d6d8cf6 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreShippingOption.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreShippingOption.yaml @@ -70,3 +70,6 @@ properties: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreStoreCreditAccount.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreStoreCreditAccount.yaml index 26cf2ddc5b258..44d636dcb555d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreStoreCreditAccount.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreStoreCreditAccount.yaml @@ -49,6 +49,9 @@ properties: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreTransactionGroup.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreTransactionGroup.yaml index 0a7fe807c1252..6ddf0d8bb727d 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreTransactionGroup.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreTransactionGroup.yaml @@ -34,4 +34,7 @@ properties: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata x-schemaName: StoreTransactionGroup diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCartLineItem.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCartLineItem.yaml index 844c2958cea64..dc31e201cc9a5 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCartLineItem.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCartLineItem.yaml @@ -11,3 +11,6 @@ properties: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomer.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomer.yaml index 700e28826a85e..8e1fb612468fd 100644 --- a/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomer.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomer.yaml @@ -21,3 +21,6 @@ properties: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomerAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomerAddress.yaml new file mode 100644 index 0000000000000..83d98b908bb9b --- /dev/null +++ b/www/apps/api-reference/specs/store/components/schemas/StoreUpdateCustomerAddress.yaml @@ -0,0 +1,63 @@ +type: object +description: The properties to update in the address. +properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. +x-schemaName: StoreUpdateCustomerAddress diff --git a/www/apps/api-reference/specs/store/components/schemas/UpdateAddress.yaml b/www/apps/api-reference/specs/store/components/schemas/UpdateAddress.yaml index 6968ed3b46b90..08c8e679b52f4 100644 --- a/www/apps/api-reference/specs/store/components/schemas/UpdateAddress.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/UpdateAddress.yaml @@ -60,3 +60,6 @@ properties: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/components/schemas/UpdateCartData.yaml b/www/apps/api-reference/specs/store/components/schemas/UpdateCartData.yaml index 32376f2c2f892..e7f19ea4968bc 100644 --- a/www/apps/api-reference/specs/store/components/schemas/UpdateCartData.yaml +++ b/www/apps/api-reference/specs/store/components/schemas/UpdateCartData.yaml @@ -47,3 +47,6 @@ properties: metadata: type: object description: The cart's metadata, ca hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata diff --git a/www/apps/api-reference/specs/store/openapi.full.yaml b/www/apps/api-reference/specs/store/openapi.full.yaml index a064f6bedd27a..5f19f1748cc47 100644 --- a/www/apps/api-reference/specs/store/openapi.full.yaml +++ b/www/apps/api-reference/specs/store/openapi.full.yaml @@ -1360,7 +1360,7 @@ paths: ``` description: Emitted when the customer in the cart is transferred. deprecated: false - version: 2.8.0 + since: 2.8.0 /store/carts/{id}/gift-cards: post: operationId: PostCartsIdGiftCards @@ -2037,21 +2037,7 @@ paths: content: application/json: schema: - type: object - description: The shipping method's details. - required: - - option_id - properties: - option_id: - type: string - title: option_id - description: The ID of the shipping option this method is created from. - data: - type: object - description: Any additional data relevant for the third-party fulfillment provider to process the shipment. - externalDocs: - url: https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter - description: Learn more about the data parameter. + $ref: '#/components/schemas/StoreAddCartShippingMethods' x-codeSamples: - lang: JavaScript label: JS SDK @@ -3693,68 +3679,7 @@ paths: content: application/json: schema: - type: object - description: The address's details. - properties: - first_name: - type: string - title: first_name - description: The customer's first name. - last_name: - type: string - title: last_name - description: The customer's last name. - phone: - type: string - title: phone - description: The customer's phone. - company: - type: string - title: company - description: The address's company. - address_1: - type: string - title: address_1 - description: The address's first line. - address_2: - type: string - title: address_2 - description: The address's second line. - city: - type: string - title: city - description: The address's city. - country_code: - type: string - title: country_code - description: The address's country code. - province: - type: string - title: province - description: The address's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_code: - type: string - title: postal_code - description: The address's postal code. - address_name: - type: string - title: address_name - description: The address's name. - is_default_shipping: - type: boolean - title: is_default_shipping - description: Whether the address is used by default for shipping during checkout. - is_default_billing: - type: boolean - title: is_default_billing - description: Whether the address is used by default for billing during checkout. - metadata: - type: object - description: Holds custom key-value pairs. + $ref: '#/components/schemas/StoreCreateCustomerAddress' x-codeSamples: - lang: JavaScript label: JS SDK @@ -3950,68 +3875,7 @@ paths: content: application/json: schema: - type: object - description: The properties to update in the address. - properties: - first_name: - type: string - title: first_name - description: The customer's first name. - last_name: - type: string - title: last_name - description: The customer's last name. - phone: - type: string - title: phone - description: The customer's phone. - company: - type: string - title: company - description: The address's company. - address_1: - type: string - title: address_1 - description: The address's first line. - address_2: - type: string - title: address_2 - description: The address's second line. - city: - type: string - title: city - description: The address's city. - country_code: - type: string - title: country_code - description: The address's country code. - province: - type: string - title: province - description: The address's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_code: - type: string - title: postal_code - description: The address's postal code. - address_name: - type: string - title: address_name - description: The address's name. - is_default_shipping: - type: boolean - title: is_default_shipping - description: Whether the address is used by default for shipping during checkout. - is_default_billing: - type: boolean - title: is_default_billing - description: Whether the address is used by default for billing during checkout. - metadata: - type: object - description: Holds custom key-value pairs. + $ref: '#/components/schemas/StoreUpdateCustomerAddress' x-codeSamples: - lang: JavaScript label: JS SDK @@ -5114,7 +4978,11 @@ paths: operationId: PostPaymentCollectionsIdPaymentSessions summary: Initialize Payment Session of a Payment Collection x-sidebar-summary: Initialize Payment Session - description: Initialize and add a payment session to a payment collection. This is used during checkout, where you create a payment collection for the cart, then initialize a payment session for the payment provider that the customer chooses. + description: | + Initialize and add a payment session to a payment collection. This is used during checkout, where you create a payment collection for the cart, then initialize a payment session for the payment provider that the customer chooses. + It's highly recommended to have an amount greater than `0` in the payment collection, as some payment providers, such as Stripe, require a non-zero amount to create a payment session. Otherwise, an error will be thrown on the payment provider's side. + In cases where you want to create a payment session for a payment collection with an amount of `0`, you can use the Manual System Payment Provider instead of third-party payment providers. The Manual System Payment Provider is built into Medusa and allows you to create payment sessions without interacting with an external payment provider. + Make sure to configure the Manual System Payment Provider in your store's region. Learn more in the [Manage Region](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details) user guide. externalDocs: url: https://docs.medusajs.com/v2/resources/storefront-development/checkout/payment description: 'Storefront guide: How to implement payment during checkout.' @@ -8977,7 +8845,7 @@ paths: ``` description: Emitted when a return is marked as received. deprecated: false - x-version: 2.8.0 + x-since: 2.8.0 /store/shipping-options: get: operationId: GetShippingOptions @@ -9708,6 +9576,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminAddDraftOrderPromotions: type: object description: The details of the promotions to add to a draft order. @@ -9748,6 +9619,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminApiKey: type: object description: The API key's details. @@ -10338,6 +10212,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id @@ -10424,6 +10301,9 @@ components: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. @@ -10611,6 +10491,9 @@ components: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -10809,6 +10692,9 @@ components: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCollectionDeleteResponse: type: object description: The details of the deleted collection. @@ -10904,15 +10790,18 @@ components: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateFulfillment: type: object description: The filfillment's details. x-schemaName: AdminCreateFulfillment required: - - data - items - metadata - order_id + - data - location_id - provider_id - delivery_address @@ -10977,6 +10866,9 @@ components: metadata: type: object description: The delivery address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata items: type: array description: The items to fulfill. @@ -11073,6 +10965,154 @@ components: metadata: type: object description: The fulfillment's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateFulfillmentSetServiceZones: + type: object + description: The service zone's details. + required: + - name + properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - metadata + - country_code + - type + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: country + - type: object + description: A province geo zone. + required: + - metadata + - country_code + - type + - province_code + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: province + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + - type: object + description: A city geo zone + required: + - metadata + - country_code + - type + - province_code + - city + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: city + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + - type: object + description: A ZIP geo zone. + required: + - metadata + - country_code + - type + - province_code + - city + - postal_expression + properties: + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + type: + type: string + title: type + description: The geo zone's type. + default: zip + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + city: + type: string + title: city + description: The geo zone's city. + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + x-schemaName: AdminCreateFulfillmentSetServiceZones AdminCreateGiftCardParams: type: object description: The details of the gift card to create. @@ -11125,6 +11165,9 @@ components: metadata: type: object description: The gift card's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateInventoryItem: type: object description: The inventory item's details. @@ -11185,6 +11228,28 @@ components: metadata: type: object description: The inventory item's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateInventoryLocationLevel: + type: object + description: The inventory level's details. + required: + - location_id + properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. + x-schemaName: AdminCreateInventoryLocationLevel AdminCreateOrderCreditLines: type: object description: The details of a credit line to add to an order. @@ -11212,6 +11277,51 @@ components: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreatePaymentCapture: + type: object + description: The payment's details. + properties: + amount: + type: number + title: amount + description: The amount to capture. + x-schemaName: AdminCreatePaymentCapture + AdminCreatePaymentCollection: + type: object + description: The payment collection's details. + required: + - order_id + - amount + properties: + order_id: + type: string + title: order_id + description: The ID of the associated order. + amount: + type: number + title: amount + description: The amount to be paid. + x-schemaName: AdminCreatePaymentCollection + AdminCreatePaymentRefund: + type: object + description: The refund's details. + properties: + amount: + type: number + title: amount + description: The amount to refund. + refund_reason_id: + type: string + title: refund_reason_id + description: The ID of a refund reason. + note: + type: string + title: note + description: A note to attach to the refund. + x-schemaName: AdminCreatePaymentRefund AdminCreatePriceList: type: object description: The price list's details. @@ -11459,6 +11569,9 @@ components: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id @@ -11505,6 +11618,9 @@ components: metadata: type: object description: The product category's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateProductOption: type: object description: The product option's details. @@ -11538,6 +11654,9 @@ components: metadata: type: object description: The product tag's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateProductType: type: object description: The details of the product type to create. @@ -11548,6 +11667,9 @@ components: metadata: type: object description: The product's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata value: type: string title: value @@ -11627,6 +11749,9 @@ components: metadata: type: object description: The variant's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The variant's prices. @@ -11794,6 +11919,9 @@ components: metadata: type: object description: The region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateReservation: type: object description: The reservation's details. @@ -11826,6 +11954,9 @@ components: metadata: type: object description: The reservation's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateReturnReason: type: object description: The details of the return reason to create. @@ -11853,6 +11984,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateSalesChannel: type: object description: The sales channel's details. @@ -11875,6 +12009,9 @@ components: metadata: type: object description: The sales channel's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateShipment: type: object description: The shipment's details. @@ -12067,6 +12204,9 @@ components: metadata: type: object description: The shipping profile's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateStockLocation: type: object description: The stock location's details. @@ -12087,6 +12227,25 @@ components: metadata: type: object description: The stock location's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminCreateStockLocationFulfillmentSet: + type: object + description: The fulfillment set to create. + required: + - type + - name + properties: + name: + type: string + title: name + description: The fulfillment set's name. + type: + type: string + title: type + description: The fulfillment set's type. + x-schemaName: AdminCreateStockLocationFulfillmentSet AdminCreateStoreCreditAccount: type: object description: The details of the store credit account to create. @@ -12107,6 +12266,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateTaxRate: type: object description: The tax rate's details. @@ -12152,6 +12314,9 @@ components: metadata: type: object description: The tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminCreateTaxRateRule: type: object description: The tax rate rule's details. @@ -12223,9 +12388,15 @@ components: metadata: type: object description: The default tax rate's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The tax region's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id @@ -12446,6 +12617,9 @@ components: metadata: type: object description: The customer's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by @@ -12557,6 +12731,9 @@ components: metadata: type: object description: The address's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -12604,6 +12781,9 @@ components: metadata: type: object description: The customer group's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -13355,6 +13535,9 @@ components: metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -13711,6 +13894,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -13837,6 +14023,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -14014,6 +14203,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -14214,6 +14406,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -14455,6 +14650,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -14547,6 +14745,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -15252,6 +15453,9 @@ components: metadata: type: object description: The location level's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_item: type: object available_quantity: @@ -15296,6 +15500,9 @@ components: metadata: type: object description: The invite's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -15306,6 +15513,24 @@ components: format: date-time title: updated_at description: The date the invite was updated. + AdminInviteAccept: + type: object + description: The details of the user to be created. + properties: + email: + type: string + title: email + description: The user's email. + format: email + first_name: + type: string + title: first_name + description: The user's first name. + last_name: + type: string + title: last_name + description: The user's last name. + x-schemaName: AdminInviteAccept AdminInviteResponse: type: object description: The invite's details. @@ -15327,6 +15552,17 @@ components: title: remove description: The ID of a product. x-schemaName: AdminLinkPriceListProducts + AdminMarkPaymentCollectionPaid: + type: object + description: The payment details. + required: + - order_id + properties: + order_id: + type: string + title: order_id + description: The ID of the order associated with the payment collection. + x-schemaName: AdminMarkPaymentCollectionPaid AdminNotification: type: object description: The notification's details. @@ -15577,6 +15813,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -15758,6 +15997,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -15881,6 +16123,9 @@ components: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at @@ -16099,6 +16344,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -16322,6 +16570,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -16593,6 +16844,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -16719,6 +16973,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -16896,6 +17153,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -17103,6 +17363,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -17287,6 +17550,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -17527,6 +17793,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostClaimsItemsActionReqSchema: type: object description: The details to update in the item. @@ -17560,6 +17829,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostClaimsShippingReqSchema: type: object description: The details of the shipping method used to ship outbound items. @@ -17586,6 +17858,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesAddItemsReqSchema: type: object description: The details of outbound items. @@ -17624,6 +17899,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesItemsActionReqSchema: type: object description: The details to update in an outbound item. @@ -17657,6 +17935,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesReturnRequestItemsReqSchema: type: object description: The details of the inbound (return) items. @@ -17695,6 +17976,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesShippingActionReqSchema: type: object description: The details of the shipping method to update. @@ -17711,6 +17995,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostExchangesShippingReqSchema: type: object description: The outbound shipping method's details. @@ -17737,6 +18024,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderClaimsReqSchema: type: object description: The claim's details. @@ -17770,6 +18060,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsAddItemsReqSchema: type: object description: The details of items to be edited. @@ -17808,6 +18101,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata compare_at_unit_price: type: number title: compare_at_unit_price @@ -17855,6 +18151,9 @@ components: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsShippingActionReqSchema: type: object description: The shipping method's details. @@ -17871,6 +18170,9 @@ components: metadata: type: object description: The order edit's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsShippingReqSchema: type: object description: The shipping method's details. @@ -17897,6 +18199,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostOrderEditsUpdateItemQuantityReqSchema: type: object description: The order item's details to update. @@ -17942,6 +18247,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReceiveReturnsReqSchema: type: object description: The return receival details. @@ -17958,6 +18266,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsConfirmRequestReqSchema: type: object description: The confirmation's details. @@ -18054,6 +18365,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsRequestItemsActionReqSchema: type: object description: The details to update in the item. @@ -18074,6 +18388,9 @@ components: metadata: type: object description: The claim's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsRequestItemsReqSchema: type: object description: The items' details. @@ -18112,6 +18429,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsReturnReqSchema: type: object description: The return's details. @@ -18128,6 +18448,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsShippingActionReqSchema: type: object description: The shipping method's details. @@ -18144,6 +18467,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPostReturnsShippingReqSchema: type: object description: The shipping method's details. @@ -18170,6 +18496,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminPrice: type: object description: The price's details. @@ -18684,6 +19013,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -18812,6 +19144,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -18963,6 +19298,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -19001,6 +19339,9 @@ components: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -19074,6 +19415,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -19134,6 +19478,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminProductTagDeleteResponse: type: object description: The details of the product tag deletion. @@ -19233,6 +19580,9 @@ components: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminProductTypeDeleteResponse: type: object description: The details of the product type deletion. @@ -19431,6 +19781,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata inventory_items: type: array description: The variant's inventory items. @@ -19845,6 +20198,9 @@ components: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -19894,6 +20250,9 @@ components: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -20007,6 +20366,9 @@ components: metadata: type: object description: The reservation's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_by: type: string title: created_by @@ -20159,6 +20521,9 @@ components: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminReturnPreviewResponse: type: object description: The details of a return and a preview of the order once the return is applied. @@ -20201,6 +20566,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -20373,6 +20741,9 @@ components: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -20586,6 +20957,9 @@ components: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -20810,8 +21184,18 @@ components: - gte - nin value: - type: string - title: value + oneOf: + - type: string + title: value + description: The shipping option rule's value. + example: 'true' + - type: array + description: The shipping option rule's values. + items: + type: string + title: value + description: A value of the shipping option rule. + example: 'true' shipping_option_id: type: string title: shipping_option_id @@ -20900,6 +21284,9 @@ components: metadata: type: object description: The shipping profile's metadata, holds custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -21154,6 +21541,9 @@ components: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -21216,6 +21606,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -21413,6 +21806,9 @@ components: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata tax_region_id: type: string title: tax_region_id @@ -21540,6 +21936,9 @@ components: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata parent_id: type: string title: parent_id @@ -21660,6 +22059,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -21708,6 +22110,9 @@ components: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminTransactionGroupsResponse: type: object description: The paginated list of transaction groups. @@ -21814,6 +22219,9 @@ components: metadata: type: object description: The customer group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateDraftOrder: type: object description: The data to update in the draft order. @@ -21876,6 +22284,9 @@ components: metadata: type: object description: The shipping address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The draft order's billing address. @@ -21928,9 +22339,15 @@ components: metadata: type: object description: The billing address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The draft order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata customer_id: type: string title: customer_id @@ -21965,6 +22382,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateDraftOrderItem: type: object description: The updates to make on a draft order's item. @@ -22005,6 +22425,165 @@ components: type: string title: internal_note description: A note viewed only by admin users about the shipping method. + AdminUpdateFulfillmentSetServiceZones: + type: object + description: The service zone's details. + properties: + name: + type: string + title: name + description: The service zone's name. + geo_zones: + type: array + description: The service zone's associated geo zones. + items: + oneOf: + - type: object + description: A country geo zone. + required: + - type + - metadata + - country_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: country + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A province geo zone. + required: + - type + - metadata + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: province + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A city geo zone + required: + - type + - metadata + - city + - country_code + - province_code + properties: + type: + type: string + title: type + description: The geo zone's type. + default: city + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + id: + type: string + title: id + description: The ID of an existing geo zone. + - type: object + description: A ZIP geo zone. + required: + - type + - metadata + - city + - country_code + - province_code + - postal_expression + properties: + type: + type: string + title: type + description: The geo zone's type. + default: zip + metadata: + type: object + description: The geo zone's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + city: + type: string + title: city + description: The geo zone's city. + country_code: + type: string + title: country_code + description: The geo zone's country code. + province_code: + type: string + title: province_code + description: The geo zone's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_expression: + type: object + description: The geo zone's postal expression or ZIP code. + id: + type: string + title: id + description: The ID of an existing geo zone. + x-schemaName: AdminUpdateFulfillmentSetServiceZones AdminUpdateGiftCardParams: type: object description: The details to update in the gift card. @@ -22040,6 +22619,85 @@ components: metadata: type: object description: The gift card's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdateInventoryItem: + type: object + description: The properties to update in the inventory item. + properties: + sku: + type: string + title: sku + description: The inventory item's SKU. + hs_code: + type: string + title: hs_code + description: The inventory item's HS code. + weight: + type: number + title: weight + description: The inventory item's weight. + length: + type: number + title: length + description: The inventory item's length. + height: + type: number + title: height + description: The inventory item's height. + width: + type: number + title: width + description: The inventory item's width. + origin_country: + type: string + title: origin_country + description: The inventory item's origin country. + mid_code: + type: string + title: mid_code + description: The inventory item's MID code. + material: + type: string + title: material + description: The inventory item's material. + title: + type: string + title: title + description: The inventory item's title. + description: + type: string + title: description + description: The inventory item's description. + requires_shipping: + type: boolean + title: requires_shipping + description: Whether the inventory item requires shipping. + thumbnail: + type: string + title: thumbnail + description: The URL of an image to be used as the inventory item's thumbnail. You can use the Upload API routes to upload an image and get its URL. + metadata: + type: object + description: The inventory item's metadata. Can be custom data in key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateInventoryItem + AdminUpdateInventoryLocationLevel: + type: object + description: The properties to update in the inventory level. + properties: + stocked_quantity: + type: number + title: stocked_quantity + description: The inventory level's stocked quantity. + incoming_quantity: + type: number + title: incoming_quantity + description: The inventory level's incoming quantity. + x-schemaName: AdminUpdateInventoryLocationLevel AdminUpdateOrder: type: object description: The details to update in the order. @@ -22102,6 +22760,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata billing_address: type: object description: The order's billing address. @@ -22154,9 +22815,28 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdatePaymentRefundReason: + type: object + description: The properties to update in the refund reason. + properties: + label: + type: string + title: label + description: The refund reason's label. + description: + type: string + title: description + description: The refund reason's description. + x-schemaName: AdminUpdatePaymentRefundReason AdminUpdatePriceList: type: object description: the details to update in a price list. @@ -22363,10 +23043,52 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata external_id: type: string title: external_id description: The ID of the product in an external or third-party system. + AdminUpdateProductCategory: + type: object + description: The properties to update in the product category. + properties: + name: + type: string + title: name + description: The product category's name. + description: + type: string + title: description + description: The product category's description. + handle: + type: string + title: handle + description: The product category's handle. Must be a unique value. + is_internal: + type: boolean + title: is_internal + description: Whether the product category is only used for internal purposes and shouldn't be shown the customer. + is_active: + type: boolean + title: is_active + description: Whether the product category is active. + parent_category_id: + type: string + title: parent_category_id + description: The ID of a parent category. + metadata: + type: object + description: The product category's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + rank: + type: number + title: rank + description: The product category's rank among other categories. + x-schemaName: AdminUpdateProductCategory AdminUpdateProductOption: type: object description: The details to update in a product option. @@ -22383,6 +23105,36 @@ components: type: string title: values description: An option value. + AdminUpdateProductTag: + type: object + description: The properties to update in the product tag. + properties: + value: + type: string + title: value + description: The product tag's value. + metadata: + type: object + description: The product tag's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateProductTag + AdminUpdateProductType: + type: object + description: The properties to update in the product type. + properties: + value: + type: string + title: value + description: The product type's value. + metadata: + type: object + description: The product type's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateProductType AdminUpdateProductVariant: type: object description: The properties to update of a product variant. @@ -22455,6 +23207,9 @@ components: metadata: type: object description: The product variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata prices: type: array description: The product variant's prices. @@ -22508,6 +23263,70 @@ components: description: An attribute value. example: prod_123 x-schemaName: AdminUpdatePromotionRule + AdminUpdateRegion: + type: object + description: The propeties to update in the region. + properties: + name: + type: string + title: name + description: The region's name. + currency_code: + type: string + title: currency_code + description: The region's currency code. + countries: + type: array + description: The region's countries. + items: + type: string + title: countries + description: A country code. + automatic_taxes: + type: boolean + title: automatic_taxes + description: Whether taxes are calculated automatically for carts in the region. + payment_providers: + type: array + description: The payment providers enabled in the region. + items: + type: string + title: payment_providers + description: A payment provider's ID. + metadata: + type: object + description: The region's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + is_tax_inclusive: + type: boolean + title: is_tax_inclusive + description: Whether the prices in the region are tax inclusive. + x-schemaName: AdminUpdateRegion + AdminUpdateReservation: + type: object + description: The properties to update in the reservation. + properties: + location_id: + type: string + title: location_id + description: The ID of the associated location. + quantity: + type: number + title: quantity + description: The reserved quantity. + description: + type: string + title: description + description: The reservation's description. + metadata: + type: object + description: The reservation's metadata. Can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateReservation AdminUpdateReturnReason: type: object description: The details to update in a return reason. @@ -22528,6 +23347,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata required: - value - label @@ -22551,6 +23373,177 @@ components: metadata: type: object description: The sales channel's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + AdminUpdateShippingOption: + type: object + description: The properties to update in the shipping option. + properties: + name: + type: string + title: name + description: The shipping option's name. + data: + type: object + description: The shipping option's data that is useful for third-party providers. + externalDocs: + url: https://docs.medusajs.com/v2/resources/commerce-modules/fulfillment/shipping-option#data-property + price_type: + type: string + description: | + The type of the shipping option's price. If `calculated`, its price is retrieved by the associated fulfillment provider during checkout. If `flat`, its price is set in the `prices` property. + enum: + - calculated + - flat + provider_id: + type: string + title: provider_id + description: The ID of the associated fulfillment provider that is used to process the option. + shipping_profile_id: + type: string + title: shipping_profile_id + description: The ID of the shipping profile this shipping option belongs to. + type: + type: object + description: The shipping option's type. + required: + - code + - description + - label + properties: + label: + type: string + title: label + description: The type's label. + description: + type: string + title: description + description: The type's description. + code: + type: string + title: code + description: The type's code. + prices: + type: array + description: The shipping option's prices. If the `price_type` is `calculated`, pass an empty array. + items: + oneOf: + - type: object + description: The shipping option's price for a currency code. + properties: + id: + type: string + title: id + description: The ID of an existing price. + currency_code: + type: string + title: currency_code + description: The price's currency code. + amount: + type: number + title: amount + description: The price's amount. + - type: object + description: The shipping option's price for a region. + properties: + id: + type: string + title: id + description: The ID of an existing price. + region_id: + type: string + title: region_id + description: The ID of the associated region. + amount: + type: number + title: amount + description: The price's amount. + rules: + type: array + description: The shipping option's rules. + items: + oneOf: + - type: object + description: The details of a new shipping option rule. + required: + - operator + - attribute + - value + properties: + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: object + description: Update the properties of an existing rule. + required: + - id + - operator + - attribute + - value + properties: + id: + type: string + title: id + description: The rule's ID. + operator: + type: string + description: The operator used to check whether a rule applies. + enum: + - in + - eq + - ne + - gt + - gte + - lt + - lte + - nin + attribute: + type: string + title: attribute + description: The name of a property or table that the rule applies to. + example: customer_group + value: + oneOf: + - type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + - type: array + description: Values of the attribute that enable this rule. + items: + type: string + title: value + description: A value of the attribute that enables this rule. + example: cusgroup_123 + x-schemaName: AdminUpdateShippingOption AdminUpdateShippingOptionRule: type: object description: The properties to update in the shipping option rule. @@ -22595,6 +23588,25 @@ components: description: A value of the attribute that enables this rule. example: cusgroup_123 x-schemaName: AdminUpdateShippingOptionRule + AdminUpdateShippingProfile: + type: object + description: The properties to update in the shipping profile. + properties: + name: + type: string + title: name + description: The shipping profile's name. + type: + type: string + title: type + description: The shipping profile's type. + metadata: + type: object + description: The shipping profile's metadata. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata + x-schemaName: AdminUpdateShippingProfile AdminUpdateStockLocation: type: object description: The properties to update in a stock location. @@ -22655,6 +23667,9 @@ components: metadata: type: object description: The stock location's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateStore: type: object description: The properties to update in a store. @@ -22701,6 +23716,9 @@ components: metadata: type: object description: The store's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateTaxRate: type: object description: The properties to update in the tax rate. @@ -22751,6 +23769,9 @@ components: metadata: type: object description: The tax rate's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateTaxRegion: type: object description: The details to update in a tax region. @@ -22767,6 +23788,9 @@ components: metadata: type: object description: The tax region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata provider_id: type: string title: provider_id @@ -22791,6 +23815,9 @@ components: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata AdminUpdateVariantInventoryItem: type: object description: The properties to update of the variant's inventory item association. @@ -22952,6 +23979,9 @@ components: metadata: type: object description: The user's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/admin#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -23520,6 +24550,9 @@ components: metadata: type: object description: The cart's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -23758,6 +24791,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -23874,6 +24910,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -23984,6 +25023,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -24042,6 +25084,9 @@ components: metadata: type: object description: The collection's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseExchangeItem: type: object description: The item's details. @@ -24078,6 +25123,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -24361,6 +25409,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -24533,6 +25584,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -24606,6 +25660,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -24859,6 +25916,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -25138,6 +26198,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -25399,6 +26462,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -25539,6 +26605,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -25718,6 +26787,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -25844,6 +26916,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -25910,6 +26985,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -25948,6 +27026,9 @@ components: metadata: type: object description: The product option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -25988,6 +27069,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -26039,6 +27123,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseProductType: type: object description: The product type's details. @@ -26075,6 +27162,9 @@ components: metadata: type: object description: The type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BaseProductVariant: type: object description: The product variant's details. @@ -26204,6 +27294,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata BasePromotionRuleValue: type: object description: The rule value's details. @@ -26296,6 +27389,9 @@ components: metadata: type: object description: The region's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -26614,6 +27710,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata CustomerGroupInCustomerFilters: type: object description: Filter by customer groups to get their associated customers. @@ -27070,6 +28169,9 @@ components: metadata: type: object description: The inventory level's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata Order: type: object description: The order change's order. @@ -27248,6 +28350,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata canceled_at: type: string format: date-time @@ -27442,6 +28547,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -27565,6 +28673,9 @@ components: metadata: type: object description: The order change's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata declined_at: type: string title: declined_at @@ -27756,6 +28867,9 @@ components: metadata: type: object description: The claim's metadata, used to store custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -27842,6 +28956,9 @@ components: metadata: type: object description: The credit line's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -27914,6 +29031,9 @@ components: metadata: type: object description: The exchange's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -28027,6 +29147,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -28181,6 +29304,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -28399,6 +29525,9 @@ components: metadata: type: object description: The return item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata order_id: type: string title: order_id @@ -28474,6 +29603,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -28694,6 +29826,9 @@ components: metadata: type: object description: The transaction's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -28736,6 +29871,9 @@ components: metadata: type: object description: The refund reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -28805,6 +29943,9 @@ components: metadata: type: object description: The return's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -28909,6 +30050,26 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreAddCartShippingMethods: + type: object + description: The shipping method's details. + required: + - option_id + properties: + option_id: + type: string + title: option_id + description: The ID of the shipping option this method is created from. + data: + type: object + description: Any additional data relevant for the third-party fulfillment provider to process the shipment. + externalDocs: + url: https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter + description: Learn more about the data parameter. + x-schemaName: StoreAddCartShippingMethods StoreAddGiftCardToCart: type: object description: The details to add a gift card to the cart. @@ -29133,6 +30294,9 @@ components: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -29315,6 +30479,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at @@ -29605,6 +30772,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string title: created_at @@ -29785,6 +30955,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -30128,6 +31301,9 @@ components: metadata: type: object description: The collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreCollectionResponse: type: object description: The collection's details. @@ -30172,6 +31348,9 @@ components: metadata: type: object description: The cart's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreCreateCustomer: type: object description: The details of the customer to create. @@ -30203,6 +31382,73 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreCreateCustomerAddress: + type: object + description: The address's details. + properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. + x-schemaName: StoreCreateCustomerAddress StoreCreatePaymentCollection: type: object description: The details of the payment collection to create. @@ -30436,6 +31682,9 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -30543,6 +31792,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -30692,31 +31944,6 @@ components: format: date-time title: updated_at description: The date the gift card was updated. - StoreGiftCardInvitation: - type: object - description: The gift card invitation's details. - x-schemaName: StoreGiftCardInvitation - required: - - id - - email - - status - - gift_card - properties: - id: - type: string - title: id - description: The gift card invitation's ID. - email: - type: string - title: email - description: The gift card invitation's email. - format: email - status: - type: string - title: status - description: The gift card invitation's status. - gift_card: - $ref: '#/components/schemas/StoreGiftCard' StoreGiftCardResponse: type: object description: The gift card's details. @@ -30872,6 +32099,9 @@ components: metadata: type: object description: The order's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -31053,6 +32283,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -31126,6 +32359,9 @@ components: metadata: type: object description: The fulfillment's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -31492,6 +32728,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -31673,6 +32912,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -31937,6 +33179,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -31960,6 +33205,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -32159,6 +33407,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -32344,6 +33595,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -32637,6 +33891,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -32667,6 +33924,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -33021,6 +34281,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata variant_id: type: string title: variant_id @@ -33202,6 +34465,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata product_id: type: string title: product_id @@ -33466,6 +34732,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -33489,6 +34758,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -33684,6 +34956,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -33869,6 +35144,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34162,6 +35440,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34192,6 +35473,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34438,6 +35722,9 @@ components: metadata: type: object description: The variant's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34623,6 +35910,9 @@ components: metadata: type: object description: The product's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34916,6 +36206,9 @@ components: metadata: type: object description: The detail's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -34946,6 +36239,9 @@ components: metadata: type: object description: The item's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -35101,6 +36397,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -35226,6 +36525,9 @@ components: metadata: type: object description: The shipping method's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -35303,6 +36605,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -35834,6 +37139,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -35952,6 +37260,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata tax_lines: type: array description: The shipping method's tax lines. @@ -36479,6 +37790,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -36835,6 +38149,9 @@ components: metadata: type: object description: The shipping method's metadata. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata original_total: type: number title: original_total @@ -36931,6 +38248,9 @@ components: metadata: type: object description: The payment collection's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata status: type: string description: The payment collection's status. @@ -37173,6 +38493,9 @@ components: metadata: type: object description: The product's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -37316,6 +38639,9 @@ components: metadata: type: object description: The category's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -37412,6 +38738,9 @@ components: metadata: type: object description: The image's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata rank: type: number title: rank @@ -37443,6 +38772,9 @@ components: metadata: type: object description: The option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -37486,6 +38818,9 @@ components: metadata: type: object description: The value's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -37541,6 +38876,9 @@ components: metadata: type: object description: The tag's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata required: - id - value @@ -37604,6 +38942,9 @@ components: metadata: type: object description: The product type's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -37687,6 +39028,9 @@ components: metadata: type: object description: The variant's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata id: type: string title: id @@ -37838,6 +39182,9 @@ components: metadata: type: object description: The region's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -38017,6 +39364,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreReturnReason: type: object description: The return reason's details. @@ -38047,6 +39397,9 @@ components: metadata: type: object description: The return reason's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -38144,6 +39497,9 @@ components: metadata: type: object description: The shipping option's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreShippingOptionListResponse: type: object description: The shipping option's details. @@ -38266,6 +39622,9 @@ components: metadata: type: object description: The store credit account's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata created_at: type: string format: date-time @@ -38354,6 +39713,9 @@ components: metadata: type: object description: The transaction group's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata x-schemaName: StoreTransactionGroup StoreUpdateCartLineItem: type: object @@ -38369,6 +39731,9 @@ components: metadata: type: object description: The item's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata StoreUpdateCustomer: type: object description: The details to update in the customer. @@ -38393,6 +39758,73 @@ components: metadata: type: object description: The customer's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata + StoreUpdateCustomerAddress: + type: object + description: The properties to update in the address. + properties: + first_name: + type: string + title: first_name + description: The customer's first name. + last_name: + type: string + title: last_name + description: The customer's last name. + phone: + type: string + title: phone + description: The customer's phone. + company: + type: string + title: company + description: The address's company. + address_1: + type: string + title: address_1 + description: The address's first line. + address_2: + type: string + title: address_2 + description: The address's second line. + city: + type: string + title: city + description: The address's city. + country_code: + type: string + title: country_code + description: The address's country code. + province: + type: string + title: province + description: The address's ISO 3166-2 province code. Must be lower-case. + example: us-ca + externalDocs: + url: https://en.wikipedia.org/wiki/ISO_3166-2 + description: Learn more about ISO 3166-2 + postal_code: + type: string + title: postal_code + description: The address's postal code. + address_name: + type: string + title: address_name + description: The address's name. + is_default_shipping: + type: boolean + title: is_default_shipping + description: Whether the address is used by default for shipping during checkout. + is_default_billing: + type: boolean + title: is_default_billing + description: Whether the address is used by default for billing during checkout. + metadata: + type: object + description: Holds custom key-value pairs. + x-schemaName: StoreUpdateCustomerAddress UpdateAddress: type: object description: The details to update in the address. @@ -38456,6 +39888,9 @@ components: metadata: type: object description: The address's metadata, can hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata UpdateCartData: type: object description: The details to update in a cart. @@ -38502,6 +39937,9 @@ components: metadata: type: object description: The cart's metadata, ca hold custom key-value pairs. + externalDocs: + url: https://docs.medusajs.com/api/store#manage-metadata + description: Learn how to manage metadata WorkflowExecutionContext: type: object description: The workflow execution's context. diff --git a/www/apps/api-reference/specs/store/paths/store_carts_{id}_customer.yaml b/www/apps/api-reference/specs/store/paths/store_carts_{id}_customer.yaml index a084f14ad2b97..7978b0448519c 100644 --- a/www/apps/api-reference/specs/store/paths/store_carts_{id}_customer.yaml +++ b/www/apps/api-reference/specs/store/paths/store_carts_{id}_customer.yaml @@ -87,4 +87,4 @@ post: ``` description: Emitted when the customer in the cart is transferred. deprecated: false - version: 2.8.0 + since: 2.8.0 diff --git a/www/apps/api-reference/specs/store/paths/store_carts_{id}_shipping-methods.yaml b/www/apps/api-reference/specs/store/paths/store_carts_{id}_shipping-methods.yaml index 441aba7d3fcf2..989c96b30150d 100644 --- a/www/apps/api-reference/specs/store/paths/store_carts_{id}_shipping-methods.yaml +++ b/www/apps/api-reference/specs/store/paths/store_carts_{id}_shipping-methods.yaml @@ -47,24 +47,7 @@ post: content: application/json: schema: - type: object - description: The shipping method's details. - required: - - option_id - properties: - option_id: - type: string - title: option_id - description: The ID of the shipping option this method is created from. - data: - type: object - description: >- - Any additional data relevant for the third-party fulfillment - provider to process the shipment. - externalDocs: - url: >- - https://docs.medusajs.com/v2/resources/storefront-development/checkout/shipping#data-request-body-parameter - description: Learn more about the data parameter. + $ref: ../components/schemas/StoreAddCartShippingMethods.yaml x-codeSamples: - lang: JavaScript label: JS SDK 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 d49b326f54f1b..908ce119d4291 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 @@ -225,72 +225,7 @@ post: content: application/json: schema: - type: object - description: The address's details. - properties: - first_name: - type: string - title: first_name - description: The customer's first name. - last_name: - type: string - title: last_name - description: The customer's last name. - phone: - type: string - title: phone - description: The customer's phone. - company: - type: string - title: company - description: The address's company. - address_1: - type: string - title: address_1 - description: The address's first line. - address_2: - type: string - title: address_2 - description: The address's second line. - city: - type: string - title: city - description: The address's city. - country_code: - type: string - title: country_code - description: The address's country code. - province: - type: string - title: province - description: The address's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_code: - type: string - title: postal_code - description: The address's postal code. - address_name: - type: string - title: address_name - description: The address's name. - is_default_shipping: - type: boolean - title: is_default_shipping - description: >- - Whether the address is used by default for shipping during - checkout. - is_default_billing: - type: boolean - title: is_default_billing - description: >- - Whether the address is used by default for billing during - checkout. - metadata: - type: object - description: Holds custom key-value pairs. + $ref: ../components/schemas/StoreCreateCustomerAddress.yaml x-codeSamples: - lang: JavaScript label: JS SDK 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 dd98c7af31882..132070ae47993 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 @@ -139,72 +139,7 @@ post: content: application/json: schema: - type: object - description: The properties to update in the address. - properties: - first_name: - type: string - title: first_name - description: The customer's first name. - last_name: - type: string - title: last_name - description: The customer's last name. - phone: - type: string - title: phone - description: The customer's phone. - company: - type: string - title: company - description: The address's company. - address_1: - type: string - title: address_1 - description: The address's first line. - address_2: - type: string - title: address_2 - description: The address's second line. - city: - type: string - title: city - description: The address's city. - country_code: - type: string - title: country_code - description: The address's country code. - province: - type: string - title: province - description: The address's ISO 3166-2 province code. Must be lower-case. - example: us-ca - externalDocs: - url: https://en.wikipedia.org/wiki/ISO_3166-2 - description: Learn more about ISO 3166-2 - postal_code: - type: string - title: postal_code - description: The address's postal code. - address_name: - type: string - title: address_name - description: The address's name. - is_default_shipping: - type: boolean - title: is_default_shipping - description: >- - Whether the address is used by default for shipping during - checkout. - is_default_billing: - type: boolean - title: is_default_billing - description: >- - Whether the address is used by default for billing during - checkout. - metadata: - type: object - description: Holds custom key-value pairs. + $ref: ../components/schemas/StoreUpdateCustomerAddress.yaml x-codeSamples: - lang: JavaScript label: JS SDK diff --git a/www/apps/api-reference/specs/store/paths/store_payment-collections_{id}_payment-sessions.yaml b/www/apps/api-reference/specs/store/paths/store_payment-collections_{id}_payment-sessions.yaml index 39c8f7fc8034f..d92626515c4a9 100644 --- a/www/apps/api-reference/specs/store/paths/store_payment-collections_{id}_payment-sessions.yaml +++ b/www/apps/api-reference/specs/store/paths/store_payment-collections_{id}_payment-sessions.yaml @@ -2,11 +2,27 @@ post: operationId: PostPaymentCollectionsIdPaymentSessions summary: Initialize Payment Session of a Payment Collection x-sidebar-summary: Initialize Payment Session - description: >- + description: > Initialize and add a payment session to a payment collection. This is used during checkout, where you create a payment collection for the cart, then initialize a payment session for the payment provider that the customer chooses. + + It's highly recommended to have an amount greater than `0` in the payment + collection, as some payment providers, such as Stripe, require a non-zero + amount to create a payment session. Otherwise, an error will be thrown on + the payment provider's side. + + In cases where you want to create a payment session for a payment collection + with an amount of `0`, you can use the Manual System Payment Provider + instead of third-party payment providers. The Manual System Payment Provider + is built into Medusa and allows you to create payment sessions without + interacting with an external payment provider. + + Make sure to configure the Manual System Payment Provider in your store's + region. Learn more in the [Manage + Region](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details) + user guide. externalDocs: url: >- https://docs.medusajs.com/v2/resources/storefront-development/checkout/payment diff --git a/www/apps/api-reference/specs/store/paths/store_returns.yaml b/www/apps/api-reference/specs/store/paths/store_returns.yaml index b793d2cc0a1c6..8f37d060d9516 100644 --- a/www/apps/api-reference/specs/store/paths/store_returns.yaml +++ b/www/apps/api-reference/specs/store/paths/store_returns.yaml @@ -67,4 +67,4 @@ post: ``` description: Emitted when a return is marked as received. deprecated: false - x-version: 2.8.0 + x-since: 2.8.0 diff --git a/www/apps/book/.env.sample b/www/apps/book/.env.sample index f9ea64ec27f34..59aaeddf1afcf 100644 --- a/www/apps/book/.env.sample +++ b/www/apps/book/.env.sample @@ -17,4 +17,5 @@ CLOUDINARY_CLOUD_NAME= NEXT_PUBLIC_BASE_PATH= NEXT_PUBLIC_GA_ID= NEXT_PUBLIC_PROD_BASE_URL= -NEXT_PUBLIC_INTEGRATION_ID= \ No newline at end of file +NEXT_PUBLIC_INTEGRATION_ID= +NEXT_MCP_SERVER_URL= \ No newline at end of file diff --git a/www/apps/book/app/learn/configurations/medusa-config/page.mdx b/www/apps/book/app/learn/configurations/medusa-config/page.mdx index 3a73320420bdd..6336f12997a5c 100644 --- a/www/apps/book/app/learn/configurations/medusa-config/page.mdx +++ b/www/apps/book/app/learn/configurations/medusa-config/page.mdx @@ -70,6 +70,87 @@ module.exports = defineConfig({ The `projectConfig` object contains essential configurations related to the Medusa application, such as database and CORS configurations. +### cookieOptions + + + +This option is available since Medusa [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5). + + + +The `projectConfig.cookieOptions` configuration defines cookie options to be passed to `express-session` when creating the session cookie. This configuration is useful when simulating a production environment locally, where you may need to set options like `secure` or `sameSite`. + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + cookieOptions: { + sameSite: "lax", + }, + // ... + }, + // ... +}) +``` + +#### Properties + +Aside from the following options, you can pass any property that the [express-session's cookie option accepts](https://www.npmjs.com/package/express-session). + + + ### databaseDriverOptions The `projectConfig.databaseDriverOptions` configuration is an object of additional options used to configure the PostgreSQL connection. For example, you can support TLS/SSL connection using this configuration's `ssl` property. @@ -758,7 +839,7 @@ The value for this configuration can be one of the following: ```ts title="medusa-config.ts" module.exports = defineConfig({ projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", + workerMode: process.env.WORKER_MODE as "shared" | "worker" | "server" || "shared", // ... }, // ... diff --git a/www/apps/book/app/learn/configurations/ts-aliases/page.mdx b/www/apps/book/app/learn/configurations/ts-aliases/page.mdx index 07b9d27b57697..ba8b48a42c186 100644 --- a/www/apps/book/app/learn/configurations/ts-aliases/page.mdx +++ b/www/apps/book/app/learn/configurations/ts-aliases/page.mdx @@ -4,17 +4,30 @@ export const metadata = { # {metadata.title} -By default, Medusa doesn't support TypeScript aliases in production. +In this chapter, you'll learn how to use TypeScript aliases in your Medusa application. -If you prefer using TypeScript aliases, install following development dependencies: +## Support for TypeScript Aliases + +By default, Medusa doesn't support TypeScript aliases in production. That means you may get build errors in production if you use them in your development. + +If you prefer using TypeScript aliases, this section will guide you through the steps to enable them in your Medusa application. + +### Step 1: Install Required Dependencies + +Start by installing the following development dependencies: ```bash npm2yarn npm install --save-dev tsc-alias rimraf ``` -Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. +Where: -Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: +- `tsc-alias` resolves TypeScript aliases. +- `rimraf` removes files and directories. + +### Step 2: Update `package.json` + +Then, add a new `resolve:aliases` script to your `package.json` and update the existing `build` script: ```json title="package.json" { @@ -26,7 +39,11 @@ Then, add a new `resolve:aliases` script to your `package.json` and update the ` } ``` -You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: +### Step 3: Update `tsconfig.json` + +Next, configure the TypeScript aliases you want to use in your `tsconfig.json` file by adding a `paths` property under `compilerOptions`. + +For example, to import anything under the `src` directory using type aliases, add the following in `tsconfig.json`: ```json title="tsconfig.json" { @@ -39,8 +56,110 @@ You can now use TypeScript aliases in your Medusa application. For example, add } ``` -Now, you can import modules, for example, using TypeScript aliases: +### Step 4: Use TypeScript Aliases + +Then, you can use the `@` alias in your application code. + +For example, if you have a service in `src/modules/brand/service.ts`, you can import it like this: ```ts import { BrandModuleService } from "@/modules/brand/service" ``` + +--- + +## Support TypeScript Aliases for Admin Customizations + +Medusa also doesn't support TypeScript aliases in the admin customizations by default. However, you can also configure your Medusa application to use TypeScript aliases in your admin customizations. + +### Step 1: Update `src/admin/tsconfig.json` + +Update `src/admin/tsconfig.json` to include `baseUrl` and `paths` configuration: + +```json title="src/admin/tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + + // other options... + } +} +``` + +The `baseUrl` option sets the base directory to `src/admin`, and the `paths` option defines the `@` alias to allow importing files from the `src/admin` directory using aliases. + +### Step 2: Update `medusa-config.ts` + +Next, update the `vite` configuration in `medusa-config.ts` to include the `resolve.alias` configuration: + +```ts title="medusa-config.ts" +import path from "path" + +module.exports = defineConfig({ + // ... + admin: { + vite: () => ({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src/admin"), + }, + }, + }), + }, +}) +``` + + + +Learn more about the `vite` configuration in the [Medusa configuration](../medusa-config/page.mdx) chapter. + + + +### Step 3: Use TypeScript Aliases in Admin Customizations + +You can now use the `@` alias in your admin customizations, just like you do in your main application code. + +For example, if you have a component in `src/admin/components/Container.tsx`, you can import it in a widget like this: + +```ts +import Container from "@/components/Container" +``` + +### Match TSConfig and Vite Alias Configuration + +Make sure that the `@` alias points to the same path as in your `src/admin/tsconfig.json`. + +For example, if you set the `@/*` alias to point to `./components/*`: + +```json title="src/admin/tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./components/*"] + } + } +} +``` + +Then, the `vite` alias configuration would be: + +```ts title="medusa-config.ts" +import path from "path" + +module.exports = defineConfig({ + // ... + admin: { + vite: () => ({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src/admin/components"), + }, + }, + }), + }, +}) +``` \ No newline at end of file diff --git a/www/apps/book/app/learn/customization/custom-features/page.mdx b/www/apps/book/app/learn/customization/custom-features/page.mdx index 204dfc459576c..ea600389f6cc0 100644 --- a/www/apps/book/app/learn/customization/custom-features/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/page.mdx @@ -8,7 +8,7 @@ In the upcoming chapters, you'll follow step-by-step guides to build custom feat By following these guides, you'll add brands to the Medusa application that you can associate with products. -To build a custom feature in Medusa, you need three main tools: +To build a custom feature in Medusa, you need three main [Framework](../../fundamentals/framework/page.mdx) tools: - [Module](../../fundamentals/modules/page.mdx): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. - [Workflow](../../fundamentals/workflows/page.mdx): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. diff --git a/www/apps/book/app/learn/customization/customize-admin/page.mdx b/www/apps/book/app/learn/customization/customize-admin/page.mdx index 44b44001e2e97..6f370e120d60c 100644 --- a/www/apps/book/app/learn/customization/customize-admin/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/page.mdx @@ -8,10 +8,10 @@ In the previous chapters, you've customized your Medusa application to [add bran After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: -- Insert components, called [widgets](../../fundamentals/admin/widgets/page.mdx), on existing pages. -- Add new pages, called [UI Routes](../../fundamentals/admin/ui-routes/page.mdx). +- Insert components, called [widgets](../../fundamentals/admin/widgets/page.mdx), into existing pages. For example, you can add a widget to the product details page that shows the product's brand. +- Add new pages, called [UI Routes](../../fundamentals/admin/ui-routes/page.mdx). For example, you can create a new page in the dashboard that lists all brands in the store. -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard +Within these customizations, you can send requests to custom API routes, allowing admin users to view and manage custom resources on the dashboard. --- diff --git a/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx b/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx new file mode 100644 index 0000000000000..dc548279cd097 --- /dev/null +++ b/www/apps/book/app/learn/debugging-and-testing/debug-workflows/page.mdx @@ -0,0 +1,462 @@ +import { Table, Prerequisites, CodeTabs, CodeTab } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Debug Workflows`, +} + +# {metadata.title} + +In this chapter, you'll learn about the different ways you can debug workflows in Medusa. + +Debugging workflows is essential to ensure your custom features work as expected. It helps you identify unexpected issues and bugs in your workflow logic. + +## Approaches to Debug Workflows + +There are several ways to debug workflows in Medusa: + + + + + + Approach + + + When to Use + + + + + + + [Write integration tests](#approach-1-write-integration-tests) + + + To ensure your workflow produces the expected results and handles edge cases. + + + + + [Add breakpoints](#approach-2-add-breakpoints) + + + To inspect specific steps in your workflow and understand the data flow. + + + + + [Log messages](#approach-3-log-messages) + + + To check values during execution with minimal overhead. + + + + + [View Workflow Executions in Medusa Admin](#approach-4-monitor-workflow-executions-in-medusa-admin) + + + To monitor stored workflow executions and long-running workflows, especially in production environments. + + + +
+ +--- + +## Approach 1: Write Integration Tests + +Integration tests run your workflow in a controlled environment to verify its behavior and outcome. By writing integration tests, you ensure your workflow produces the expected results and handles edge cases. + +### When to Use Integration Tests + +It's recommended to always write integration tests for your workflows. This helps you catch issues early and ensures your custom logic works as intended. + +### How to Write Integration Tests for Workflows + +Refer to the [Integration Tests](../testing-tools/integration-tests/page.mdx) chapter to learn how to write integration tests for your workflows and find examples of testing workflows. + +--- + +## Approach 2: Add Breakpoints + +Breakpoints allow you to pause workflow execution at specific steps and inspect the data. They're useful for understanding the data flow in your steps and identifying issues. + +### When to Use Breakpoints + +Use breakpoints when you need to debug specific steps in your workflow, rather than the entire workflow. You can verify that the step is behaving as expected and is producing the correct output. + +### Where Can You Add Breakpoints + +Since Medusa stores an internal representation of the workflow constructor on application startup, breakpoints within the workflow's constructor won't work during execution. Learn more in the [Data Manipulation](../../fundamentals/workflows/variable-manipulation/page.mdx) chapter. + +Instead, you can add breakpoints in: + +- A step function. +- A step's compensation function. +- The `transform` callback function of a step. + +For example: + + + + +```ts highlights={[["11"], ["12"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // Add a breakpoint here to inspect the message + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + + + +```ts highlights={[["18"], ["19"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + }, + async () => { + // Add a breakpoint here to inspect the compensation logic + console.log("Compensating step 1") + } +) + +const step2 = createStep( + "step-2", + async () => { + throw new Error("This is an error in step 2") + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + step2() + + return new WorkflowResponse({ + response, + }) + } +) +``` + + + +```ts highlights={[["28"], ["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + const transformedMessage = transform( + { response }, + (data) => { + // Add a breakpoint here to inspect the transformed data + const upperCase = data.response.toUpperCase() + return upperCase + } + ) + + return new WorkflowResponse({ + response: transformedMessage, + }) + } +) +``` + + + +### How to Add Breakpoints + +If your code editor supports adding breakpoints, you can add them in your step and compensation functions, or the `transform` callback function. When the workflow execution reaches the breakpoint, your code editor will pause execution, allowing you to inspect the data and walk through the code. + +If you're using VS Code or Cursor, learn how to add breakpoints in the [VS Code documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_breakpoints). For other code editors, refer to their respective documentation. + +--- + +## Approach 3: Log Messages + +Logging messages is a simple yet effective way to debug code. By logging messages, you can check values during execution with minimal overhead. + +### When to Use Logging + +Use logging when debugging workflows and you want to check values during execution without the overhead of setting up breakpoints. + +Logging is also useful when you want to verify variable values between steps or in a `transform` callback function. + +### How to Log Messages + +Since Medusa stores an internal representation of the workflow constructor on application startup, you can't directly log messages in the workflow's constructor. + +Instead, you can log messages in: + +- A step function. +- A step's compensation function. +- The `transform` callback function of a step. + +You can log messages with `console.log`. In step and compensation functions, you can also use the [Logger](../logging/page.mdx) to log messages with different log levels (info, warn, error). + +For example: + + + + +```ts highlights={[["14"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const logger = container.resolve("logger") + const message = "Hello from step 1!" + + logger.info(`Step 1 output: ${message}`) + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + + + + +```ts highlights={[["22"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const logger = container.resolve("logger") + const message = "Hello from step 1!" + + logger.info(`Step 1 output: ${message}`) + + return new StepResponse( + message + ) + }, + async (_, { container }) => { + const logger = container.resolve("logger") + logger.warn("Compensating step 1") + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + + + + +```ts highlights={[["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + const transformedMessage = transform( + { response }, + (data) => { + const upperCase = data.response.toUpperCase() + console.log("Transformed Data:", upperCase) + return upperCase + } + ) + + return new WorkflowResponse({ + response: transformedMessage, + }) + } +) +``` + + + + +If you execute the workflow, you'll see the logged message in your console. + + + +Learn more about logging in the [Logger](../logging/page.mdx) chapter. + + + +--- + +## Approach 4: Monitor Workflow Executions in Medusa Admin + +The Medusa Admin has a [Workflows](!user-guide!/settings/developer/workflows) settings page that provides a user-friendly interface to view stored workflow executions. + +### When to Use Admin Monitoring + +Use the Medusa Admin to monitor [stored workflow executions](../../fundamentals/workflows/store-executions/page.mdx) when debugging unexpected issues and edge cases, especially in production environments and long-running workflows that run in the background. + +By viewing the workflow executions through the Medusa Admin, you can: + +- View the status of stored workflow executions. +- Inspect input and output data for each execution and its steps. +- Identify any issues or errors in the workflow execution. + +### How to Monitor Workflow Executions in the Admin + +The Workflows settings page in the Medusa Admin shows you the history of stored workflow executions only. Workflow executions are stored if a workflow is [long-running](../../fundamentals/workflows/long-running-workflow/page.mdx), or if the `store` and `retentionTime` options are set on the workflow. + +For example, to store workflow executions: + + + +```ts highlights={[["22"], ["23"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + { + name: "my-workflow", + retentionTime: 99999, + store: true, + }, + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + + + +Refer to the [Store Workflow Executions](../../fundamentals/workflows/store-executions/page.mdx) chapter to learn more. + + + +You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](!user-guide!/settings/developer/workflows). Each execution will show you the status, input, and output data. diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx index 92e543cef493f..66727b39de905 100644 --- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx +++ b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx @@ -6,7 +6,13 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](../page.mdx) from Medusa's Testing Framwork. +In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](../page.mdx) from Medusa's Testing Framework. + + + +For other debugging approaches, refer to the [Debug Workflows](../../../debug-workflows/page.mdx) chapter. + + -## Write Integration Test for Workflow +## Write Integration Test for a Workflow Consider you have the following workflow defined at `src/workflows/hello-world.ts`: @@ -65,7 +71,7 @@ medusaIntegrationTestRunner({ jest.setTimeout(60 * 1000) ``` -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test passes if the workflow returns the string `"Hello, World!"`. ### Jest Timeout @@ -78,32 +84,32 @@ jest.setTimeout(60 * 1000) --- -## Run Test +## Run Tests Run the following command to run your tests: ```bash npm2yarn -npm run test:integration +npm run test:integration:http ``` -If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands). +If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands). -This runs your Medusa application and runs the tests available under the `integrations/http` directory. +This runs your Medusa application and runs the tests available under the `integration-tests/http` directory. --- ## Test That a Workflow Throws an Error -You might want to test that a workflow throws an error in certain cases. To test this: +You might want to verify that a workflow throws an error in certain edge cases. To test that a workflow throws an error: - Disable the `throwOnError` option when executing the workflow. - Use the returned `errors` property to check what errors were thrown. -For example, if you have a step that throws this error: +For example, if you have the following step in your workflow that throws a `MedusaError`: ```ts title="src/workflows/hello-world.ts" import { MedusaError } from "@medusajs/framework/utils" @@ -123,7 +129,7 @@ import { helloWorldWorkflow } from "../../src/workflows/hello-world" medusaIntegrationTestRunner({ testSuite: ({ getContainer }) => { describe("Test hello-world workflow", () => { - it("returns message", async () => { + it("should throw error when item doesn't exist", async () => { const { errors } = await helloWorldWorkflow(getContainer()) .run({ throwOnError: false, @@ -139,6 +145,432 @@ medusaIntegrationTestRunner({ jest.setTimeout(60 * 1000) ``` -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, which is the error thrown. + +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. + +--- + +## Test Long-Running Workflows + +Since [long-running workflows](../../../../fundamentals/workflows/long-running-workflow/page.mdx) run asynchronously, testing them requires a different approach than synchronous workflows. + +When testing long-running workflows, you need to: + +1. Set the asynchronous steps as successful manually. +2. Subscribe to the workflow's events to listen for the workflow execution's completion. +3. Verify the output of the workflow after it has completed. + +For example, consider you have the following long-running workflow defined at `src/workflows/long-running-workflow.ts`: + +```ts title="src/workflows/long-running-workflow.ts" highlights={[["15"]]} +import { + createStep, + createWorkflow, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) + +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") + } +) + +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) + +const longRunningWorkflow = createWorkflow( + "long-running", + function () { + step1() + step2() + const message = step3() + + return new WorkflowResponse({ + message, + }) + } +) + +export default longRunningWorkflow +``` + +`step2` in this workflow is an asynchronous step that you need to set as successful manually in your test. + +You can write the following test to ensure that the long-running workflow completes successfully: + +export const longRunningWorkflowHighlights1 = [ + ["11", "longRunningWorkflow", "Execute the long-running workflow."], + ["14", "workflowEngineService", "Resolve the Workflow Engine Module's service."], + ["19", "workflowCompletion", "Create a promise to wait for the workflow's completion."], + ["30", "subscribe", "Subscribe to the workflow's events."], + ["44", "setStepSuccess", "Set the asynchronous step as successful."], + ["54", "afterSubscriber", "Wait for the promise to resolve when workflow execution completes."], + ["56", "expect", "Assert that the workflow's result matches the expected output."], +] + +```ts title="integration-tests/http/long-running-workflow.spec.ts" highlights={longRunningWorkflowHighlights1} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import longRunningWorkflow from "../../src/workflows/long-running-workflow" +import { Modules, TransactionHandlerType } from "@medusajs/framework/utils" +import { StepResponse } from "@medusajs/framework/workflows-sdk" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test long-running workflow", () => { + it("returns message", async () => { + const container = getContainer() + const { transaction } = await longRunningWorkflow(container) + .run() + + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + let workflowOk: any + const workflowCompletion = new Promise((ok) => { + workflowOk = ok + }) + + const subscriptionOptions = { + workflowId: "long-running", + transactionId: transaction.transactionId, + subscriberId: "long-running-subscriber", + } + + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + workflowOk(data.result.message) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } + }, + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-2", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done!"), + }) + + const afterSubscriber = await workflowCompletion + + expect(afterSubscriber).toBe("Finished three steps") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +In this test, you: + +1. Execute the long-running workflow and get the transaction details from the `run` method's result. +2. Resolve the [Workflow Engine Module](!resources!/infrastructure-modules/workflow-engine)'s service from the Medusa container. +3. Create a promise to wait for the workflow's completion. +4. Subscribe to the workflow's events using the Workflow Engine Module's `subscribe` method. + - The `subscriber` function is called whenever an event related to the workflow occurs. On the `onFinish` event that indicates the workflow has completed, you resolve the promise with the workflow's result. +5. Set the asynchronous step as successful using the `setStepSuccess` method of the Workflow Engine Module. +6. Wait for the promise to resolve, which indicates that the workflow has completed successfully. +7. Finally, you assert that the workflow's result matches the expected output. + +If you run the integration test, it will execute the long-running workflow and verify that it completes and returns the expected result. + +### Example with Multiple Asynchronous Steps + +If your long-running workflow has multiple asynchronous steps, you must set each of them as successful in your test before the workflow can complete. + +Here's how the test would look like if you had two asynchronous steps: + +export const longRunningWorkflowHighlights2 = [ + ["43", "setStepSuccess", "Set the first asynchronous step as successful."], + ["53", "setStepSuccess", "Set the second asynchronous step as successful."], +] + +```ts title="integration-tests/http/long-running-workflow-multiple-steps.spec.ts" highlights={longRunningWorkflowHighlights2} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import longRunningWorkflow from "../../src/workflows/long-running-workflow" +import { Modules, TransactionHandlerType } from "@medusajs/framework/utils" +import { StepResponse } from "@medusajs/framework/workflows-sdk" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test long-running workflow with multiple async steps", () => { + it("returns message", async () => { + const container = getContainer() + const { transaction } = await longRunningWorkflow(container) + .run() + + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + let workflowOk: any + const workflowCompletion = new Promise((ok) => { + workflowOk = ok + }) + + const subscriptionOptions = { + workflowId: "long-running", + transactionId: transaction.transactionId, + subscriberId: "long-running-subscriber", + } + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + workflowOk(data.result.message) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } + }, + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-2", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done!"), + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-3", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done with step 3!"), + }) + + const afterSubscriber = await workflowCompletion + + expect(afterSubscriber).toBe("Finished three steps") + }) + }) + }, +}) +``` + +In this example, you set both `step-2` and `step-3` as successful before waiting for the workflow to complete. + +--- + +## Test Database Operations in Workflows + +In real use cases, you'll often test workflows that perform database operations, such as creating a brand. + +When you test such workflows, you may need to: + +- Verify that the database operations were performed correctly. For example, that a brand was created with the expected properties. +- Perform database actions before testing the workflow. For example, creating a brand before testing a workflow that deletes it. + +This section provides examples of both scenarios. + +### Verify Database Operations in Workflow Test + +To retrieve data from the database after running a workflow, you can resolve and use either the module's service (for example, the Brand Module's service) or [Query](../../../../fundamentals/module-links/query/page.mdx). + +For example, the following test verifies that a brand was created by a workflow: + +export const workflowBrandHighlights = [ + ["10", "createBrandWorkflow", "Execute the create brand workflow."], + ["17", "brandModuleService", "Resolve the Brand Module's service."], + ["19", "retrieveBrand", "Retrieve the created brand from the database."], + ["21", "expect", "Assert that the brand was created with the expected properties."], +] + +```ts title="integration-tests/http/workflow-brand.spec.ts" highlights={workflowBrandHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { createBrandWorkflow } from "../../src/workflows/create-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test create brand workflow", () => { + it("creates a brand", async () => { + const container = getContainer() + const { result: brand } = await createBrandWorkflow(container) + .run({ + input: { + name: "Test Brand", + }, + }) + + const brandModuleService = container.resolve(BRAND_MODULE) + + const createdBrand = await brandModuleService.retrieveBrand(brand.id) + expect(createdBrand).toBeDefined() + expect(createdBrand.name).toBe("Test Brand") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +In this test, you run the workflow, which creates a brand. Then, you retrieve the brand from the database using the Brand Module's service and verify that it was created with the expected properties. + +### Perform Database Actions Before Testing Workflow + +You can perform database actions before testing workflows in the `beforeAll` or `beforeEach` hooks of your test suite. In those hooks, you can create data that is useful for your workflow tests. + + + +Learn more about test hooks in [Jest's Documentation](https://jestjs.io/docs/setup-teardown). + + + +You can perform the database actions before testing a workflow by either: + +- Using the module's service (for example, the Brand Module's service). +- Using an existing workflow that performs the database actions. + +#### Use Module's Service + +For example, the following test creates a brand using the Brand Module's service before running the workflow that deletes it: + +export const workflowBrandDeleteHighlights = [ + ["14", "createBrands", "Create a brand using the Brand Module's service."], + ["24", "deleteBrandWorkflow", "Run the delete brand workflow."], + ["34", "expect", "Assert that the brand was deleted successfully."], +] + +```ts title="integration-tests/http/workflow-brand-delete.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { deleteBrandWorkflow } from "../../src/workflows/delete-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + let brandId: string + + beforeAll(async () => { + const container = getContainer() + + const brandModuleService = container.resolve(BRAND_MODULE) + + const brand = await brandModuleService.createBrands({ + name: "Test Brand", + }) + + brandId = brand.id + }) + + describe("Test delete brand workflow", () => { + it("deletes a brand", async () => { + const container = getContainer() + const { result } = await deleteBrandWorkflow(container) + .run({ + input: { + id: brandId, + }, + }) + + expect(result.success).toBe(true) + + const brandModuleService = container.resolve(BRAND_MODULE) + await expect(brandModuleService.retrieveBrand(brandId)) + .rejects.toThrow() + }) + }) + }, +}) +``` + +In this example, you: + +1. Use the `beforeAll` hook to create a brand before running the workflow that deletes it. +2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand. +3. Verify that the brand was deleted successfully by checking that retrieving it throws an error. + +#### Use Existing Workflow + +Alternatively, if you already have a workflow that performs the database operations, you can use that workflow in the `beforeAll` or `beforeEach` hook. This is useful if the database operations are complex and are already encapsulated in a workflow. + +For example, you can modify the `beforeAll` hook to use the `createBrandWorkflow`: + +export const workflowBrandDeleteHighlights2 = [ + ["13", "createBrandWorkflow", "Create a brand using the create brand workflow."], + ["26", "deleteBrandWorkflow", "Run the delete brand workflow."], + ["36", "expect", "Assert that the brand was deleted successfully."], +] + +```ts title="integration-tests/http/workflow-brand-delete.spec.ts" highlights={workflowBrandDeleteHighlights2} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { deleteBrandWorkflow } from "../../src/workflows/delete-brand" +import { createBrandWorkflow } from "../../src/workflows/create-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + let brandId: string + + beforeAll(async () => { + const container = getContainer() + + const { result: brand } = await createBrandWorkflow(container) + .run({ + input: { + name: "Test Brand", + }, + }) + + brandId = brand.id + }) + + describe("Test delete brand workflow", () => { + it("deletes a brand", async () => { + const container = getContainer() + const { result } = await deleteBrandWorkflow(container) + .run({ + input: { + id: brandId, + }, + }) + + expect(result.success).toBe(true) + + const brandModuleService = container.resolve(BRAND_MODULE) + await expect(brandModuleService.retrieveBrand(brandId)) + .rejects.toThrow() + }) + }) + }, +}) +``` + +In this example, you: -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. \ No newline at end of file +1. Use the `beforeAll` hook to run the `createBrandWorkflow`, which creates a brand before running the workflow that deletes it. +2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand. +3. Verify that the brand was deleted successfully by checking that retrieving it throws an error. diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx deleted file mode 100644 index d8ac8fb9afa5d..0000000000000 --- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx +++ /dev/null @@ -1,83 +0,0 @@ -import { Prerequisites } from "docs-ui" - -export const metadata = { - title: `${pageNumber} Example: Integration Tests for a Module`, -} - -# {metadata.title} - -In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](../page.mdx) from Medusa's Testing Framework. - - - -## Write Integration Test for Module - -Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: - -```ts title="src/modules/blog/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" - -class BlogModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - -To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: - -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import MyCustom from "../models/my-custom" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - describe("BlogModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() - - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. - ---- - -## Run Test - -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](../../page.mdx#add-test-commands). - - - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx index b3278d9b6e090..b1d6828311501 100644 --- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx +++ b/www/apps/book/app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx @@ -19,9 +19,26 @@ In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's ## moduleIntegrationTestRunner Utility -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. +`moduleIntegrationTestRunner` creates integration tests for a module's service. 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`: +For example, consider a Blog Module with a `BlogModuleService` that has a `getMessage` method: + +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + async getMessage(): Promise { + return "Hello, World!" + } +} + +export default BlogModuleService +``` + +To create an integration test for the module's service, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: ```ts title="src/modules/blog/__tests__/service.spec.ts" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" @@ -34,7 +51,13 @@ moduleIntegrationTestRunner({ moduleModels: [Post], resolve: "./src/modules/blog", testSuite: ({ service }) => { - // TODO write tests + describe("BlogModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) }, }) @@ -43,10 +66,10 @@ jest.setTimeout(60 * 1000) The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: -- `moduleName`: The name of the module. +- `moduleName`: The registration 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. +- `testSuite`: A function that defines [Jest](https://jestjs.io/) 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. @@ -96,6 +119,8 @@ moduleIntegrationTestRunner({ }) ``` +`moduleOptions` is an object of key-value pair options that your module's service receives in its constructor. + --- ## Write Tests for Modules without Data Models @@ -123,6 +148,58 @@ jest.setTimeout(60 * 1000) --- +## Inject Dependencies in Module Tests + +Some modules have injected dependencies, such as the [Event Module's service](!resources!/infrastructure-modules/event). When writing tests for those modules, you need to inject the dependencies that the module's service requires to avoid errors. + +You can inject dependencies as mock dependencies that simulate the behavior of the original service. This way you avoid unexpected behavior or results, such as sending real events or making real API calls. + +To inject dependencies, pass the `injectedDependencies` property to the `moduleIntegrationTestRunner` function. + +For example: + +export const mockDependenciesHighlights = [ + ["11", "injectedDependencies", "Inject dependencies into the module's service."], + ["12", "Modules.EVENT_BUS", "The registration name of the dependency."], + ["12", "MockEventBusService", "The mock service to inject."] +] + +```ts title="src/modules/blog/__tests__/service.spec.ts" highlights={mockDependenciesHighlights} +import { MockEventBusService, moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import Post from "../models/post" +import { Modules } from "@medusajs/framework/utils" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./src/modules/blog", + injectedDependencies: { + [Modules.EVENT_BUS]: new MockEventBusService(), + }, + testSuite: ({ service }) => { + describe("BlogModuleService", () => { + it("says hello world", async () => { + const message = await service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +`injectedDependencies`'s value is an object whose keys are registration names of the dependencies you want to inject, and the values are the mock services. + +In this example, you inject a mock Event Module service into the `BlogModuleService`. Medusa exposes a `MockEventBusService` class that you can use to mock the Event Module's service. + +For other modules, you can create a mock service that implements the same interface as the original service. Make sure to use the same registration name as the original service when injecting it. + +--- + ### Other Options and Inputs Refer to [the Test Tooling Reference](!resources!/test-tools-reference/moduleIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function. diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/page.mdx index 8f64553872777..5da2bf10033a0 100644 --- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/page.mdx +++ b/www/apps/book/app/learn/debugging-and-testing/testing-tools/page.mdx @@ -41,6 +41,7 @@ module.exports = { { jsc: { parser: { syntax: "typescript", decorators: true }, + target: "es2021", }, }, ], @@ -78,7 +79,7 @@ Finally, add the following scripts to `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:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" }, ``` diff --git a/www/apps/book/app/learn/deployment/page.mdx b/www/apps/book/app/learn/deployment/page.mdx index d3b6610c56913..56021b2f17276 100644 --- a/www/apps/book/app/learn/deployment/page.mdx +++ b/www/apps/book/app/learn/deployment/page.mdx @@ -13,7 +13,7 @@ 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) +![Medusa deployment architecture showing the relationship between three main components: the Medusa server (backend API), Medusa Admin dashboard (for store management), and customer-facing storefronts, all connected to facilitate complete ecommerce functionality](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. @@ -25,7 +25,7 @@ You must deploy the Medusa application before the storefront, as it connects to 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) +![Medusa server deployment infrastructure diagram illustrating the backend services ecosystem: the Node.js Medusa server connected to essential services including PostgreSQL database for data storage, and Redis for caching and session management](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). diff --git a/www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx b/www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx index 14334758463d6..a1aca890b0e9f 100644 --- a/www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx @@ -4,11 +4,11 @@ export const metadata = { # {metadata.title} -This chapter lists some constraints of admin widgets and UI routes. +This chapter lists some development constraints of admin widgets and UI routes. ## Arrow Functions -Widget and UI route components must be created as arrow functions. +Widget and UI route components must be created as arrow functions. Otherwise, Medusa doesn't register them correctly. export const arrowHighlights = [ ["2", "function", "Don't declare the widget / UI route as a function."], @@ -31,7 +31,7 @@ const ProductWidget = () => { ## Widget Zone -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. Otherwise, Medusa doesn't register the widget correctly. export const zoneHighlights = [ ["3", "`product.details.before`", "Don't specify the value of `zone` as a template literal."], diff --git a/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx b/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx index 679ea170f6308..d36e3aa6c43e5 100644 --- a/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/environment-variables/page.mdx @@ -65,10 +65,16 @@ If you receive a type error on `import.meta.env`, create the file `src/admin/vit ```ts title="src/admin/vite-env.d.ts" /// + +declare const __BASE__: string +declare const __BACKEND_URL__: string +declare const __STOREFRONT_URL__: string ``` This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +Note that the `__BASE__`, `__BACKEND_URL__`, and `__STOREFRONT_URL__` variables are global variables available in your admin customizations. Learn more in the [Tips for Admin Customizations](../tips/page.mdx#global-variables-in-admin-customizations) chapter. + --- ## Check Node Environment in Admin Customizations @@ -122,3 +128,13 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` + +To fix possible type errors, create the file `src/admin/vite-env.d.ts` and add the global variables: + +```ts title="src/admin/vite-env.d.ts" +/// + +declare const __BACKEND_URL__: string +declare const __BASE__: string +declare const __STOREFRONT_URL__: string +``` \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/admin/page.mdx b/www/apps/book/app/learn/fundamentals/admin/page.mdx index 86f9df9c5b15c..71cfc22d702ce 100644 --- a/www/apps/book/app/learn/fundamentals/admin/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/page.mdx @@ -6,17 +6,17 @@ export const metadata = { In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it. -## What is the Medusa Admin? - -The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management featuers related to products, orders, customers, and more. - -To explore more what you can do with the Medusa Admin, check out the [User Guide](!user-guide!). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin. +To explore the Medusa Admin's commerce features, check out the [User Guide](!user-guide!). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin. -The Medusa Admin is built with [Vite](https://vite.dev/). When you [install the Medusa application](../../installation/page.mdx), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`. +## What is the Medusa Admin? + +The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management features related to products, orders, customers, and more. + +The Medusa Admin is built with [Vite v5](https://v5.vite.dev/). When you [install the Medusa application](../../installation/page.mdx), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`. @@ -37,7 +37,7 @@ The next chapters will cover these two topics in detail. ### What You Can't Customize in the Medusa Admin -You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). +You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). You also can't customize the login page, the authentication flow, or change the Medusa logo used in the admin dashboard. If your use case requires heavy customization of the admin dashboard, you can build a custom admin dashboard using Medusa's [Admin API routes](!api!/admin). diff --git a/www/apps/book/app/learn/fundamentals/admin/routing/page.mdx b/www/apps/book/app/learn/fundamentals/admin/routing/page.mdx index e8dd579b2fbf9..d154b4bd7ed04 100644 --- a/www/apps/book/app/learn/fundamentals/admin/routing/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/routing/page.mdx @@ -4,9 +4,9 @@ export const metadata = { # {metadata.title} -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. +The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. This gives you more flexibility in routing-related customizations using React Router's utilities, hooks, and components. -In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. +In this chapter, you'll learn about routing-related customizations that you can use in your widgets, UI routes, and settings pages using React Router. @@ -49,7 +49,7 @@ This adds a widget to a product's details page with a link to the Orders page. T --- -## Admin Route Loader +## Fetch Data with Route Loaders @@ -57,9 +57,16 @@ Route loaders are available starting from Medusa v2.5.1. -In your UI route or any other custom admin route, you may need to retrieve data to use it in your route component. For example, you may want to fetch a list of products to display on a custom page. +In your UI routes and settings pages, you may need to retrieve data to use in your route component. For example, you may want to fetch a list of products to display on a custom page. -To do that, you can export a `loader` function in the route file, which is a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader). In this function, you can fetch and return data asynchronously. Then, in your route component, you can use the [useLoaderData](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) hook from React Router to access the data. +The recommended approach is to fetch data within the UI route component asynchronously using the JS SDK with Tanstack (React) Query as explained in [this chapter](../tips/page.mdx#send-requests-to-api-routes). + +However, if you need the data to be fetched before the route is rendered, such as if you're [setting breadcrumbs dynamically](../ui-routes/page.mdx#set-breadcrumbs-dynamically), you can use a route loader. + +To fetch data with a route loader: + +1. Define and export a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader) function in the UI route's file. In this function, you can fetch and return data asynchronously. +2. In your UI route's component, you can use the [useLoaderData hook from React Router](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) to access the data returned by the `loader` function. For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: @@ -103,12 +110,20 @@ In this example, you first export a `loader` function that can be used to fetch Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component. -### Route Parameters +### Route Loaders Block Rendering + +Route loaders block the rendering of your UI route until the data is fetched, which may negatively impact the user experience. So, only use route loaders when the route component needs essential data before rendering, or if you're preparing data that doesn't require sending API requests. + +Otherwise, use the JS SDK with Tanstack (React) Query in the UI route component as explained in the [Tips](../tips/page.mdx#send-requests-to-api-routes) chapter to fetch data asynchronously and update the UI when the data is available. + +![Timeline comparison of a UI route with and without a route loader](https://res.cloudinary.com/dza7lstvk/image/upload/v1753428567/Medusa%20Book/ui-route-loading_vycev8.jpg) -You can also access route params in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: +### Access Route Parameters in Loader + +You can access route parameters in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: export const loaderParamHighlights = [ - ["7", "params", "Access route params in the loader."] + ["7", "params", "Access route parameters in the loader."] ] ```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} @@ -144,16 +159,10 @@ const CustomPage = () => { export default CustomPage ``` -Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object of type `LoaderFunctionArgs` from React Router. This object has a `params` property that contains the route parameters. +Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object that has a `params` property containing the route parameters. In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook. -### When to Use Route Loaders - -A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. - -Only use route loaders when the route component needs data essential before rendering. Otherwise, use the JS SDK with Tanstack (React) Query as explained in [this chapter](../tips/page.mdx#send-requests-to-api-routes). This way, you can fetch data asynchronously and update the UI when the data is available. You can also use a loader to prepare some initial data that's used in the route component before the data is retrieved. - --- ## Other React Router Utilities @@ -176,6 +185,8 @@ export const handle = { } ``` +You can also use the `handle` object to define a breadcrumb for the route. Learn more in the [UI Route](../ui-routes/page.mdx) chapter. + ### React Router Components and Hooks 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. diff --git a/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx b/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx index 4abca34b99157..b0e4f8e00d987 100644 --- a/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx @@ -170,6 +170,16 @@ In your admin customizations, you can use the following global variables: - `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](../../../configurations/medusa-config/page.mdx#backendurl) configuration in `medusa-config.ts`. - `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](../../../configurations/medusa-config/page.mdx#storefrontUrl) configuration in `medusa-config.ts`. +If you get type errors while using these variables, you can create the file `src/admin/vite-env.d.ts` with the following content: + +```ts title="src/admin/vite-env.d.ts" +/// + +declare const __BASE__: string +declare const __BACKEND_URL__: string +declare const __STOREFRONT_URL__: string +``` + --- ## Admin Translations diff --git a/www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx b/www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx index 55dcb3eecfece..209603600618e 100644 --- a/www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx @@ -10,7 +10,7 @@ In this chapter, you’ll learn how to create a UI route in the admin dashboard. ## What is a UI Route? -The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. +The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions. For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. @@ -110,7 +110,7 @@ The above example adds a new sidebar item with the label `Custom Route` and an i ### Nested UI Routes -Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: +Consider that alongside the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: ![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) @@ -142,8 +142,8 @@ This UI route is shown in the sidebar as an item nested in the parent "Custom Ro Some caveats for nested UI routes in the sidebar: - Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. -- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. -- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. +- Nested routes in settings pages aren't shown in the sidebar to follow the admin's design conventions. +- The `icon` configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions. ### Route Under Existing Admin Route @@ -237,7 +237,135 @@ export default CustomPage You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params). -If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. +If you run the Medusa application and go to `http://localhost:9000/app/custom/123`, you'll see `123` printed in the page. + +--- + +## Set UI Route Breadcrumbs + +The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes. + +To set the breadcrumbs of a UI route, export a `handle` object with a `breadcrumb` property in the UI route's file: + +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the UI route."]]} +import { Container, Heading } from "@medusajs/ui" + +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} + +export default CustomPage + +export const handle = { + breadcrumb: () => "Custom Route", +} +``` + +The `breadcrumb`'s value is a function that returns the breadcrumb label as a string, or a React JSX element. + +### Set Breadcrumbs for Nested UI Routes + +If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route. + +For example, if you have the following UI route at `src/admin/routes/custom/nested/page.tsx` that's nested under the previous one: + +```tsx title="src/admin/routes/custom/nested/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the nested UI route."]]} +import { Container, Heading } from "@medusajs/ui" + +const NestedCustomPage = () => { + return ( + +
+ This is my nested custom route +
+
+ ) +} + +export default NestedCustomPage + +export const handle = { + breadcrumb: () => "Nested Custom Route", +} +``` + +Then, when you open the nested route at `http://localhost:9000/app/custom/nested`, you'll see the breadcrumbs as `Custom Route > Nested Custom Route`. Each breadcrumb is clickable, allowing users to navigate back to the parent route. + +### Set Breadcrumbs Dynamically + +In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically. + +To do that, you can: + +1. Define and export a `loader` function in the UI route file that fetches the data needed for the breadcrumb. +2. Receive the data in the `breadcrumb` function and return the dynamic label. + +For example, create a UI route at `src/admin/routes/brands/[id]/page.tsx` with the following content: + +export const dynamicBreadcrumbsHighlights = [ + ["25", "loader", "Define a loader function to fetch the brand data."], + ["35", "breadcrumb", "Set the breadcrumb using the fetched brand data."], + ["36", "data", "The data returned by the loader function is passed as props to the breadcrumb function."], +] + +```tsx title="src/admin/routes/brands/[id]/page.tsx" highlights={dynamicBreadcrumbsHighlights} +import { Container, Heading } from "@medusajs/ui" +import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom" +import { sdk } from "../../../lib/sdk" + +type BrandResponse = { + brand: { + name: string + } +} + +const BrandPage = () => { + const { brand } = useLoaderData() as Awaited + + return ( + +
+ {brand.name} +
+
+ ) +} + +export default BrandPage + +export async function loader({ params }: LoaderFunctionArgs) { + const { id } = params + const { brand } = await sdk.client.fetch(`/admin/brands/${id}`) + + return { + brand, + } +} + +export const handle = { + breadcrumb: ( + { data }: UIMatch + ) => data.brand.name || "Brand", +} +``` + +In the `loader` function, you retrieve the brands from a custom API route and return them. + +Then, in the `handle.breadcrumb` function, you receive `data` prop containing the brand information returned by the `loader` function. You can use this data to return a dynamic breadcrumb label. + +When you open the UI route at `http://localhost:9000/app/brands/123`, the breadcrumb will show the brand's name, such as `Acme`. + + + +You also use the `useLoaderData` hook to access the data returned by the `loader` function in the UI route component. Learn more in the [Routing Customizations](../routing/page.mdx#fetch-data-with-route-loaders) chapter. + + --- diff --git a/www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx b/www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx index 66a5a089916eb..8a89236b2ccdc 100644 --- a/www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx +++ b/www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx @@ -6,13 +6,13 @@ export const metadata = { # {metadata.title} -In this chapter, you’ll learn more about widgets and how to use them. +In this chapter, you’ll learn about widgets and how to use them. ## What is an Admin Widget? -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. +The Medusa Admin's pages are customizable for inserting widgets of custom content in pre-defined injection zones. For example, you can add a widget on the product details page that allows admin users to sync products to a third-party service. -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +You create these widgets as React components that render the content and functionality of the widget. --- @@ -25,7 +25,10 @@ For example, you can add a widget on the product details page that allow admin u }]} /> -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. +You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file must export: + +1. A React component that renders the widget. This will be the file's default export. +2. The widget’s configurations indicating where to insert the widget. For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: @@ -33,7 +36,7 @@ For example, create the file `src/admin/widgets/product-widget.tsx` with the fol export const widgetHighlights = [ ["5", "ProductWidget", "The React component of the product widget."], - ["17", "zone", "The zone to inject the widget to."] + ["17", "zone", "The zone to inject the widget into."] ] ```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} @@ -59,11 +62,11 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](!ui!), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. +In the example above, the widget is injected at the top of a product’s details. -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. +You export the `ProductWidget` component, which displays the heading `Product Widget`. In the widget, you use [Medusa UI](!ui!) to customize the dashboard with the same components used to build it. -In the example above, the widget is injected at the top of a product’s details. +To export the widget's configuration, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts an object as a parameter with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. @@ -83,9 +86,9 @@ Then, open a product’s details page. You’ll find your custom widget at the t --- -## Props Passed in Detail Pages +## Props Passed to Widgets on Detail Pages -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. +Widgets that are injected into a detail page receive a `data` prop, which is the main data of the details page. For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: @@ -130,12 +133,59 @@ The props type is `DetailWidgetProps`, and it accepts as a type argument the exp --- -## Injection Zone +## Injection Zones List -Refer to [this reference](!resources!/admin-widget-injection-zones) for the full list of injection zones and their props. +Refer to the [Admin Widget Injection Zones](!resources!/admin-widget-injection-zones) reference for the full list of injection zones and their props. --- ## Admin Components List -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-components) to find common components. +While the Medusa Admin uses the [Medusa UI](!ui!) components, it also expands on them for styling and design purposes. + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to the [Admin Components](!resources!/admin-components) guide. You'll find components like `Header`, `JSON View`, and more that match the Medusa Admin's design. + +--- + +## Show Widgets Conditionally + +In some cases, you may want to show a widget only if certain conditions are met. For example, you may want to show a widget only if the product has a brand. + +To prevent the widget from showing, return an empty fragment from the widget component: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + if (!data.metadata?.brand) { + return <> // Don't show the widget if the product has no brand + } + + return ( + +
+ + Brand: {data.metadata.brand} + +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In the above example, you return an empty fragment if the product has no brand. Otherwise, you show the brand name in the widget. diff --git a/www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx index d0f5a9d7f14bb..92ac95cf8ae5e 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx @@ -8,7 +8,7 @@ In this chapter, you'll learn about how to add new API routes for each HTTP meth ## HTTP Method Handler -An API route is created for every HTTP method you export a handler function for in a route file. +An API route is created for every HTTP method you export a handler function for in a `route.ts` or `route.js` file. Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. @@ -39,7 +39,7 @@ export const POST = async ( } ``` -This adds two API Routes: +This file adds two API Routes: -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. \ No newline at end of file +- A `GET` API route at `http://localhost:9000/hello-world`. +- A `POST` API route at `http://localhost:9000/hello-world`. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx index 68a0d068ba790..ce50582e41638 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx @@ -14,7 +14,7 @@ A middleware is a function executed when a request is sent to an API Route. It's Middlewares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. -![Diagram showcasing how a middleware is executed when a request is sent to an API route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg) +![API middleware execution flow diagram showing how HTTP requests first pass through middleware functions for authentication, validation, and data processing before reaching the actual route handler, providing a secure and flexible request processing pipeline in Medusa applications](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg) diff --git a/www/apps/book/app/learn/fundamentals/api-routes/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/page.mdx index cb75d18035500..f8916cc444a8d 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/page.mdx @@ -4,25 +4,25 @@ export const metadata = { # {metadata.title} -In this chapter, you’ll learn what API Routes are and how to create them. +In this chapter, you’ll learn what API routes are and how to create them. ## What is an API Route? -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. +An API route is a REST endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. -The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. +The Medusa core application provides a set of [admin](!api!/admin) and [store](!api!/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. --- ## How to Create an API Route? -An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. +You can create an API route in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. ![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). +Each file exports API route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). -For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: +For example, to create a `GET` API route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: ```ts title="src/api/hello-world/route.ts" import type { @@ -40,7 +40,7 @@ export const GET = ( } ``` -### Test API Route +### Test the API Route To test the API route above, start the Medusa application: @@ -48,18 +48,22 @@ To test the API route above, start the Medusa application: npm run dev ``` -Then, send a `GET` request to the `/hello-world` API Route: +Then, send a `GET` request to the `/hello-world` API route: ```bash curl http://localhost:9000/hello-world ``` ---- +You should receive the following response: -## When to Use API Routes +```json +{ + "message": "[GET] Hello world!" +} +``` - +--- -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. +## Next Chapters: Learn More About API Routes - +Follow the next chapters to learn about the different HTTP methods you can use in API routes, parameters and validation, protecting routes, and more. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx index bdbbe7a12756e..87f95b70e5395 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx @@ -8,7 +8,7 @@ In this chapter, you'll learn how to send a response in your API route. ## Send a JSON Response -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +To send a JSON response, use the `json` method of the `MedusaResponse` object that is passed as the second parameter of your API route handler. For example: @@ -72,13 +72,15 @@ The response of this API route has the status code `201`. To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. -For example, to create an API route that returns an event stream: +### Example: Server-Sent Events (SSE) + +For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`: export const streamHighlights = [ ["7", "writeHead", "Set the response's headers."], ["7", "200", "Set the status code."], ["8", `"Content-Type"`, "Set the response's content type."], - ["13", "interval", "Simulate stream data using an interval"], + ["13", "interval", "Simulate stream data using an interval."], ["14", "write", "Write stream data."], ["17", "on", "Stop the stream when the request is terminated."] ] @@ -97,9 +99,14 @@ export const GET = async ( }) const interval = setInterval(() => { - res.write("Streaming data...\n") + res.write("data: Streaming data...\n\n") }, 3000) + req.on("close", () => { + clearInterval(interval) + res.end() + }) + req.on("end", () => { clearInterval(interval) res.end() @@ -109,10 +116,16 @@ export const GET = async ( The `writeHead` method accepts two parameters: -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. +1. The first parameter is the response's status code. +2. The second parameter is an object of key-value pairs to set the response headers. + +This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. + +### Tip: Fetching Stream with JS SDK + +The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream. -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. +Learn more in the [JS SDK](!resources!/js-sdk) documentation. --- 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 index e6c9518e8e0f8..f1ed1bdad2d8c 100644 --- 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 @@ -45,7 +45,7 @@ The API routes that restrict the fields and relations you can retrieve are: ### 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 these routes, you need to override the allowed fields and relations to be retrieved. You can do this by applying a [global 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: @@ -66,10 +66,9 @@ export default defineMiddlewares({ routes: [ { matcher: "/store/customers/me", - method: "GET", middlewares: [ (req, res, next) => { - req.allowed?.push("b2b_company") + (req.allowed ??= []).push("b2b_company") next() }, ], @@ -90,3 +89,9 @@ curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ ``` In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. + + + +This approach only works using a global middleware. It doesn't work in a route middleware. + + \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx b/www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx index ed3973ba7970b..0f1f5775d7238 100644 --- a/www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx +++ b/www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx @@ -4,17 +4,22 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. +In this chapter, you'll learn how to create and execute custom scripts using Medusa's CLI tool. ## What is a Custom CLI Script? -A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. +A custom CLI script is a function that you can execute using Medusa's CLI tool. It is useful when you need a script that has access to the [Medusa container](../medusa-container/page.mdx) and can be executed using Medusa's CLI. + +For example, you can create a custom CLI script that: + +- [Seeds data into the database](./seed-data/page.mdx). +- Runs a script before starting the Medusa application. --- ## How to Create a Custom CLI Script? -To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. +To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must export a function by default. For example, create the file `src/scripts/my-script.ts` with the following content: @@ -37,13 +42,13 @@ export default async function myScript({ container }: ExecArgs) { } ``` -The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. +The function receives as a parameter an object with a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. --- -## How to Run Custom CLI Script? +## How to Run a Custom CLI Script? -To run the custom CLI script, run the Medusa CLI's `exec` command: +To run a custom CLI script, run the Medusa CLI's `exec` command: ```bash npx medusa exec ./src/scripts/my-script.ts @@ -55,7 +60,7 @@ npx medusa exec ./src/scripts/my-script.ts Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. -For example: +For example, create the following CLI script that logs the command line arguments: ```ts import { ExecArgs } from "@medusajs/framework/types" @@ -65,8 +70,32 @@ export default async function myScript({ args }: ExecArgs) { } ``` -Then, pass the arguments in the `exec` command after the file path: +Then, run the script with the `exec` command and pass arguments after the script's path. ```bash npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` + +--- + +## Run Custom Script on Application Startup + +In some cases, you may need to perform an action when the Medusa application starts. + +If the action is related to a module, you should use a [loader](../modules/loaders/page.mdx). Otherwise, you can create a custom CLI script and run it before starting the Medusa application. + +To run a custom script on application startup, modify the `dev` and `start` commands in `package.json` to execute your script first. + +For example: + +```json title="package.json" +{ + "scripts": { + "startup": "medusa exec ./src/scripts/startup.ts", + "dev": "npm run startup && medusa develop", + "start": "npm run startup && medusa start" + } +} +``` + +The `startup` script will run every time you run the Medusa application. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx index fc6b1dd15df64..798664a4de04e 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx @@ -84,3 +84,65 @@ 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. + +--- + +## Examples + +This section covers common use cases where check constraints are particularly useful. + +### 1. Enforce Text Length + +Ensure that text properties meet minimum or maximum length requirements: + +```ts +const User = model.define("user", { + username: model.text(), + password: model.text(), +}) +.checks([ + { + name: "password_length_check", + expression: (columns) => `LENGTH(${columns.password}) >= 8`, + }, +]) +``` + +In the above example, the check constraint fails if the `password` property is less than 8 characters long. + +### 2. Validate Email Format + +Ensure email addresses contain the `@` symbol: + +```ts +const Customer = model.define("customer", { + email: model.text(), +}) +.checks([ + { + name: "email_format_check", + expression: (columns) => `${columns.email} LIKE '%@%'`, + }, +]) +``` + +In the above example, the check constraint fails if the `email` property does not contain the `@` symbol. + +### 3. Enforce Date Ranges + +Ensure dates fall within valid ranges: + +```ts +const Event = model.define("event", { + start_date: model.dateTime(), + end_date: model.dateTime(), +}) +.checks([ + { + name: "date_order_check", + expression: (columns) => `${columns.end_date} >= ${columns.start_date}`, + }, +]) +``` + +In the above example, the check constraint fails if the `end_date` is earlier than the `start_date`. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/data-models/json-properties/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/json-properties/page.mdx new file mode 100644 index 0000000000000..9e7268d23c048 --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/data-models/json-properties/page.mdx @@ -0,0 +1,243 @@ +export const metadata = { + title: `${pageNumber} JSON Properties in Data Models`, +} + +# {metadata.title} + +In this chapter, you'll learn how to use and manage [JSON properties](../properties/page.mdx#json) in data models. + +## What is a JSON Property? + +A JSON property in a data model is a flexible object that can contain various types of values. + +For example, you can create a `Brand` data model with a `metadata` JSON property to store additional information about the brand: + +```ts highlights={[["6"]]} +import { model } from "@medusajs/framework/utils" + +const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), + metadata: model.json(), +}) +``` + +### Accepted Values in JSON Property + +JSON properties are made up of key-value pairs. The keys are strings, and the values can be one of the following types: + +- **Strings**: Text values. + - Empty strings remove the property from the JSON object. Learn more in the [Remove a Property from the JSON Property](#remove-a-property-from-the-json-property) section. +- **Numbers**: Numeric values. +- **Booleans**: `true` or `false` values. +- **Nested Objects**: Objects within objects. +- **Arrays**: Lists of values of any of the above types. + +For example, a `metadata` JSON property can look like this: + +```json +{ + "category": "electronics", + "views": 1500, + "is_featured": true, + "tags": ["new", "sale"], + "details": { + "warranty": "2 years", + "origin": "USA" + } +} +``` + +### What are JSON Properties Useful For? + +JSON properties allow you to store flexible and dynamic data structures that can evolve over time without requiring changes to the database schema. + +Most data models in Medusa's Commerce Modules have a `metadata` property that is a JSON object. `metadata` allows you to store custom information that is not part of the core data model. + +Some examples of data to store in JSON properties: + +- Custom gift message for line items in an order. +- Product's ID in a third-party system. +- Brand's category or tags. + +### What are JSON Properties Not Useful For? + +JSON properties are not suitable for structured data that requires strict validation or relationships with other data models. + +For example, if you want to re-use brands across different products, it's better to create a `Brand` data model and [define a link](../../module-links/page.mdx) to the `Product` data model instead of storing brand information in the product's `metadata` JSON property. + +--- + +## How to Manage JSON Properties? + +### How Medusa Updates JSON Properties + +Consider a Brand Module with a `Brand` data model as shown [in the previous section](#what-is-a-json-property). The [module's service](../../modules/page.mdx#2-create-service) will extend `MedusaService`, which generates methods like [updateBrands](!resources!/service-factory-reference/methods/update) to update a brand. + +When you pass a JSON property in the `updateBrands` method, Medusa will merge the provided JSON object with the existing one in the database. So, only the properties you pass will be updated, and the rest will remain unchanged. + +The following sections show examples of how to add, update, and remove properties in a JSON property. + + + +The following examples assume you have a `brandModuleService` that is resolved from the [Medusa container](../../medusa-container/page.mdx). For example: + +```ts +const brandModuleService = container.resolve(BRAND_MODULE) +``` + + + +### Add a Property to the JSON Property + +Continuing with the Brand example, to add a `category` property to the `metadata` property, pass the new property in the `update` or `create` methods: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + category: "electronics", + }, +}) +``` + +The brand record will now have the `metadata` property updated to include the new `category` property: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics" + } +} +``` + +If you want to add another `is_featured` property later, you can do so by passing it in the update method again without affecting the existing properties: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + is_featured: true, + }, +}) +``` + +The brand record will now have the `metadata` property updated to include both `category` and `is_featured` properties: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "is_featured": true + } +} +``` + +### Update an Existing Property in the JSON Property + +To update an existing property in the JSON property, pass the updated value in the `update` method. + +Continuing with the Brand example, to update the `category` property in the `metadata`: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + category: "home appliances", + }, +}) +``` + +The brand record will now have the `metadata` property updated to reflect the new `category` value, and existing properties will remain unchanged: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "home appliances", + "is_featured": true + } +} +``` + +### Caveat: Updating Nested Objects + +If you want to update a nested object within the JSON property, you need to provide the entire nested object in the update method. + +For example, consider that you have a `details` object within the `metadata`: + +```json +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "details": { + "warranty": "1 year", + "origin": "China" + } + } +} +``` + +To update the `warranty` property within the `details` object, you need to provide the entire `details` object in the update method: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + details: { + warranty: "2 years", + origin: "China", + }, + }, +}) +``` + +The brand record will now have the `metadata` property updated with the new `warranty` value: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "details": { + "warranty": "2 years", + "origin": "China" + } + } +} +``` + +### Remove a Property from the JSON Property + +To remove a property from the JSON property, you can pass an empty string as the value of the property in the update method. + +For example, to remove the `is_featured` property from the `metadata`: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + is_featured: "", + }, +}) +``` + +The brand record will now have the `metadata` property updated to remove the `is_featured` property: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "home appliances" + } +} +``` + +### Manage JSON Properties in API Routes + +The instructions above also apply when managing JSON properties in API routes, since they typically use a service's `update` method under the hood. + +Refer to the [API reference](!api!/store#manage-metadata) to learn more about managing JSON properties, such as `metadata`, in API routes. diff --git a/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx index d2ab8e51c262c..1ea9ef71706a5 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/properties/page.mdx @@ -58,6 +58,32 @@ const Post = model.define("post", { export default Post ``` +#### Limit Text Length + +To limit the allowed length of a `text` property, use the [checks method](../check-constraints/page.mdx). + +For example, to limit the `name` property to a maximum of 50 characters: + +export const textLengthHighlights = [ + ["7", "checks", "Add check constraints to the data model."], +] + +```ts highlights={textLengthHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + name: model.text(), + // ... +}) +.checks([ + (columns) => `${columns.name.length} <= 50`, +]) + +export default Post +``` + +This will add a database check constraint that ensures the `name` property of a record does not exceed 50 characters. If a record with a longer `name` is attempted to be inserted, an error will be thrown. + ### number The `number` method defines a number property. @@ -194,7 +220,7 @@ export default Post ### json -The `json` method defines a property whose value is a stringified JSON object. +The `json` method defines a property whose value is stored as a stringified JSON object in the database. For example: @@ -211,6 +237,8 @@ const Post = model.define("post", { export default Post ``` +Learn more in the [JSON Properties](../json-properties/page.mdx) chapter. + ### array The `array` method defines an array of strings property. diff --git a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx index f12c9cc5ddf0c..f23e352975236 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx @@ -39,8 +39,9 @@ For example: export const oneToOneHighlights = [ ["5", "hasOne", "A user has one email."], - ["10", "belongsTo", "An email belongs to a user."], - ["11", `"email"`, "The relationship's name in the `User` data model."] + ["6", `"user"`, "The relationship's name in the `Email` data model."], + ["12", "belongsTo", "An email belongs to a user."], + ["13", `"email"`, "The relationship's name in the `User` data model."] ] ```ts highlights={oneToOneHighlights} @@ -48,7 +49,9 @@ import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -63,15 +66,68 @@ In the example above, a user has one email, and an email belongs to one user. The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. +Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. ### Optional Relationship To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](../properties/page.mdx#make-property-optional). +### One-to-One Relationship in the Database + +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + ### One-sided One-to-One Relationship -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one. + +You can do this either from the `hasOne` or the `belongsTo` side. + +#### hasOne Side + +By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table. + +If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method: + +- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model. +- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side. + +For example: + +export const oneToOneForeignKeyHighlights = [ + ["5", "hasOne", "A user has one email."], + ["6", "foreignKey", "Add the foreign key column to the `user` table."], + ["7", "mappedBy", "Set to `undefined` since the relationship is only defined on the `User` data model."], +] + +```ts highlights={oneToOneForeignKeyHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + foreignKey: true, + mappedBy: undefined, + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), +}) +``` + +In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model. + +The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model. + + +#### belongsTo Side + +To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter. For example: @@ -94,14 +150,9 @@ const Email = model.define("email", { }) ``` -### One-to-One Relationship in the Database - -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. +In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model. -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +The `User` data model does not have a relationship property pointing to the `Email` data model. --- @@ -118,8 +169,9 @@ For example: export const oneToManyHighlights = [ ["5", "hasMany", "A store has many products."], - ["10", "belongsTo", "A product has one store."], - ["11", `"products"`, "The relationship's name in the `Store` data model."] + ["6", `"store"`, "The relationship's name in the `Product` data model."], + ["12", "belongsTo", "A product has one store."], + ["13", `"products"`, "The relationship's name in the `Store` data model."] ] ```ts highlights={oneToManyHighlights} @@ -127,7 +179,9 @@ import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -165,7 +219,7 @@ For example: export const manyToManyHighlights = [ ["5", "manyToMany", "An order is associated with many products."], - ["12", "manyToMany", "A product is associated with many orders."] + ["15", "manyToMany", "A product is associated with many orders."] ] ```ts highlights={manyToManyHighlights} @@ -278,43 +332,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties: --- -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -export const relationNameHighlights = [ - ["6", `"owner"`, "The relationship's name in the `Email` data model."], - ["13", `"email"`, "The relationship's name in the `User` data model."] -] - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - ---- - ## Cascades When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. diff --git a/www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx index 324f158622832..5c1c5ca0de572 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx @@ -37,7 +37,7 @@ You can also write migrations manually. To do that, create a file in the `migrat For example: -```ts title="src/modules/blog/migrations/Migration202507021059.ts" +```ts title="src/modules/blog/migrations/Migration202507021059_create_author.ts" import { Migration } from "@mikro-orm/migrations" export class Migration202507021059 extends Migration { @@ -63,6 +63,12 @@ Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migrati +### Migration File Naming + +Migrations are executed in the ascending order of their file names. So, it's recommended to prefix the migration file name with the timestamp of when the migration was created. This ensures that migrations are executed in the order they were created. + +For example, if you create a migration on July 2, 2025, at 10:59 AM, the file name should be `Migration202507021059_create_brand.ts`. This way, the migration will be executed after any previous migrations that were created before this date and time. + --- ## Run the Migration @@ -106,3 +112,67 @@ 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](!resources!/medusa-cli/commands/db). + +--- + +## Data Migration Scripts + +In some use cases, you may need to perform data migration after updates to the database. For example, after you added a `Site` data model to the Blog Module, you want to assign all existing posts and authors to a default site. Another example is updating data stored in a third-party system. + +In those scenarios, you can instead create a data migration script. They are asynchronous function that the Medusa application executes once when you run the `npx medusa db:migrate command`. + +### How to Create a Data Migration Script + +You can create data migration scripts in a TypeScript or JavaScript file under the `src/migration-scripts` directory. The file must export an asynchronous function that will be executed when the `db:migrate` command is executed. + +For example, to create a data migration script for the Blog Module example, create the file `src/migration-scripts/migrate-blog-data.ts` with the following content: + +export const dataMigrationHighlights = [ + ["6", "migrateBlogData", "Function to execute when migrations are run"], + ["8", "isInstalled", "Confirm that the Blog Module is installed before running the workflow."], + ["12", "migrateBlogDataWorkflow", "Perform the data migration action."], +] + +```ts title="src/migration-scripts/migrate-blog-data.ts" highlights={dataMigrationHighlights} +import { MedusaModule } from "@medusajs/framework/modules-sdk" +import { ExecArgs } from "@medusajs/framework/types" +import { BLOG_MODULE } from "../modules/blog" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export default async function migrateBlogData({ container }: ExecArgs) { + // Check that the blog module exists + if (!MedusaModule.isInstalled(BLOG_MODULE)) { + return + } + + await migrateBlogDataWorkflow(container).run({}) +} + +const migrateBlogDataWorkflow = createWorkflow( + "migrate-blog-data", + () => { + // Assuming you have these steps + createDefaultSiteStep() + + assignBlogDataToSiteStep() + } +) +``` + +In the above example, you default export an asynchronous function that receives an object parameter with the [Medusa Container](../../medusa-container/page.mdx) property. + +In the function, you first ensure that the Blog Module is installed to avoid errors otherwise. Then, you run a workflow that you've created in the same file that performs the necessary data migration. + +#### Test Data Migration Script + +To test out the data migration script, run the migration command: + +```bash +npx medusa db:migrate +``` + +Medusa will run any pending migrations and migration scripts, including your script. + +If the script runs successfully, Medusa won't run the script again. + +If there are errors in the script, you'll receive an error in the migration script logs. Medusa will keep running the script every time you run the migration command until it runs successfully. diff --git a/www/apps/book/app/learn/fundamentals/generated-types/page.mdx b/www/apps/book/app/learn/fundamentals/generated-types/page.mdx new file mode 100644 index 0000000000000..257eb9ac3aa9c --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/generated-types/page.mdx @@ -0,0 +1,88 @@ +import { CodeTabs, CodeTab } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Automatically Generated Types`, +} + +# {metadata.title} + +In this chapter, you'll learn about the types Medusa automatically generates under the `.medusa` directory and how you should use them. + +## What are Automatically Generated Types? + +Medusa automatically generates TypeScript types for: + +1. Data models collected in the [Query's graph](../module-links/query/page.mdx#querying-the-graph). These types provide you with auto-completion and type checking when using Query. + - Generated data model types are located in the `.medusa/types/query-entry-points.d.ts` file. +2. Modules registered in the [Medusa container](../medusa-container/page.mdx). These types provide you with auto-completion and type checking when resolving modules from the container. + - Generated module registration names are located in the `.medusa/types/modules-bindings.d.ts` file. + +![Diagram showcasing the directory structure of the `.medusa` directory, highlighting the `types` folder and its contents.](https://res.cloudinary.com/dza7lstvk/image/upload/v1753448927/Medusa%20Book/generated-types-dir_bmvdts.jpg) + +--- + +## How to Trigger Type Generation? + +The Medusa application generates these types automatically when you run the application with the `dev` command: + +```bash npm2yarn +npm run dev +``` + +So, if you add a new data model or module and you don't find it in auto-completion or type checking, you can run the `dev` command to regenerate the types. + +--- + +## How to Use the Generated Types? + +The generated types are only meant for auto-completion and type checking. + +For example, consider you have a Brand Module with a `Brand` data model. Due to the auto-generated types, you can do the following: + + + + +```ts +const { data: [brand] } = await query.graph({ + entity: "brand", + fields: ["*"], + filters: { + id: "brand_123", + }, +}) + +// brands has the correct type, so you can access its properties with auto-completion and type checking +console.log(brand.name) +``` + + + + +```ts +const brandModuleService = req.scope.resolve("brand") + +// brandModuleService has the correct type, so you can access its methods with auto-completion and type checking +const brand = await brandModuleService.retrieveBrand("brand_123") +``` + + + + +### Don't Import the Generated Types + +The generated types are only meant to help you in your development process by providing auto-completion and type checking. You should not import them directly in your code. + +Since you don't commit the `.medusa` directory to your version control system or your production environment, importing these types may lead to build errors. + +Instead, if you need to use a data model's type in your customizations, you can use the [InferTypeOf](../data-models/infer-type/page.mdx) utility function, which infers the type of a data model based on its properties. + +For example, if you want to use the `Brand` data model's type in your customizations, you can do the following: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" // relative path to the model + +export type BrandType = InferTypeOf +``` + +You can then use the `BrandType` type in your customizations, such as in workflow inputs or service method outputs. diff --git a/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx b/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx index beaeed86ec85e..ce0f21d8d099b 100644 --- a/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx +++ b/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx @@ -178,3 +178,13 @@ const step1 = createStep("step-1", async (_, { container }) => { A [module](../../fundamentals/modules/page.mdx), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. Learn more about the module's container in [this chapter](../../fundamentals/modules/container/page.mdx). + +--- + +## Container Registration Keys Auto-Completion + +When you resolve a resource from the Medusa container, you can use auto-completion to find the registration key of the resource. + +This is possible due to Medusa's auto-generated types, which are generated in the `.medusa` directory when you run the `dev` command. + +Learn more in the [Auto-Generated Types](../generated-types/page.mdx) chapter. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/module-links/read-only/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/read-only/page.mdx index ac141a83a1229..bcd3caed5c15a 100644 --- a/www/apps/book/app/learn/fundamentals/module-links/read-only/page.mdx +++ b/www/apps/book/app/learn/fundamentals/module-links/read-only/page.mdx @@ -110,7 +110,7 @@ export default defineLink( field: "id", }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { @@ -272,7 +272,7 @@ export default defineLink( field: "id", }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { @@ -346,7 +346,7 @@ export default defineLink( isList: true, }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { diff --git a/www/apps/book/app/learn/fundamentals/modules/container/page.mdx b/www/apps/book/app/learn/fundamentals/modules/container/page.mdx index b6daa783b7794..5119efbad422f 100644 --- a/www/apps/book/app/learn/fundamentals/modules/container/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/container/page.mdx @@ -4,9 +4,9 @@ export const metadata = { # {metadata.title} -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 about the module container and how to resolve resources from it. -Since modules are [isolated](../isolation/page.mdx), each module has a local container only used by the resources of that module. +Since modules are [isolated](../isolation/page.mdx), each module has a local container used only by the resources of that module. So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container. @@ -16,13 +16,13 @@ Find a list of resources or dependencies registered in a module's container in [ --- -## Resolve Resources +## Resolve Resources from the Module Container -### Services +### Resolve in Services -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. To resolve a resource, add the resource's registration name as a property of the object. -For example: +For example, to resolve the [Logger](../../../debugging-and-testing/logging/page.mdx) from the container: ```ts highlights={[["4"], ["10"]]} import { Logger } from "@medusajs/framework/types" @@ -44,11 +44,13 @@ export default class BlogModuleService { } ``` -### Loader +You can then use the logger in the service's methods. -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +### Resolve in Loaders -For example: +[Loaders](../loaders/page.mdx) accept an object parameter with the property `container`. Its value is the module's container that can be used to resolve resources using its `resolve` method. + +For example, to resolve the [Logger](../../../debugging-and-testing/logging/page.mdx) in a loader: ```ts highlights={[["9"]]} import { @@ -66,3 +68,69 @@ export default async function helloWorldLoader({ logger.info("[helloWorldLoader]: Hello, World!") } ``` + +You can then use the logger in the loader's code. + +--- + +## Caveat: Resolving Module Services in Loaders + +Consider a module that has a main service `BrandModuleService`, and an internal service `CmsService`. Medusa will register both of these services in the module's container. + +However, loaders are executed before any services are initialized and registered in the module's container. So, you can't resolve the `BrandModuleService` and `CmsService` in a loader. + +Instead, if your main service extends the `MedusaService` [service factory](../service-factory/page.mdx), you can resolve the internal services generated for each data model passed to the `MedusaService` function. + +For example, if the `BrandModuleService` is defined as follows: + +```ts +import { MedusaService } from "@medusajs/framework/utils" +import Brand from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { +} + +export default BrandModuleService +``` + +Then, you can resolve the `brandService` that allows you to manage brands in the module's loader: + +```ts +import { + LoaderOptions, +} from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const brandService = container.resolve("brandService") + + const brands = await brandService.list() + + console.log("[helloWorldLoader]: Brands:", brands) +} +``` + +Refer to the [Service Factory reference](!resources!/service-factory-reference) for details on the available methods in the generated services. + +--- + +## Alternative to Resolving Other Modules' Services + +Since modules are [isolated](../isolation/page.mdx), you can't resolve resources that belong to other modules from the module's container. For example, you can't resolve the Product Module's service in the Blog Module's service. + +Instead, to build commerce features that span multiple modules, you can create [workflows](../../workflows/page.mdx). In those workflows, you can resolve services of all modules registered in the Medusa application, including the services of the Product and Blog modules. + +Then, you can execute the workflows in [API routes](../../api-routes/page.mdx), [subscribers](../../events-and-subscribers/page.mdx), or [scheduled jobs](../../scheduled-jobs/page.mdx). + +Learn more and find examples in the [Module Isolation](../isolation/page.mdx) chapter. + +--- + +## Avoid Circular Dependencies + +When resolving resources in a module's services, make sure you don't create circular dependencies. For example, if `BlogModuleService` resolves `CmsService`, and `CmsService` resolves `BlogModuleService`, it will cause a circular dependency error. + +Instead, you should generally only resolve services within the main service. For example, `BlogModuleService` can resolve `CmsService`, but `CmsService` should not resolve `BlogModuleService`. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx b/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx index c49a998a5b900..3010edf026364 100644 --- a/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx @@ -4,26 +4,26 @@ export const metadata = { # {metadata.title} -In this document, you'll learn about the expected files and directories in your module. +In this chapter, 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](../page.mdx). +The `index.ts` file is in the root of your module's directory and exports the module's definition as explained in the [Modules](../page.mdx) chapter. --- ## 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](../page.mdx). +A module must have a main service that contains the module's business logic. It's created in the `service.ts` file at the root of your module directory as explained in the [Modules](../page.mdx) chapter. --- ## Other Directories -The following directories are optional and their content are explained more in the following chapters: +The following directories are optional and you can choose to create them based on your module's functionality: -- `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. \ No newline at end of file +- `models`: Holds the [data models](../../data-models/page.mdx) representing tables in the database. +- `migrations`: Holds the [migration](../../data-models/write-migration/page.mdx) files used to reflect changes on the database. +- `loaders`: Holds the [script files to run on the application's startup](../loaders/page.mdx) when Medusa loads the module. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/modules/page.mdx b/www/apps/book/app/learn/fundamentals/modules/page.mdx index 79a5f2aee0f46..9de5e121178d2 100644 --- a/www/apps/book/app/learn/fundamentals/modules/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/page.mdx @@ -44,7 +44,7 @@ A data model represents a table in the database. You create data models using Me You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) +![Blog module directory structure showing the organized file hierarchy with the newly created 'models' folder containing the 'post.ts' data model file, which defines the data model for blog posts using Medusa's data modeling language](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) ```ts title="src/modules/blog/models/post.ts" import { model } from "@medusajs/framework/utils" diff --git a/www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx b/www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx index f40c0e0122334..3504e140b3b63 100644 --- a/www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx @@ -12,11 +12,11 @@ In this chapter, you’ll learn about what the service factory is and how to use Medusa provides a service factory that your module’s main service can extend. -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. +The service factory generates data management methods for your data models, saving you time on implementing these methods manually. -Your service provides data-management functionalities of your data models. +Your service provides data-management functionality for your data models. @@ -48,7 +48,7 @@ export default BlogModuleService ### MedusaService Parameters -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. +The `MedusaService` function accepts one parameter, which is an object of data models for which to generate data-management methods. In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. @@ -56,7 +56,7 @@ In the example above, since the `BlogModuleService` extends `MedusaService`, it The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. +The method names are the operation name, suffixed by the data model's key in the object parameter passed to `MedusaService`. For example, the following methods are generated for the service above: @@ -66,7 +66,7 @@ Find a complete reference of each of the methods in [this documentation](!resour
- + listPosts listAndCountPosts @@ -78,7 +78,7 @@ Find a complete reference of each of the methods in [this documentation](!resour restorePosts - + ### listPosts @@ -279,7 +279,7 @@ Find a complete reference of each of the methods in [this documentation](!resour ### Using a Constructor -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +If you implement a `constructor` in your service, make sure to call `super` and pass it `...arguments`. For example: @@ -297,3 +297,45 @@ class BlogModuleService extends MedusaService({ export default BlogModuleService ``` + +--- + +## Generated Internal Services + +The service factory also generates internal services for each data model passed to the `MedusaService` function. These services are registered in the module's container and can be resolved using their camel-cased names. + +For example, if the `BlogModuleService` is defined as follows: + +```ts +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ +} + +export default BlogModuleService +``` + +Then, you'll have a `postService` registered in the module's container that allows you to manage posts. + +Generated internal services have the same methods as the `BlogModuleService`, such as `create`, `retrieve`, `update`, and `delete`, but without the data model name suffix. + +These services are useful when you need to perform database operations in loaders, as they are executed before the module's services are registered. Learn more in the [Module Container](../container/page.mdx) documentation. + +For example, you can create a loader that logs the number of posts in the database: + +```ts +import { LoaderOptions } from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const postService = container.resolve("postService") + + const [_, count] = await postService.listAndCount() + + console.log(`[helloWorldLoader]: There are ${count} posts in the database.`) +} +``` diff --git a/www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx b/www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx index dbcca6f573852..6a0d6a360523d 100644 --- a/www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx +++ b/www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx @@ -1,14 +1,20 @@ export const metadata = { - title: `${pageNumber} Scheduled Jobs Number of Executions`, + title: `${pageNumber} Scheduled Job Number of Executions`, } # {metadata.title} In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. -## numberOfExecutions Option +## Default Number of Scheduled Job Executions -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. +By default, a scheduled job is executed whenever it matches its specified pattern. For example, if you set a scheduled job to run every five minutes, it will run every five minutes until you stop the Medusa application. + +--- + +## Configure Number of Scheduled Job Executions + +To execute a scheduled job a specific number of times only, you can configure it with the `numberOfExecutions` option. Its value is the number of times the scheduled job can be executed during the Medusa application's runtime. For example: @@ -18,7 +24,7 @@ export const highlights = [ ```ts highlights={highlights} export default async function myCustomJob() { - console.log("I'll be executed three times only.") + console.log("I'll be executed only three times.") } export const config = { @@ -31,10 +37,10 @@ export const config = { 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. +So, Medusa will execute this job only 3 times, once every minute, and then it won't be executed anymore during the current runtime. - +### Configuration is Per Application Runtime -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +Medusa tracks the number of executions for a scheduled job during its current runtime. Once the application stops, the next time you start it, the counter will be reset to `0`. - +So, if you restart the Medusa application, the scheduled job will be executed again until it reaches the number of executions specified. diff --git a/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx b/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx index 71e80857d0f8a..10cd18522b177 100644 --- a/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx +++ b/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx @@ -10,14 +10,14 @@ In this chapter, you’ll learn about scheduled jobs and how to use them. 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. +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 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](../modules/loaders/page.mdx) instead. +- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, create a [custom CLI script](../custom-cli-scripts/page.mdx) and execute it using the operating system's equivalent of a cron job. +- You want to execute the action once when the application loads. Use [loaders](../modules/loaders/page.mdx) or [custom CLI scripts](../custom-cli-scripts/page.mdx#run-custom-script-on-application-startup) instead. - You want to execute the action if an event occurs. Use [subscribers](../events-and-subscribers/page.mdx) instead. @@ -63,7 +63,7 @@ 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!`. +This scheduled job executes every minute and logs into the terminal the message `Greeting!`. ### Test the Scheduled Job @@ -83,11 +83,11 @@ 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. +In a realistic scenario like syncing products to an ERP once a day, you should create a [workflow](../workflows/page.mdx) and execute it in a scheduled job. -When implementing flows spanning across systems or [modules](../modules/page.mdx), you use [workflows](../workflows/page.mdx). 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. +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](../workflows/page.mdx), 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: +You can learn how to create a workflow in the [Workflows](../workflows/page.mdx) chapter, 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" diff --git a/www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx index 5645330c7e4d5..1bc191aef060d 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx @@ -6,19 +6,21 @@ export const metadata = { In this chapter, you'll learn how to expose a hook in your workflow. -## When to Expose a Hook + - - -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. +Refer to the [Workflow Hooks](../workflow-hooks/page.mdx) chapter to learn what a workflow hook is and how to consume Medusa's workflow hooks. - +## When to Expose a Hook? -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. +Medusa exposes hooks in many of its workflows to allow you to inject custom functionality. - +You can also expose your own hooks in your workflows to allow other developers to consume them. This is useful when you're creating a workflow in a [plugin](../../plugins/page.mdx) and you want plugin users to extend the workflow's functionality. + +For example, you are creating a blog plugin and you want to allow developers to perform custom validation before a blog post is created. You can expose a hook in your workflow that developers can consume to perform their custom validation. + +If your workflow is not in a plugin, you probably don't need to expose a hook as you can perform the necessary actions directly in the workflow. --- @@ -29,32 +31,32 @@ To expose a hook in your workflow, use `createHook` from the Workflows SDK. For example: export const hookHighlights = [ - ["13", "createHook", "Add a hook to the workflow."], - ["14", `"productCreated"`, "The hook's name."], - ["15", "productId", "The data to pass to the hook handler."], + ["12", "createHook", "Add a hook to the workflow."], + ["13", `"validate"`, "The hook's name."], + ["14", "", "The data to pass to the hook handler."], ["19", "hooks", "Return the list of hooks in the workflow."] ] -```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} +```ts title="src/workflows/create-blog-post/index.ts" highlights={hookHighlights} import { createStep, createHook, createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { createProductStep } from "./steps/create-product" +import { createPostStep } from "./steps/create-post" -export const myWorkflow = createWorkflow( - "my-workflow", +export const createBlogPostWorkflow = createWorkflow( + "create-blog-post", function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } + const validate = createHook( + "validate", + { post: input } ) + const post = createPostStep(input) - return new WorkflowResponse(product, { - hooks: [productCreatedHook], + return new WorkflowResponse(post, { + hooks: [validate], }) } ) @@ -62,29 +64,61 @@ export const myWorkflow = createWorkflow( The `createHook` function accepts two parameters: -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. +1. The first is a string indicating the hook's name. Developers consuming the hook will use this name to access the hook. +2. The second is the input to pass to the hook handler. Developers consuming the hook will receive this input in the hook handler. -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. +You must also return the hook in the workflow's response by passing a `hooks` property to the `WorkflowResponse`'s second parameter object. Its value is an array of the workflow's hooks. ### How to Consume the Hook? -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +To consume the hook of the workflow, create the file `src/workflows/hooks/create-blog-post.ts` with the following content: export const handlerHighlights = [ - ["3", "productCreated", "Invoke the hook, passing it a step function as a parameter."], + ["4", "validate", "Invoke the hook, passing it a step function as a parameter."], ] -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +```ts title="src/workflows/hooks/create-blog-post.ts" highlights={handlerHighlights} +import { MedusaError } from "@medusajs/framework/utils" +import { createBlogPostWorkflow } from "../create-blog-post" -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { +createBlogPostWorkflow.hooks.validate( + async ({ post }, { container }) => { // TODO perform an action + if (!post.additional_data.custom_title) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Custom title is required" + ) + } } ) ``` -The hook is available on the workflow's `hooks` property using its name `productCreated`. +The hook is available on the workflow's `hooks` property using its name `validate`. You invoke the hook, passing a step function (the hook handler) as a parameter. + +The hook handler is essentially a step function. You can perform in it any actions you perform in a step. For example, you can throw an error, which would stop the workflow execution. + +You can also access the Medusa container in the hook handler to perform actions like using Query or module services. + +For example: + +```ts title="src/workflows/hooks/create-blog-post.ts" +import { createBlogPostWorkflow } from "../create-blog-post" + +createBlogPostWorkflow.hooks.validate( + async ({ post }, { container }) => { + const query = container.resolve("query") + + const { data: existingPosts } = await query.graph({ + entity: "post", + fields: ["*"], + }) + + // TODO do something with existing posts... + } +) +``` + +Learn more about the hook handler in the [Workflow Hooks](../workflow-hooks/page.mdx) chapter. diff --git a/www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx index 7ce0054ca1a85..dda65638b3b80 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx @@ -333,11 +333,9 @@ Instead, refer to the [Error Handling](../errors/page.mdx) chapter for alternati --- -## Step Constraints +## Returned Value Constraints -### Returned Values - -A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. +Data returned from workflows and steps are serialized, allowing Medusa to store them in the database. So, you must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object, from workflows and steps. Values of other types, such as Maps, aren't allowed. @@ -379,4 +377,67 @@ const step1 = createStep( }) } ) -``` \ No newline at end of file +``` + +### Buffer Example + +In some cases, you may need to return a buffer. For example, when your workflow generates a file and you want to return it as a buffer. + +In those cases, you can return an object containing the buffer as a property. Then, in customizations that execute the workflow, you can recreate the buffer from the serialized data. + +For example, consider the following workflow that returns a buffer: + +```ts +import { + createWorkflow, + createStep, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + (_, { container }) => { + const buffer = Buffer.from("Hello, World!") + + return new StepResponse({ + buffer, + }) + } +) + +const myWorkflow = createWorkflow( + "hello-world", + function () { + const step1Response = step1() + + return new WorkflowResponse({ + buffer: step1Response.buffer, + }) + } +) +``` + +Then, in an API route that executes this workflow, you can recreate the buffer from the serialized data using `Buffer.from`: + +```ts +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() + + const buffer = Buffer.from(result.buffer) + + res.setHeader("Content-Type", "application/octet-stream") + res.setHeader("Content-Disposition", "attachment; filename=hello.txt") + res.send(buffer) +} +``` diff --git a/www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx index cd85aa8ed68cc..2b6b17b5af0ad 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx @@ -1,16 +1,18 @@ export const metadata = { - title: `${pageNumber} Execute Another Workflow`, + title: `${pageNumber} Execute Nested Workflows`, } # {metadata.title} -In this chapter, you'll learn how to execute a workflow in another. +In this chapter, you'll learn how to execute a workflow in another workflow. -## Execute in a Workflow +## How to Execute a Workflow in Another? -To execute a workflow in another, use the `runAsStep` method that every workflow has. +In many cases, you may have a workflow that you want to re-use in another workflow. This is most common when you build custom workflows and you want to utilize Medusa's [existing workflows](!resources!/medusa-workflows-reference). -For example: +Executing a workflow within another is slightly different from how you usually execute a workflow. Instead of invoking the workflow, passing it the container, then running its `run` method, you use the `runAsStep` method of the workflow. This will pass the Medusa container and workflow context to the nested workflow. + +For example, to execute the [createProductsWorkflow](!resources!/references/medusa-workflows/createProductsWorkflow) in your custom workflow: export const workflowsHighlights = [ ["11", "runAsStep", "Use the `runAsStep` method to run the workflow as a step."], @@ -41,21 +43,21 @@ const workflow = createWorkflow( ) ``` -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +The `runAsStep` method accepts an `input` property to pass input to the workflow. -The object has an `input` property to pass input to the workflow. +### Returned Data ---- +Notice that you don't need to use `await` when executing the nested workflow, as it's not a promise in this scenario. -## Preparing Input Data +You also receive the workflow's output as a return value from the `runAsStep` method. This is different from the usual workflow response, where you receive the output in a `result` property. -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. +--- - +## Prepare Input Data -Learn about transform in [this chapter](../variable-manipulation/page.mdx). +Since Medusa creates an internal representation of your workflow's constructor function, you can't manipulate data directly in the workflow constructor. You can learn more about this in the [Data Manipulation](../variable-manipulation/page.mdx) chapter. - +If you need to perform some data manipulation to prepare the nested workflow's input data, use `transform` from the Workflows SDK. For example: @@ -99,25 +101,25 @@ const workflow = createWorkflow( ) ``` -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`. +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as input to the `createProductsWorkflow`. ---- + -## Run Workflow Conditionally +Learn more about `transform` in the [Data Manipulation](../variable-manipulation/page.mdx) chapter. -To run a workflow in another based on a condition, use when-then from the Workflows SDK. + - +--- -Learn about when-then in [this chapter](../conditions/page.mdx). +## Run Workflow Conditionally - +Similar to the [previous section](#prepare-input-data), you can't use conditional statements directly in the workflow constructor. Instead, you can use the `when-then` function from the Workflows SDK to run a workflow conditionally. For example: export const whenHighlights = [ ["20", "when", "If `should_create` passed in the input is enabled, then run the function passed to `then`."], - ["22", "createProductsWorkflow", "Workflow only runs if `when`'s condition is `true`."] + ["22", "createProductsWorkflow", "The workflow only runs if `when`'s condition is `true`."] ] ```ts highlights={whenHighlights} collapsibleLines="1-16" @@ -152,4 +154,30 @@ const workflow = createWorkflow( ) ``` -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. +In this example, you use `when-then` to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + + + +Learn more about `when-then` in the [When-Then Conditions](../conditions/page.mdx) chapter. + + + +--- + +## Errors in Nested Workflows + +A nested workflow behaves similarly to a step in a workflow. So, if the nested workflow fails, it will throw an error that stops the parent workflow's execution and compensates previous steps. + +In addition, if another step fails after the nested workflow, the nested workflow's steps will be compensated as part of the compensation process. + +Learn more about handling errors in workflows in the [Error Handling](../errors/page.mdx) chapter. + +--- + +## Nested Long-Running Workflows + +When you execute a long-running workflow within another workflow, the parent workflow becomes a long-running workflow as well. + +So, the parent workflow will wait for the nested workflow to finish before continuing its execution. + +Refer to the [Long-Running Workflows](../long-running-workflow/page.mdx) chapter for more information on how to handle long-running workflows. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx index 1b285d904b7e0..ec021b18a1213 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx @@ -82,6 +82,7 @@ A workflow is also considered long-running if: - One of its steps has its `async` configuration set to `true` and doesn't return a step response. - One of its steps has its `retryInterval` option set as explained in the [Retry Failed Steps chapter](../retry-failed-steps/page.mdx). +- One of its [nested workflows](../execute-another-workflow/page.mdx) is a long-running workflow. --- @@ -151,9 +152,6 @@ export const setStepSuccessStep = createStep( workflowId: "hello-world", }, stepResponse: new StepResponse("Done!"), - options: { - container, - }, }) } ) @@ -203,20 +201,6 @@ The `setStepSuccess` method of the workflow engine's main service accepts as a p description: "Set the response of the step. This is similar to the response you return in a step's definition, but since the `async` step doesn't have a response, you set its response when changing its status.", optional: false }, - { - name: "options", - type: "`Record`", - description: "Options to pass to the step.", - optional: true, - children: [ - { - name: "container", - type: "`MedusaContainer`", - description: "An instance of the Medusa Container", - optional: true - } - ] - } ]} /> @@ -269,9 +253,6 @@ export const setStepFailureStep = createStep( workflowId: "hello-world", }, stepResponse: new StepResponse("Failed!"), - options: { - container, - }, }) } ) diff --git a/www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx index 26b4c645cf4b7..888320ccb0800 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx @@ -1,5 +1,5 @@ export const metadata = { - title: `${pageNumber} Multiple Step Usage in Workflow`, + title: `${pageNumber} Multiple Step Usage in Workflows`, } # {metadata.title} @@ -12,7 +12,7 @@ In some cases, you may need to use a step multiple times in the same workflow. The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. -Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: +Steps must have a unique ID, which you pass as the first parameter of the `createStep` function. ```ts const useQueryGraphStep = createStep( @@ -21,7 +21,7 @@ const useQueryGraphStep = createStep( ) ``` -This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: +So, when a step is used multiple times in a workflow, it's registered multiple times in the workflow with the same ID, which causes an error. ```ts const helloWorkflow = createWorkflow( @@ -51,7 +51,7 @@ When you execute a step in a workflow, you can chain a `config` method to it to Use the `config` method to change a step's ID for a single execution. -So, this is the correct way to write the example above: +For example, this is the correct way to write the example above: export const highlights = [ ["13", "name", "Change the step's ID for this execution."] @@ -75,6 +75,6 @@ const helloWorkflow = createWorkflow( ) ``` -The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. +The `config` method accepts an object with a `name` property. Its value is the new ID for the step to use for this execution only. The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. diff --git a/www/apps/book/app/learn/installation/docker/page.mdx b/www/apps/book/app/learn/installation/docker/page.mdx new file mode 100644 index 0000000000000..95ee301ba544e --- /dev/null +++ b/www/apps/book/app/learn/installation/docker/page.mdx @@ -0,0 +1,436 @@ +import { + Prerequisites, + CodeTabs, + CodeTab +} from "docs-ui" +import Feedback from "@/components/Feedback" + +export const metadata = { + title: `${pageNumber} Install Medusa with Docker`, +} + +# {metadata.title} + +In this chapter, you'll learn how to install and run a Medusa application using Docker. + +The main supported installation method is using [create-medusa-app](../page.mdx). However, since it requires prerequisites like PostgreSQL, Docker may be a preferred and easier option for some users. You can follow this guide to set up a Medusa application with Docker in this case. + +You can follow this guide on any operating system that supports Docker, including Windows, macOS, and Linux. + + + +## 1. Clone Medusa Starter Repository + +To get started, clone the Medusa Starter repository that a Medusa application is based on: + +```bash +git clone https://github.com/medusajs/medusa-starter-default.git --depth=1 my-medusa-store +``` + +This command clones the repository into a directory named `my-medusa-store`. + +--- + +## 2. Create `docker-compose.yml` + +In the cloned repository, create a file named `docker-compose.yml` with the following content: + +```yaml title="docker-compose.yml" +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: medusa_postgres + restart: unless-stopped + environment: + POSTGRES_DB: medusa-store + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - medusa_network + + # Redis + redis: + image: redis:7-alpine + container_name: medusa_redis + restart: unless-stopped + ports: + - "6379:6379" + networks: + - medusa_network + + # Medusa Server + # This service runs the Medusa backend application + # and the admin dashboard. + medusa: + build: . + container_name: medusa_backend + restart: unless-stopped + depends_on: + - postgres + - redis + ports: + - "9000:9000" + environment: + - NODE_ENV=development + - DATABASE_URL=postgres://postgres:postgres@postgres:5432/medusa-store + - REDIS_URL=redis://redis:6379 + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + networks: + - medusa_network + +volumes: + postgres_data: + +networks: + medusa_network: + driver: bridge +``` + +You define three services in this file: + +- `postgres`: The PostgreSQL database service that stores your Medusa application's data. +- `redis`: The Redis service that stores session data. +- `medusa`: The Medusa service that runs the server and the admin dashboard. It connects to the PostgreSQL and Redis services. + +You can add environment variables either in the `environment` section of the `medusa` service or in a separate `.env` file. + +### Recommendations for Multiple Local Projects + +If this isn't the first Medusa project you're setting up with Docker on your machine, make sure to: + +- Change the `container_name` for each service to avoid conflicts. + - For example, use `medusa_postgres_myproject`, `medusa_redis_myproject`, and `medusa_backend_myproject` instead of the current names. +- Change the volume names to avoid conflicts. + - For example, use `postgres_data_myproject` instead of `postgres_data`. +- Change the network name to avoid conflicts. + - For example, use `medusa_network_myproject` instead of `medusa_network`. +- Change the ports to avoid conflicts with other projects. For example: + - Use `"5433:5432"` for PostgreSQL. + - Use `"6380:6379"` for Redis. + - Use `"9001:9000"` for the Medusa server. +- Update the `DATABASE_URL` and `REDIS_URL` environment variables accordingly. + +--- + +## 3. Create `start.sh` + +Next, you need to create a script file that [runs database migrations](../../fundamentals/data-models/write-migration/page.mdx) and starts the Medusa development server. + +Create the file `start.sh` with the following content: + + + +```shell title="start.sh" +#!/bin/sh + +# Run migrations and start server +echo "Running database migrations..." +npx medusa db:migrate + +echo "Seeding database..." +yarn seed || echo "Seeding failed, continuing..." + +echo "Starting Medusa development server..." +yarn dev +``` + + + +```shell title="start.sh" +#!/bin/sh + +# Run migrations and start server +echo "Running database migrations..." +npx medusa db:migrate + +echo "Seeding database..." +npm run seed || echo "Seeding failed, continuing..." + +echo "Starting Medusa development server..." +npm run dev +``` + + + +Make sure to give the script executable permissions: + + + +Setting permissions isn't necessary on Windows, but it's recommended to run this command on Unix-based systems like macOS and Linux. + + + +```bash +chmod +x start.sh +``` + +--- + +## 4. Create `Dockerfile` + +The `Dockerfile` defines how the Medusa service is built. + +Create a file named `Dockerfile` with the following content: + + + +```dockerfile title="Dockerfile" +# Development Dockerfile for Medusa +FROM node:20-alpine + +# Set working directory +WORKDIR /server + +# Copy package files and yarn config +COPY package.json yarn.lock .yarnrc.yml ./ + +# Install all dependencies using yarn +RUN yarn install + +# Copy source code +COPY . . + +# Expose the port Medusa runs on +EXPOSE 9000 + +# Start with migrations and then the development server +CMD ["./start.sh"] +``` + + +```dockerfile title="Dockerfile" +# Development Dockerfile for Medusa +FROM node:20-alpine + +# Set working directory +WORKDIR /server + +# Copy package files and npm config +COPY package.json package-lock.json ./ + +# Install all dependencies using npm +RUN npm install + +# Copy source code +COPY . . + +# Expose the port Medusa runs on +EXPOSE 9000 + +# Start with migrations and then the development server +CMD ["./start.sh"] +``` + + + +In the `Dockerfile`, you use the `node:20-alpine` image as the base since Medusa requires Node.js v20 or later. + +Then, you set the working directory to `/server`, copy the necessary files, install dependencies, expose the `9000` port that Medusa uses, and run the `start.sh` script to start the server. + + + +While it's more common to use `/app` as the working directory, it's highly recommended to use `/server` for the Medusa service to avoid conflicts with Medusa Admin customizations. Learn more in [this troubleshooting guide](!resources!/troubleshooting/medusa-admin/no-widget-route#errors-in-docker). + + + +--- + +## 5. Install Dependencies + +The Medusa Starter repository has a `yarn.lock` file that was generated by installing dependencies with Yarn v1.22.19. + +If you're using a different Yarn version, or you're using NPM, you need to install the dependencies again to ensure compatibility with the Docker setup. + +To install the dependencies, run the following command: + +```bash npm2yarn +npm install +``` + +This will update `yarn.lock` or generate a `package-lock.json` file, depending on your package manager. + +--- + +## 6. Update Scripts in `package.json` + +Next, you need to update the `scripts` section in your `package.json` file to start the development server using Docker. + +Add the following scripts in `package.json`: + +```json title="package.json" +{ + "scripts": { + // Other scripts... + "docker:up": "docker compose up --build -d", + "docker:down": "docker compose down" + } +} +``` + +Where: + +- `docker:up` starts the development server in a Docker container as a background process. +- `docker:down` stops and removes the Docker containers. + +--- + +## 7. Update Medusa Configuration + +If you try to run the Medusa application now with Docker, you'll encounter an SSL error as the server tries to connect to the PostgreSQL database. + +To resolve the error, add the following configurations in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { loadEnv, defineConfig } from "@medusajs/framework/utils" + +loadEnv(process.env.NODE_ENV || "development", process.cwd()) + +module.exports = defineConfig({ + projectConfig: { + // ... + databaseDriverOptions: { + ssl: false, + sslmode: "disable", + }, + }, +}) +``` + +You add the [projectConfig.databaseDriverOptions](../../configurations/medusa-config/page.mdx#databasedriveroptions) to disable SSL for the PostgreSQL database connection. + +--- + +## 8. Add `.dockerignore` + +To ensure only the necessary files are copied into the Docker image, create a `.dockerignore` file with the following content: + +```dockerignore title=".dockerignore" +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git +.gitignore +README.md +.env.test +.nyc_output +coverage +.DS_Store +*.log +dist +build +``` + +--- + +## 9. Create `.env` File + +You can add environment variables either in the `environment` section of the `medusa` service in `docker-compose.yml` or in a separate `.env` file. + +If you don't want to use a `.env` file, you can remove the `env_file` section from the `medusa` service in `docker-compose.yml`. + +Otherwise, copy the `.env.template` file to `.env` and update the values as needed. + +--- + +## 10. Start the Medusa Application with Docker + +All configurations are now ready. You can start the Medusa application using Docker by running the following command: + +```bash npm2yarn +npm run docker:up +``` + +Docker will pull the necessary images, start the PostgreSQL and Redis services, build the Medusa service, and run the development server in a Docker container. + +You can check the logs to ensure everything is running smoothly with the following command: + +```bash +docker compose logs -f +``` + +Once you see the following message, the Medusa server and admin are ready: + +```shell +✔ Server is ready on port: 9000 – 3ms +info: Admin URL → http://localhost:9000/app +``` + +You can now access the Medusa server at `http://localhost:9000` and the Medusa Admin dashboard at `http://localhost:9000/app`. + + + +--- + +## Create Admin User + +To create an admin user, run the following command: + +```bash +docker compose run --rm medusa npx medusa user -e admin@example.com -p supersecret +``` + +Make sure to replace `admin@example.com` and `supersecret` with your desired email and password. + +You can now log in to the Medusa Admin dashboard at `http://localhost:9000/app` using the email and password you just created. + +--- + +## Stop the Medusa Application Running in Docker + +To stop the Medusa application running in Docker, run the following command: + +```bash npm2yarn +npm run docker:down +``` + +This command stops and removes the Docker containers created by the `docker-compose.yml` file. + +This doesn't delete any data in your application or its database. You can start the server again using the `docker:up` command. + +--- + +## Check Logs + +You can check the logs of the Medusa application running in Docker using the following command: + +```bash +docker compose logs -f medusa +``` + +This command shows the logs of the `medusa` service, allowing you to see any errors or messages from the Medusa application. + +--- + +## Learn More about your Medusa Application + +You can learn more about your Medusa application and its setup in the [Installation chapter](../page.mdx). diff --git a/www/apps/book/app/learn/installation/page.mdx b/www/apps/book/app/learn/installation/page.mdx index 84c34e8bb33be..bdac4712c806b 100644 --- a/www/apps/book/app/learn/installation/page.mdx +++ b/www/apps/book/app/learn/installation/page.mdx @@ -12,6 +12,12 @@ In this chapter, you'll learn how to install and run a Medusa application. A Medusa application is made up of a Node.js server and an admin dashboard. You can optionally install the [Next.js Starter Storefront](!resources!/nextjs-starter) separately either while installing the Medusa application or at a later point. + + +While this is the recommended way to create a Medusa application, you can alternatively [install a Medusa application with Docker](./docker/page.mdx). + + + + +To customize the default installation behavior, such as specify a database URL, refer to the [create-medusa-app reference](!resources!/create-medusa-app). + + + After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to. If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`. -![Diagram showcasing an overview of the installation directories](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg) +![Directory structure overview after Medusa installation showing the main project folder containing the Medusa backend application and admin dashboard, alongside the separate storefront directory for the customer-facing Next.js application](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg) ### Successful Installation Result @@ -51,7 +63,7 @@ If you also installed the Next.js Starter Storefront, it'll be running at `http: You can stop the servers for the Medusa application and Next.js Starter Storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development). -![Diagram showcasing the server and applications running after successful installation](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg) +![Post-installation running services overview: Medusa backend server and admin dashboard running on localhost:9000, Next.js Starter Storefront running on localhost:8000, with PostgreSQL database and other essential services active and ready for development](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg) ### Troubleshooting Installation Errors diff --git a/www/apps/book/app/learn/introduction/architecture/page.mdx b/www/apps/book/app/learn/introduction/architecture/page.mdx index ce2f5c1289d28..a791df39d1f18 100644 --- a/www/apps/book/app/learn/introduction/architecture/page.mdx +++ b/www/apps/book/app/learn/introduction/architecture/page.mdx @@ -29,7 +29,7 @@ These layers of stack can be implemented within [plugins](../../fundamentals/plu
-![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) +![Medusa application architecture diagram illustrating the HTTP layer flow: External clients (storefront and admin) send requests to API routes, which execute workflows containing business logic, which then interact with modules to perform data operations on PostgreSQL databases](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) --- @@ -43,7 +43,7 @@ Modules can be implemented within [plugins](../../fundamentals/plugins/page.mdx)
-![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) +![Database layer architecture diagram showing how Medusa modules establish connections to PostgreSQL databases through injected database connections, enabling data persistence and retrieval operations](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) --- @@ -89,4 +89,4 @@ All of the third-party services mentioned above can be replaced to help you buil 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) +![Complete Medusa architecture overview showing the full technology stack: client applications (storefront and admin) connecting through HTTP layer to workflows, which coordinate with commerce and Infrastructure Modules to manage data operations, third-party integrations, and database persistence](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) diff --git a/www/apps/book/app/learn/introduction/build-with-llms-ai/page.mdx b/www/apps/book/app/learn/introduction/build-with-llms-ai/page.mdx index 40e687a983b66..ac4cc296bed37 100644 --- a/www/apps/book/app/learn/introduction/build-with-llms-ai/page.mdx +++ b/www/apps/book/app/learn/introduction/build-with-llms-ai/page.mdx @@ -1,4 +1,13 @@ -import { InlineIcon } from "docs-ui" +import { + InlineIcon, + Tabs, + TabsContent, + TabsContentWrapper, + TabsList, + TabsTrigger, + Button, + Link +} from "docs-ui" import { AiAssistent, AiAssistentLuminosity } from "@medusajs/icons" export const metadata = { @@ -53,6 +62,70 @@ You can add new lines using the `Shift + Enter` shortcut. --- +## MCP Remote Server + +The Medusa documentation provides a remote Model Context Protocol (MCP) server that allows you to find information from the Medusa documentation right in your IDEs or AI tools, such as Cursor. + +Medusa hosts a Streamable HTTP MCP server available at `https://docs.medusajs.com/mcp`. you can add it to AI agents that support connecting to MCP servers. + + + + Cursor + VSCode + Claude Code + + + + + Click here to add the Medusa MCP server to Cursor. + + To manually connect to the Medusa MCP server in Cursor, add the following to your `.cursor/mcp.json` file or Cursor settings, as explained in the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol): + +```json +{ + "mcpServers": { + "medusa": { + "url": "https://docs.medusajs.com/mcp" + } + } +} +``` + + + + + + Click here to add the Medusa MCP server to VSCode. + + To manually connect to the Medusa MCP server in VSCode, add the following to your `.vscode/mcp.json` file in your workspace: + +```json +{ + "servers": { + "medusa": { + "type": "http", + "url": "https://docs.medusajs.com/mcp" + } + } +} +``` + + Learn more in the [VSCode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). + + + + To manually connect to the Medusa MCP server in Claude Code, run the following command in your terminal: + +```sh +claude mcp add --transport http medusa https://docs.medusajs.com/mcp +``` + + + + + +--- + ## Plain Text Documentation The Medusa documentation is available in plain text format, which allows LLMs and AI tools to easily parse and understand the content. diff --git a/www/apps/book/app/learn/introduction/from-v1-to-v2/page.mdx b/www/apps/book/app/learn/introduction/from-v1-to-v2/page.mdx new file mode 100644 index 0000000000000..70e19eb058727 --- /dev/null +++ b/www/apps/book/app/learn/introduction/from-v1-to-v2/page.mdx @@ -0,0 +1,1910 @@ +import { Table, CardList } from "docs-ui" + +export const metadata = { + title: `${pageNumber} From Medusa v1 to v2: Conceptual Differences`, +} + +# {metadata.title} + +In this chapter, you'll learn about the differences and changes between concepts in Medusa v1 to v2. + +## What to Expect in This Chapter + +This chapter is designed to help developers migrate from Medusa v1 to v2 by understanding the conceptual differences between the two versions. + +This chapter will cover: + +- The general steps to update your project from Medusa v1 to v2. +- The changes in tools and plugins between Medusa v1 and v2. +- The high-level changes in the concepts and commerce features between Medusa v1 and v2. + +By following this chapter, you'll learn about the general changes you need to make in your project, with links to read more about each topic. Only topics documented in the v1 documentation are covered. + +This chapter is also useful for developers who are already familiar with Medusa v1 and want to learn about the main differences from Medusa v2. However, it doesn't cover all the new and improved concepts in Medusa v2. Instead, it's highly recommended to read the rest of this documentation to learn about them. + +--- + +## Prerequisites + +### Node.js Version + +While Medusa v1 supported Node.js v16+, Medusa v2 requires Node.js v20+. So, make sure to update your Node.js version if it's older. + +Refer to the [Node.js documentation](https://nodejs.org/en/docs/) for instructions on how to update your Node.js version. + +### New Database + +Medusa v2 makes big changes to the database. So, your existing database will not be compatible with the database for your v2 project. + +If you want to keep your product catalog, you should export the products from the admin dashboard, as explained in [this V1 User Guide](https://docs.medusajs.com/v1/user-guide/products/export). Then, you can import them into your new v2 project from the [Medusa Admin](!user-guide!/products/import). + +For other data types, you'll probably need to migrate them manually through custom scripts. [Custom CLI scripts](../../fundamentals/custom-cli-scripts/page.mdx) may be useful for this. + +--- + +## How to Upgrade from Medusa v1 to v2 + +In this section, you'll learn how to upgrade your Medusa project from v1 to v2. + + + +To create a fresh new Medusa v2 project, check out the [Installation chapter](../../installation/page.mdx). + + + + + +It's highly recommended to fully go through this chapter before you actually update your application, as some v1 features may have been removed or heavily changed in v2. By doing so, you'll formulate a clearer plan for your migration process and its feasibility. + + + +### 1. Update Dependencies in package.json + +The first step is to update the dependencies in your `package.json`. + +A basic v2 project has the following dependencies in `package.json`: + +```json +{ + "dependencies": { + "@medusajs/admin-sdk": "2.8.2", + "@medusajs/cli": "2.8.2", + "@medusajs/framework": "2.8.2", + "@medusajs/medusa": "2.8.2", + "@mikro-orm/core": "6.4.3", + "@mikro-orm/knex": "6.4.3", + "@mikro-orm/migrations": "6.4.3", + "@mikro-orm/postgresql": "6.4.3", + "awilix": "^8.0.1", + "pg": "^8.13.0" + }, + "devDependencies": { + "@medusajs/test-utils": "2.8.2", + "@mikro-orm/cli": "6.4.3", + "@swc/core": "1.5.7", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.13", + "@types/node": "^20.0.0", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.2.25", + "jest": "^29.7.0", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.2", + "vite": "^5.2.11", + "yalc": "^1.0.0-pre.53" + } +} +``` + +The main changes are: + +- You need to install the following Medusa packages (All these packages use the same version): + - `@medusajs/admin-sdk` + - `@medusajs/cli` + - `@medusajs/framework` + - `@medusajs/medusa` + - `@medusajs/test-utils` (as a dev dependency) +- You need to install the following extra packages: + - Database packages: + - `@mikro-orm/core@6.4.3` + - `@mikro-orm/knex@6.4.3` + - `@mikro-orm/migrations@6.4.3` + - `@mikro-orm/postgresql@6.4.3` + - `@mikro-orm/cli@6.4.3` (as a dev dependency) + - `pg^8.13.0` + - Framework packages: + - `awilix@^8.0.1` + - Development and Testing packages: + - `@swc/core@1.5.7` + - `@swc/jest@^0.2.36` + - `@types/node@^20.0.0` + - `jest@^29.7.0` + - `ts-node@^10.9.2` + - `typescript@^5.6.2` + - `vite@^5.2.11` + - `yalc@^1.0.0-pre.53` +- Other packages, such as `@types/react` and `@types/react-dom`, are necessary for admin development and TypeScript support. + + + +Notice that Medusa now uses MikroORM instead of TypeORM for database functionalities. + + + +Once you're done, run the following command to install the new dependencies: + +```bash npm2yarn +npm install +``` + + + +In Medusa v1, you needed to install Medusa modules like the Cache, Event, or Pricing modules. + +These modules are now available out of the box, and you don't need to install or configure them separately. + + + +### 2. Update Script in package.json + +Medusa v2 comes with changes and improvements to its CLI tool. So, update your `package.json` with the following scripts: + +```json +{ + "scripts": { + "build": "medusa build", + "seed": "medusa exec ./src/scripts/seed.ts", + "start": "medusa start", + "dev": "medusa develop", + "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=false --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" + } +} +``` + +Where: + +- `build`: Builds the Medusa application for production. +- `seed`: Seeds the database with initial data. +- `start`: Starts the Medusa server in production. +- `dev`: Starts the Medusa server in development mode. +- `test:integration:http`: Runs HTTP integration tests. +- `test:integration:modules`: Runs module integration tests. +- `test:unit`: Runs unit tests. + +You'll learn more about the [changes in the CLI tool later in this chapter](#medusa-cli-changes). You can also refer to the following documents to learn more about these changes: + +- [Medusa CLI reference](!resources!/medusa-cli) +- [Build Medusa Application](../../build/page.mdx) +- [Integration and Module Tests](../../debugging-and-testing/testing-tools/page.mdx). + +### 3. TSConfig Changes + +In Medusa v1, you had multiple TSConfig configuration files for different customization types. For example, you had `tsconfig.admin.json` for admin customizations and `tsconfig.server.json` for server customizations. + +In Medusa v2, you only need one [root tsconfig.json](https://github.com/medusajs/medusa-starter-default/blob/master/tsconfig.json) file in your project. For admin customizations, you create a [src/admin/tsconfig.json file](https://github.com/medusajs/medusa-starter-default/blob/master/src/admin/tsconfig.json). Refer to each of those links for the recommended configurations. + +### 4. Update Configuration File + +In Medusa v1, you configured your application in the `medusa-config.js` file. Medusa v2 supports this file as `medusa-config.ts`, so make sure to rename it. + +`medusa-config.ts` now exports configurations created with the `defineConfig` utility. It also uses the [loadEnv](../../fundamentals/environment-variables/page.mdx) utility to load environment variables based on the current environment. + +For example, this is the configuration file for a basic Medusa v2 project: + +```ts title="medusa-config.ts" +import { loadEnv, defineConfig } from "@medusajs/framework/utils" + +loadEnv(process.env.NODE_ENV || "development", process.cwd()) + +module.exports = defineConfig({ + projectConfig: { + databaseUrl: process.env.DATABASE_URL, + http: { + storeCors: process.env.STORE_CORS!, + adminCors: process.env.ADMIN_CORS!, + authCors: process.env.AUTH_CORS!, + jwtSecret: process.env.JWT_SECRET || "supersecret", + cookieSecret: process.env.COOKIE_SECRET || "supersecret", + }, + }, +}) +``` + +You can refer to the full list of configurations in the [Medusa Configurations](../../configurations/medusa-config/page.mdx) chapter. The following table highlights the main changes between v1 and v2: + + + + + + Medusa v1 + + + Medusa v2 + + + + + + + `projectConfig.store_cors` + + + [projectConfig.http.storeCors](../../configurations/medusa-config/page.mdx#httpstorecors) + + + + + `projectConfig.admin_cors` + + + [projectConfig.http.adminCors](../../configurations/medusa-config/page.mdx#httpadmincors) + + + + + `projectConfig.auth_cors` + + + [projectConfig.http.authCors](../../configurations/medusa-config/page.mdx#httpauthcors) + + + + + `projectConfig.cookie_secret` + + + [projectConfig.http.cookieSecret](../../configurations/medusa-config/page.mdx#httpcookiesecret) + + + + + `projectConfig.jwt_secret` + + + [projectConfig.http.jwtSecret](../../configurations/medusa-config/page.mdx#httpjwtsecret) + + + + + `projectConfig.database_database` + + + [projectConfig.databaseName](../../configurations/medusa-config/page.mdx#databasename) + + + + + `projectConfig.database_url` + + + [projectConfig.databaseUrl](../../configurations/medusa-config/page.mdx#databaseurl) + + + + + `projectConfig.database_schema` + + + [projectConfig.databaseSchema](../../configurations/medusa-config/page.mdx#databaseschema) + + + + + `projectConfig.database_logging` + + + [projectConfig.databaseLogging](../../configurations/medusa-config/page.mdx#databaselogging) + + + + + `projectConfig.database_extra` + + + [projectConfig.databaseDriverOptions](../../configurations/medusa-config/page.mdx#databasedriveroptions) + + + + + `projectConfig.database_driver_options` + + + [projectConfig.databaseDriverOptions](../../configurations/medusa-config/page.mdx#databasedriveroptions) + + + + + `projectConfig.redis_url` + + + [projectConfig.redisUrl](../../configurations/medusa-config/page.mdx#redisurl) + + + + + `projectConfig.redis_prefix` + + + [projectConfig.redisPrefix](../../configurations/medusa-config/page.mdx#redisprefix) + + + + + `projectConfig.redis_options` + + + [projectConfig.redisOptions](../../configurations/medusa-config/page.mdx#redisoptions) + + + + + `projectConfig.session_options` + + + [projectConfig.sessionOptions](../../configurations/medusa-config/page.mdx#sessionoptions) + + + + + `projectConfig.http_compression` + + + [projectConfig.http.compression](../../configurations/medusa-config/page.mdx#httpcompression) + + + + + `projectConfig.jobs_batch_size` + + + No longer supported. + + + + + `projectConfig.worker_mode` + + + [projectConfig.workerMode](../../configurations/medusa-config/page.mdx#workermode) + + + + + `modules` + + + [Array of modules](../../configurations/medusa-config/page.mdx#module-configurations-modules) + + + +
+ +#### Plugin Changes + +While the `plugins` configuration hasn't changed, plugins available in Medusa v1 are not compatible with Medusa v2. These are covered later in the [Plugin Changes](#plugin-changes) section. + +#### Module Changes + +In Medusa v1, you had to configure modules like Inventory, Stock Location, Pricing, and Product. These modules are now available out of the box, and you don't need to install or configure them separately. + +For the Cache and Event modules, refer to the [Redis Cache Module](!resources!/infrastructure-modules/cache/redis) and [Redis Event Module](!resources!/infrastructure-modules/event/redis) documentations to learn how to configure them in v2 if you had them configured in v1. + +#### Feature Flags + +Some features like product categories and tax inclusive pricing were disabled behind feature flags. + +All of these features are now available out-of-the-box. So, you don't need to enable them in your configuration file anymore. + +#### Admin Configurations + +In v1, the admin dashboard was installed as a plugin with configurations. In v2, the Medusa Admin is available out-of-the-box with different configurations. + +The [Medusa Admin Changes](#medusa-admin-changes) section covers the changes in the Medusa Admin configurations and customizations. + +### 5. Setup New Database + +Now that you have updated your dependencies and configuration file, you need to set up the database for your v2 project. + +This will not take into account entities and data customizations in your v1 project, as you still need to change those. Instead, it will only create the database and tables for your v2 project. + +First, change your database environment variables to the following: + +```bash +DATABASE_URL=postgres://localhost/$DB_NAME +DB_NAME=medusa-v2 +``` + +You can change `medusa-v2` to any database name you prefer. + +Then, run the following commands to create the database and tables: + +```bash npm2yarn +npx medusa db:setup +``` + +This command will create the database and tables for your v2 project. + +After that, you can start your Medusa application with the `dev` command. Note that you may have errors if you need to make implementation changes that are covered in the rest of this guide, so it's better to wait until you finish the v2 migration process before starting the Medusa application. + +### (Optional) 6. Seed with Demo Data + +If you want to seed your Medusa v2 project with demo data, you can copy the content of [this file](https://github.com/medusajs/medusa-starter-default/blob/master/src/scripts/seed.ts) to your `src/scripts/seed.ts` file. + +Then, run the following command to seed the database: + +```bash npm2yarn +npm run seed +``` + +This will seed your database with demo data. + +--- + +## Medusa Admin Changes + +In this section, you'll learn about the changes in the Medusa Admin between v1 and v2. + + + +This section doesn't cover changes related to Medusa Admin customizations. They're covered later in the [Admin Customization Changes](#admin-customization-changes) section. + + + +The Medusa Admin is now available out-of-the-box. It's built with [Vite v5](https://vite.dev/) and runs at `http://localhost:9000/app` by default when you start your Medusa application. + +### Admin Configurations + +You previously configured the admin dashboard when you added it as a plugin in Medusa v1. + +In Medusa v2, you configure the Medusa Admin within the `defineConfig` utility in `medusa-config.ts`. `defineConfig` accepts an `admin` property to configure the Medusa Admin: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // admin options... + }, +}) +``` + +You can refer to the [Medusa Configuration](../../configurations/medusa-config/page.mdx#admin-configurations-admin) chapter to learn about all the admin configurations. The following table highlights the main changes between v1 and v2: + + + + + + Medusa v1 + + + Medusa v2 + + + + + + + `serve` + + + [admin.disable](../../configurations/medusa-config/page.mdx#disable) + + + + + `autoRebuild` + + + No longer supported. The Medusa Admin is always built when you run the `medusa build` command. + + + + + `backend` + + + [admin.backendUrl](../../configurations/medusa-config/page.mdx#backendurl) + + + + + `outDir` + + + No longer supported. The Medusa Admin is now built in the `.medusa/server/public/admin` directory. Learn more in the [Build](../../build/page.mdx) chapter. + + + + + `develop` options + + + No longer supported. The [admin.vite](../../configurations/medusa-config/page.mdx#vite) property may be used to achieve similar results. + + + +
+ +### Admin Webpack Configurations + +In v1, you were able to modify Webpack configurations of the admin dashboard. + +Since Medusa Admin is now built with Vite, you can modify the Vite configurations with the `admin.vite` configuration. Learn more in the [Medusa Configuration](../../configurations/medusa-config/page.mdx#vite) chapter. + +### Admin CLI Tool + +In Medusa v1, you used the `medusa-admin` CLI tool to build and run the admin dashboard. + +In Medusa v2, the Medusa Admin doesn't have a CLI tool. Instead, running `medusa build` and `medusa develop` also builds and runs the Medusa Admin, respectively. + +In addition, you can build the Medusa Admin separately from the Medusa application using the `--admin-only` option. Learn more in the [Build Medusa Application](../../build/page.mdx#separate-admin-build) chapter. + +--- + +## Medusa CLI Changes + +The Medusa CLI for v2 is now in the `@medusajs/cli` package. However, you don't need to install it globally. You can just use `npx medusa` in your Medusa projects. + +Refer to the [Medusa CLI reference](!resources!/medusa-cli) for the full list of commands and options. The following table highlights the main changes between v1 and v2: + + + + + + Medusa v1 + + + Medusa v2 + + + + + + + `migrations run` + + + [db:migrate](!resources!/medusa-cli/commands/db#dbmigrate) + + + + + `migrations revert` + + + [db:rollback](!resources!/medusa-cli/commands/db#dbrollback). However, this command reverts migrations of specific modules, not all migrations. + + + + + `migrations show` + + + No longer supported. + + + + + `seed` + + + No longer supported. However, you can create a [custom CLI script](../../fundamentals/custom-cli-scripts/page.mdx) and [seed data in it](../../fundamentals/custom-cli-scripts/seed-data/page.mdx). + + + + + `start-cluster` + + + [start --cluster \](!resources!/medusa-cli/commands/start) + + + +
+ +--- + +## Plugin Changes + +Medusa v2 supports plugins similar to Medusa v1, but with changes in its usage, development, and configuration. + +In Medusa v1, you created plugins that contained customizations like services that integrated third-party providers, custom API routes, and more. + +In Medusa v2, a plugin can contain customizations like modules that integrate third-party providers, custom API routes, workflows, and more. The plugin development experience has also been improved to resolve big pain points that developers faced in v1. + +Refer to the [Plugins](../../fundamentals/plugins/page.mdx) chapter to learn more about plugins in Medusa v2. + +The rest of this section will cover some of the main changes in plugins between v1 and v2. + +### Medusa Plugins Alternative + +In v1, Medusa provided a set of plugins that you could use in your project. For example, the Stripe and SendGrid plugins. + +In v2, some of these plugins are now available as module providers out-of-the-box. For example, the Stripe and SendGrid module providers. Other plugins may no longer be available, but you can still find guides to create them. + +The following table highlights the alternatives for the Medusa v1 plugins: + + + + + + Medusa v1 + + + v2 Alternative + + + + + + + Algolia + + + [Guide](!resources!/integrations/guides/algolia) + + + + + Brightpearl + + + Not available, but you can follow the [ERP recipe](!resources!/recipes/erp). + + + + + Contenful + + + [Guide](!resources!/integrations/guides/contentful) + + + + + Discount Generator + + + Not available, but you can build it [as a module](../../fundamentals/modules/page.mdx). + + + + + IP Lookup + + + Not available, but you can build it [as a module](../../fundamentals/modules/page.mdx). + + + + + Klarna + + + Not available, but you can integrate it as a [Payment Module Provider](!resources!/references/payment/provider). + + + + + Local File + + + [Local File Module Provider](!resources!/infrastructure-modules/file/local). + + + + + Mailchimp + + + [Guide](!resources!/integrations/guides/mailchimp) + + + + + MinIO + + + [S3 (compatible APIs) File Module Provider](!resources!/infrastructure-modules/file/s3) + + + + + MeiliSearch + + + Not available, but you can integrate it [in a module](../../fundamentals/modules/page.mdx). + + + + + PayPal + + + Not available, but you can integrate it as a [Payment Module Provider](!resources!/references/payment/provider). + + + + + Restock Notification + + + [Guide](!resources!/recipes/commerce-automation/restock-notification) + + + + + S3 + + + [S3 (compatible APIs) File Module Provider](!resources!/infrastructure-modules/file/s3) + + + + + Segment + + + [Guide](!resources!/integrations/guides/segment) + + + + + SendGrid + + + [SendGrid Module Provider](!resources!/infrastructure-modules/notification/sendgrid) + + + + + Shopify + + + Not available, but you can build it [as a module](../../fundamentals/modules/page.mdx). + + + + + Slack + + + [Guide](!resources!/integrations/guides/slack) + + + + + Spaces (DigitalOcean) + + + [S3 (compatible APIs) File Module Provider](!resources!/infrastructure-modules/file/s3). + + + + + Strapi + + + Not available, but you can integrate it [in a module](../../fundamentals/modules/page.mdx). + + + + + Stripe + + + [Stripe Payment Module Provider](!resources!/commerce-modules/payment/payment-provider/stripe) + + + + + Twilio + + + [Guide](!resources!/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms) + + + + + Wishlist + + + [Guide](!resources!/plugins/guides/wishlist) + + + +
+ +You can also find Medusa and community integrations in the [Integrations](https://medusajs.com/integrations/) page. + +### Plugin Options + +Similar to Medusa v1, you can pass options to plugins in Medusa v2. + +However, plugin options are now only passed to modules and module providers created in a plugin. + +So, if you previously accessed options in a plugin's subscriber, for example, that's not possible anymore. You need to access the options in a module or module provider instead, then use its service in the plugin's subscriber. + +For example, this is how you can access options in a plugin's subscriber in v2: + +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const customModuleService = container.resolve("custom") + + const options = customModuleService.getOptions() + + // Use the options in your logic... +} + +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` + +Learn more in the [Create Plugin](../../fundamentals/plugins/create/page.mdx) chapter. + +#### enableUI Option + +Plugins in v1 accepted an `enableUI` option to configure whether a plugin's admin customizations should be shown. + +In v2, this option is no longer supported. All admin customizations in a plugin will be shown in the Medusa Admin. + +--- + + +## Tool Changes + +This section covers changes to tools that were available in Medusa v1. + + + + + + Medusa v1 + + + Medusa v2 + + + + + + + JS Client + + + [JS SDK](!resources!/js-sdk) + + + + + Medusa React + + + No longer supported. Instead, you can [use the JS SDK with Tanstack Query](!resources!/js-sdk#use-tanstack-react-query-in-admin-customizations) or similar approaches. + + + + + Next.js Starter Template + + + [Next.js Starter Storefront](!resources!/nextjs-starter). The changes may be big to support v2 commerce changes. + + + + + Medusa Dev CLI + + + No longer supported. + + + +
+ +--- + +## Changes in Concepts and Development + +In the next sections, you'll learn about the changes in specific concepts and development practices between Medusa v1 and v2. + +### Entities, Services, and Modules + +In Medusa v1, entities, services, and modules were created separately: + +- You create an entity to add a new table to the database. +- You create a service to add new business logic to the Medusa application. +- You create a module to add new features to the Medusa application. It may include entities and services. + +In Medusa v2, you create entities (now called data models) and services in a module. You can't create them separately anymore. The data models define new tables to add to the database, and the service provides data-management features for those data models. + +In this section, you'll learn about the most important changes related to these concepts. You can also learn more in the [Modules](../../fundamentals/modules/page.mdx) chapter. + +#### Modules + +A module is a reusable package of functionalities related to a single domain or integration. For example, Medusa provides a Product Module for product-related data models and features. + +So, if in Medusa v1 you had a `Brand` entity and a service to manage it, in v2, you create a Brand Module that defines a `Brand` data model and a service to manage it. + +To learn how to create a module, refer to the [Modules](../../fundamentals/modules/page.mdx) chapter. + +![Diagram showcasing the directory structure difference between Medusa v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748277500/Medusa%20Book/modules-v1-v2_dsnzyl.jpg) + +#### Data Models + +In Medusa v1, you created data models (entities) using TypeORM. + +In Medusa v2, you use Medusa's Data Model Language (DML) to create data models. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +For example: + +```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(), +}) +``` + +Learn more about data models in the [Data Models](../../fundamentals/data-models/page.mdx) chapters. + +#### Migrations + +In Medusa v1, you had to write migrations manually to create or update tables in the database. Migrations were based on TypeORM. + +In Medusa v2, you can use the Medusa CLI to generate migrations based on MikroORM. For example: + +```bash npm2yarn +npx medusa db:generate brand +``` + +This generates migrations for data models in the Brand Module. Learn more in the [Migrations](../../fundamentals/data-models/write-migration/page.mdx) chapter. + +#### Services + +In Medusa v1, you created a service with business logic related to a feature within your Medusa project. For example, you created a `BrandService` at `src/services/brand.ts` to manage the `Brand` entity. + +In Medusa v2, you can only create a service in a module, and the service either manages the module's data models in the database, or connects to third-party services. + +For example, you create a `BrandService` in the Brand Module at `src/modules/brand/service.ts`: + +```ts title="src/modules/brand/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { + +} + +export default BrandModuleService +``` + +The service has automatically generated data-management methods by extending `MedusaService` from the Modules SDK. So, you now have methods like `retrieveBrand` and `createBrands` available in the service. + +Learn more in the [Service Factory](../../fundamentals/modules/service-factory/page.mdx) chapter. + +When you register the module in the Medusa application, the service is registered in the [Medusa container](../../fundamentals/medusa-container/page.mdx), allowing you to use its methods in workflows, subscribers, scheduled jobs, and API routes. + +#### Repositories + +In Medusa v1, you used the repository of a data model in a service to provide data-management features. For example, you used the `BrandRepository` to manage the `Brand` entity. Repositories were also based on TypeORM. + +In Medusa v2, you generally don't need repositories for basic data-management features, as they're generated by the service factory. However, for more complex use cases, you can use the data model repository based on MikroORM. + +For example: + +```ts title="src/modules/brand/service.ts" +import { InferTypeOf, DAL } from "@medusajs/framework/types" +import Post from "./models/post" + +type Post = InferTypeOf + +type InjectedDependencies = { + postRepository: DAL.RepositoryService +} + +class BlogModuleService { + protected postRepository_: DAL.RepositoryService + + constructor({ + postRepository, + }: InjectedDependencies) { + super(...arguments) + this.postRepository_ = postRepository + } +} + +export default BlogModuleService +``` + +Learn more in the [Database Operations](../../fundamentals/modules/db-operations/page.mdx) chapter. + +#### Module Isolation + +In Medusa v1, you had access to all entities and services in the Medusa application. While this approach was flexible, it introduced complexities, was difficult to maintain, and resulted in hacky workarounds. + +In Medusa v2, modules are isolated. This means that you can only access entities and services within the module. This isolation allows you to integrate modules into your application without side effects, while still providing you with the necessary flexibility to build your use cases. + +The [Module Isolation](../../fundamentals/modules/isolation/page.mdx) chapter explains this concept in detail. The rest of this section gives a general overview of how module isolation affects your Medusa v1 customizations. + +#### Extending Entities + +In Medusa v1, you were able to extend entities by creating a new entity that extended the original one. For example, you could create a custom `Product` entity that extended the original `Product` entity to add a `brand` column. + +In Medusa v2, you can no longer extend entities. Instead, you need to create a new data model that contains the columns you want to add. Then, you can create a [Module Link](../../fundamentals/module-links/page.mdx) that links your data model to the one you want to extend. + +For example, you create a Brand Module that has a `Brand` data model. Then, you create a Module Link that links the `Brand` data model to the `Product` data model in the Product Module: + +```ts title="src/links/product-brand.ts" +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 can then associate brands with a product, retrieve them in API routes and custom functionalities, and more. + +Learn more in the [Module Links](../../fundamentals/module-links/page.mdx) chapter. + +#### Extending Services + +In Medusa v1, you were able to extend services by creating a new service that extended the original one. For example, you could create a custom `ProductService` that extended the original `ProductService` to add a new method. + +In Medusa v2, you can no longer extend services. Instead, you need to [create a module](../../fundamentals/modules/page.mdx) with a service that contains the methods you want to add. Then, you can: + +- Build [workflows](../../fundamentals/workflows/page.mdx) that use both services to achieve a custom feature. +- Consume [Workflow Hooks](../../fundamentals/workflows/workflow-hooks/page.mdx) to run custom actions in existing workflows. +- For more complex use cases, you can re-create an existing workflow and use your custom module's service in it. + +For example, if you extended the `CartService` in v1 to add items with custom prices to the cart, you can instead build a custom workflow that uses your custom module to retrieve an item's price, then add it to the cart using the existing `addToCartWorkflow`: + +```ts +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { addToCartWorkflow } from "@medusajs/medusa/core-flows" +import { + getCustomPriceStep, +} from "./steps/get-custom-price" + +type AddCustomToCartWorkflowInput = { + cart_id: string + item: { + variant_id: string + quantity: number + metadata?: Record + } +} + +export const addCustomToCartWorkflow = createWorkflow( + "add-custom-to-cart", + ({ cart_id, item }: AddCustomToCartWorkflowInput) => { + // assuming this step uses a custom module to get the price + const price = getCustomPriceStep({ + variant: item.variant_id, + currencyCode: "usd", + quantity: item.quantity, + }) + + const itemToAdd = transform({ + item, + price, + }, (data) => { + return [{ + ...data.item, + unit_price: data.price, + }] + }) + + addToCartWorkflow.runAsStep({ + input: { + items: itemToAdd, + cart_id, + }, + }) + } +) +``` + +Refer to the [Workflows](../../fundamentals/workflows/page.mdx) chapters to learn more about workflows in Medusa v2. + +#### Integrating Third-Party Services + +In Medusa v1, you integrated third-party services by creating a service under `src/services` and using it in your customizations. + +In Medusa v2, you can integrate third-party services by creating a module with a service that contains the methods to interact with the third-party service. You can then use the module's service in a [workflow](../../fundamentals/workflows/page.mdx) to build custom features. + +![Directory structure change between v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748278103/Medusa%20Book/integrations-v1-v2_cjzkus.jpg) + +--- + +### Medusa and Module Containers + +In Medusa v1, you accessed dependencies from the container in all your customizations, such as services, API routes, and subscribers. + +In Medusa v2, there are two containers: + + + + + + Container + + + Description + + + Accessed By + + + + + + + [Medusa container](../../fundamentals/medusa-container/page.mdx) + + + Main container that contains Framework and commerce resources, such as services of registered modules. + + + - Workflows + - API routes + - Subscribers + - Scheduled jobs + - Custom CLI scripts + + + + + [Module container](../../fundamentals/modules/container/page.mdx) + + + Container of a module. It contains some resources from the Framework, and resources implemented in the module. + + + Services and loaders in the module. + + + +
+ +You can view the list of resources in each container in the [Container Resources](!resources!/medusa-container-resources) reference. + +--- + +### Workflow Changes + +In Medusa v2, workflows are the main way to implement custom features spanning across modules and systems. + +Workflows have been optimized for data reliability, flexibility, and orchestration across systems. You can learn more in the [Workflows](../../fundamentals/workflows/page.mdx) chapters. + +This section highlights the main changes in workflows between v1 and v2. + +#### Workflows SDK Imports + +In Medusa v1, you imported all Workflows SDK functions and types from the `@medusajs/workflows-sdk` package. + +In Medusa v2, you import them from the `@medusajs/framework/workflows-sdk` package. For example: + +```ts title="src/workflows/hello-world.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +``` + +#### Workflow Return Value + +In Medusa v1, you returned any value from a workflow, such as a string or an object. + +In Medusa v2, you must return an instance of `WorkflowResponse` from a workflow. The data passed to `WorkflowResponse`'s constructor is returned to the caller of the workflow. + +For example: + +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +export const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + return new WorkflowResponse("Hello, world!") + } +) + +// in API route, for example: +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + // message is "Hello, world!" + const { result: message } = await helloWorldWorkflow(req.scope) + .run() + + res.json({ + message, + }) +} +``` + +#### New Workflow Features + +- [Use when-then in workflows to run steps if a condition is satisfied](../../fundamentals/workflows/conditions/page.mdx). +- [Consume hooks to run custom steps in existing workflows](../../fundamentals/workflows/workflow-hooks/page.mdx). +- [Create long-running workflows that run asynchronously in the background](../../fundamentals/workflows/long-running-workflow/page.mdx). + +--- + +### API Route Changes + +API routes are generally similar in Medusa v1 and v2, but with minor changes. + +You can learn more about creating API routes in the [API Routes](../../fundamentals/api-routes/page.mdx) chapters. This section highlights the main changes in API routes between v1 and v2. + +#### HTTP Imports + +In Medusa v1, you imported API-route related types and functions from the `@medusajs/medusa` package. + +In Medusa v2, you import them from the `@medusajs/framework/http` package. For example: + +```ts title="src/api/store/custom/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +``` + +#### Protected API Routes + +In Medusa v1, routes starting with `/store/me` and `/admin` were protected by default. + +In Medusa v2, routes starting with `/store/customers/me` are accessible by registered customers, and `/admin` routes are accessible by admin users. + +In an API route, you can access the logged in user or customer using the `auth_context.actor_id` property of `AuthenticatedMedusaRequest`. For example: + +```ts title="src/api/store/custom/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.auth_context?.actor_id + + // ... +} +``` + +Learn more in the [Protected API Routes](../../fundamentals/api-routes/protected-routes/page.mdx) chapter. + +#### Authentication Middlewares + +In Medusa v1, you had three middlewares to protect API routes: + +- `authenticate` to protect API routes for admin users. +- `authenticateCustomer` to optionally authenticate customers. +- `requireCustomerAuthentication` to require customer authentication. + +In Medusa v2, you can use a single `authenticate` middleware for the three use cases. For example: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], + }, + { + matcher: "/custom/customer*", + // equivalent to requireCustomerAuthentication + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + { + matcher: "/custom/all-customers*", + // equivalent to authenticateCustomer + middlewares: [authenticate("customer", ["session", "bearer"], { + allowUnauthenticated: true, + })], + }, + ], +}) +``` + +Learn more in the [Protected API Routes](../../fundamentals/api-routes/protected-routes/page.mdx) chapter. + +#### Middlewares + +In Medusa v1, you created middlewares by exporting an object in the `src/api/middlewares.ts` file. + +In Medusa v2, you create middlewares by exporting an object created with `defineMiddlewares`, which accepts an object with the same properties as in v1. For example: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, + ], + }, + ], +}) +``` + +Learn more in the [Middlewares](../../fundamentals/api-routes/middlewares/page.mdx) chapter. + +#### Disable Body Parser + +In Medusa v1, you disabled the body parser in API routes by setting `bodyParser: false` in the route's middleware configuration. + +In Medusa v2, you disable the body parser by setting `bodyParser.preserveRawBody` to `true` in the route's middleware configuration. For example: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { preserveRawBody: true }, + matcher: "/custom", + }, + ], +}) +``` + +Learn more in the [Body Parser](../../fundamentals/api-routes/parse-body/page.mdx) chapter. + +#### Extending Validators + +In Medusa v1, you passed custom request parameters to Medusa's API routes by extending a request's validator. + +In Medusa v2, some Medusa API routes support passing additional data in the request body. You can then configure the validation of that additional data and consume them in the hooks of the workflow used in the API route. + +For example: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], +}) +``` + +In this example, you allow passing a `brand` property as additional data to the `/admin/products` API route. + +You can learn more in the [Additional Data](../../fundamentals/api-routes/additional-data/page.mdx) chapter. + +If a route doesn't support passing additional data, you need to [replicate it](../../fundamentals/api-routes/override/page.mdx) to support your custom use case. + +--- + +### Events and Subscribers Changes + +Events and subscribers are similar in Medusa v1 and v2, but with minor changes. + +You can learn more in the [Events and Subscribers](../../fundamentals/events-and-subscribers/page.mdx) chapters. This section highlights the main changes in events and subscribers between v1 and v2. + +#### Emitted Events + +Medusa v2 doesn't emit the same events as v1. Refer to the [Events Reference](!resources!/references/events) for the full list of events emitted in v2. + +#### Subscriber Type Imports + +In Medusa v1, you imported subscriber types from the `@medusajs/medusa` package. + +In Medusa v2, you import them from the `@medusajs/framework` package. For example: + +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +``` + +#### Subscriber Parameter Change + +In Medusa v1, a subscriber function received an object parameter that has `eventName` and `data` properties. + +In Medusa v2, the subscriber function receives an object parameter that has an `event` property. The `event` property contains the event name and data. For example: + +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + // ... +} + +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` + +Also, the `pluginOptions` property is no longer passed in the subscriber's parameter. Instead, you can access the options passed to a plugin within its modules' services, which you can resolve in a subscriber. + +Learn more in the [Events and Subscribers](../../fundamentals/events-and-subscribers/page.mdx) chapter. + +#### Subscriber Implementation Change + +In Medusa v1, you implemented functionalities, such as sending confirmation email, directly within a subscriber. + +In Medusa v2, you should implement these functionalities in a [workflow](../../fundamentals/workflows/page.mdx) and call the workflow in the subscriber. By using workflows, you benefit from rollback mechanism, among other features. + +For example: + +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { + sendOrderConfirmationWorkflow, +} from "../workflows/send-order-confirmation" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` + +#### Emitting Events + +In Medusa v1, you emitted events in services and API routes by resolving the Event Module's service from the container. + +In Medusa v2, you should emit events in workflows instead. For example: + +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... + + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) +``` + +If you need to emit events in a service, you can add the Event Module as a dependency of your module. Then, you can resolve the Event Module's service from the module container and emit the event. This approach is only recommended for events related to under-the-hood processes. + +Learn more in the [Emit Events](../../fundamentals/events-and-subscribers/emit-event/page.mdx) chapter. + +--- + +### Loader Changes + +In Medusa v1, you created loaders in the `src/loaders` directory to perform tasks at application startup. + +In Medusa v2, loaders can only be created in a module. You can create loaders in the `src/modules//loaders` directory. That also means the loader can only access resources in the module's container. + +Learn more in the [Loaders](../../fundamentals/modules/loaders/page.mdx) chapter. + +#### Loader Parameter Changes + +In Medusa v1, a loader function receives two parameters: `container` and `config`. If the loader was created in a module, it also received a `logger` parameter. + +In Medusa v2, a loader function receives a single object parameter that has a `container` and `options` properties. The `options` property contains the properties passed to the module. + +For example: + +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, + options, +}: LoaderOptions) { + const logger = container.resolve("logger") + + logger.info("[HELLO MODULE] Just started the Medusa application!") +} +``` + +--- + +### Scheduled Job Changes + +Scheduled jobs are similar in Medusa v1 and v2, but with minor changes. + +You can learn more about scheduled jobs in the [Scheduled Jobs](../../fundamentals/scheduled-jobs/page.mdx) chapters. This section highlights the main changes in scheduled jobs between v1 and v2. + +#### Scheduled Job Parameter Changes + +In Medusa v1, a scheduled job function received an object of parameters. + +In Medusa v2, a scheduled job function receives only the Medusa container as a parameter. For example: + +```ts title="src/jobs/hello-world.ts" +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: "* * * * *", +} +``` + +The `pluginOptions` property is no longer available, as you can access the options passed to a plugin within its modules' services, which you can resolve in a scheduled job. + +The `data` property is also no longer available, as you can't pass data in the scheduled job's configuration anymore. + +#### Scheduled Job Configuration Changes + +In Medusa v2, the `data` property is removed from the scheduled job's configuration object. + +#### Scheduled Job Implementation Changes + +In Medusa v1, you implemented functionalities directly in the job function. + +In Medusa v2, you should implement these functionalities in a [workflow](../../fundamentals/workflows/page.mdx) and call the workflow in the scheduled job. By using workflows, you benefit from rollback mechanism, among other features. + +For example: + +```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 * * *", +} +``` + +--- + +### Removed Concepts and Alternatives + +The following table highlights concepts that have been removed or changed in Medusa v2 and their alternatives: + + + + + + Medusa v1 + + + Medusa v2 + + + + + + + Batch Jobs and Strategies + + + [Long-Running Workflows](../../fundamentals/workflows/long-running-workflow/page.mdx) + + + + + File Service + + + [File Module Provider](!resources!/infrastructure-modules/file) + + + + + Notification Provider Service + + + [Notification Module Provider](!resources!/infrastructure-modules/notification) + + + + + Search Service + + + Can be integrated as a [module](../../fundamentals/modules/page.mdx). + + + +
+ +--- + +### Admin Customization Changes + +This section covers changes to the admin customizations between Medusa v1 and v2. + +#### Custom Admin Environment Variables + +In Medusa v1, you set custom environment variables to be passed to the admin dashboard by prefixing them with `MEDUSA_ADMIN_`. + +In Medusa v2, you can set custom environment variables to be passed to the admin dashboard by prefixing them with `VITE_`. Learn more in the [Admin Environment Variables](../../fundamentals/admin/environment-variables/page.mdx) chapter. + +#### Admin Widgets + +Due to design changes in the Medusa Admin, some widget injection zones may have been changed or removed. Refer to the [Admin Widgets Injection Zones](!resources!/admin-widget-injection-zones) reference for the full list of injection zones in v2. + +Also, In Medusa v1, you exported in the widget's file a `config` object with the widget's configurations, such as its injection zone. + +In Medusa v2, you export a configuration object defined with `defineWidgetConfig` from the Admin Extension SDK. For example: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["9"], ["10"], ["11"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" + +// The widget +const ProductWidget = () => { + // ... +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +The function accepts an object with a `zone` property, indicating the zone to inject the widget. + +Refer to the [Admin Widgets](../../fundamentals/admin/widgets/page.mdx) chapter to learn more about creating widgets in Medusa v2. + +### Admin UI Routes + +In Medusa v1, UI Routes were prefixed by `/a`. For example, a route created at `src/admin/routes/custom/page.tsx` would be available at `http://localhost:9000/a/custom`. + +In Medusa v2, the `/a` prefix has been removed. So, that same route would be available at `http://localhost:9000/app/custom` (where `/app` is the path to the admin dashboard, not a prefix). + +Also, in v1, you exported a `config` object with the route's configurations to show the route in the dashboard's sidebar. + +In v2, you export a configuration object defined with `defineRouteConfig` from the Admin Extension SDK. For example: + +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["8"], ["9"], ["10"], ["11"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" + +const CustomPage = () => { + // ... +} + +export const config = defineRouteConfig({ + label: "Custom Route", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +The `defineRouteConfig` function accepts an object with the following properties: + +- `label`: The label of the route to show in the sidebar. +- `icon`: The icon to use in the sidebar for the route. + +Refer to the [Admin UI Routes](../../fundamentals/admin/ui-routes/page.mdx) chapter to learn more about creating UI routes in Medusa v2. + +### Admin Setting Routes + +In Medusa v1, you created setting pages under the `src/admin/settings` directory with their own configurations. + +In Medusa v2, setting pages are UI routes created under the `src/admin/routes/settings` directory. + +For example, if you had a `src/admin/settings/custom/page.tsx` file in v1, you should move it to `src/admin/routes/settings/custom/page.tsx` in v2. The file's content will be the same as a UI route. + +For example: + +```tsx title="src/admin/routes/settings/custom/page.tsx" highlights={[["14"], ["15"], ["16"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const CustomSettingPage = () => { + return ( + +
+ Custom Setting Page +
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Custom", +}) + +export default CustomSettingPage +``` + +In v1, you exported a `config` object that showed a setting page as a card in the settings page. In v2, you export the same configuration object as a UI route. + +Learn more about creating setting pages in the [Admin UI Routes](../../fundamentals/admin/ui-routes/page.mdx#create-settings-page) chapter. + +### notify Props in Widgets, UI Routes, and Settings + +In Medusa v1, admin widgets, UI routes, and setting pages received a `notify` prop to show notifications in the admin dashboard. + +This prop is no longer passed in v2. Instead, use the [toast utility from Medusa UI](!ui!/components/toast) to show notifications. + +For example: + +```tsx title="src/admin/widgets/product-details.tsx" highlights={[["7"], ["8"], ["9"]]} +import { toast } from "@medusajs/ui" +import { defineWidgetConfig } from "@medusajs/admin-sdk" + +// The widget +const ProductWidget = () => { + const handleOnClick = () => { + toast.info("Info", { + description: "The quick brown fox jumps over the lazy dog.", + }) + } + // ... +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +Learn more about the `toast` utility in the [Medusa UI Toasts](!ui!/components/toast) documentation. + +### Sending Requests to Medusa Server + +In Medusa v1, you used Medusa React to send requests to the Medusa server. + +Medusa v2 no longer supports Medusa React. Instead, you can use the JS SDK with Tanstack Query to send requests from your admin customizations to the Medusa server. + +Learn more in the [Admin Development Tips](../../fundamentals/admin/tips/page.mdx#send-requests-to-api-routes) chapter. + +### Admin Languages + +Medusa Admin v2 supports different languages out-of-the-box, and you can contribute with new translations. + +Refer to the [User Guide](!user-guide!/tips/languages) for the list of languages supported in the Medusa Admin. + +--- + +## Commerce Features Changes + +In Medusa v2, commerce features are implemented as [Commerce Modules](!resources!/commerce-modules). For example, the [Product Module](!resources!/commerce-modules/product) implements the product-related features, whereas the [Cart Module](!resources!/commerce-modules/cart) implements the cart-related features. + +So, it's difficult to cover all changes in commerce features between v1 and v2. Instead, this section will highlight changes to customizations that were documented in the Medusa v1 documentation. + +To learn about all commerce features in Medusa v2, refer to the [Commerce Modules](!resources!/commerce-modules) documentation. + +### Providers are now Module Providers + +In Medusa v1, you created providers for payment, fulfillment, and tax in services under `src/services`. + +In Medusa v2, you create these providers as module providers that belong to the Payment, Fulfillment, and Tax modules respectively. + +Refer to the following guides to learn how to create these module providers: + +- [Payment Module Provider](!resources!/commerce-modules/payment/payment-provider) +- [Fulfillment Module Provider](!resources!/commerce-modules/fulfillment/fulfillment-provider) +- [Tax Module Provider](!resources!/commerce-modules/tax/tax-provider) + +### Overridden Cart Completion + +In Medusa v1, you were able to override the cart completion strategy to customize the cart completion process. + +In Medusa v2, the cart completion process is now implemented in the [completeCartWorkflow](!resources!/references/medusa-workflows/completeCartWorkflow). There are two ways you can customize the completion process: + +- [Consuming hooks](../../fundamentals/workflows/workflow-hooks/page.mdx) like the [validate](!resources!/references/medusa-workflows/completeCartWorkflow#validate) hook. This is useful if you only want to make changes in key points of the cart completion process. + - You can view available hooks in the [completeCartWorkflow reference](!resources!/references/medusa-workflows/completeCartWorkflow#hooks) +- For more complex use cases, you can create a new workflow with the desired functionality. Then, you can [replicate the complete cart API route](../../fundamentals/api-routes/override/page.mdx) and use it in your storefront. + +### Overridden Tax Calculation + +In Medusa v1, you were able to override the tax calculation strategy to customize the tax calculation process. + +In Medusa v2, the tax calculation process is now implemented in a [Tax Module Provider](!resources!/commerce-modules/tax/tax-provider). So, you can [create a custom tax provider](!resources!/references/tax/provider) with the calculation logic you want, then [use it in a tax region](!user-guide!/settings/tax-regions#edit-tax-region). + +### Overridden Price Selection + +In Medusa v1, you were able to override the price selection strategy to customize the price selection process. + +In Medusa v2, the price selection process is now implemented in the [Pricing Module's calculate method](!resources!/commerce-modules/pricing/price-calculation). The Pricing Module allows you to set [flexible rules and tiers](!resources!/commerce-modules/pricing/price-rules) to support your use case. + +If your use case is complex and these rules are not enough, you can create a new [module](../../fundamentals/modules/page.mdx) with the necessary logic, then use that module in your custom workflows. + +### Gift Card Features + +Medusa v1 has gift card features out-of-the-box. + +In Medusa v2, gift card features are now only available to [Cloud](https://medusajs.com/cloud/) users. + +--- + +## Deployment Changes + +The deployment process in Medusa v2 is similar to v1, but with some changes. For example, the Medusa server is now deployed with Medusa Admin. + +Medusa also provides [Cloud](https://medusajs.com/cloud/), a managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. + +Refer to the [Deployment](!resources!/deployment) documentation to learn about the deployment process for Medusa applications and Next.js Starter Storefront. diff --git a/www/apps/book/app/learn/page.mdx b/www/apps/book/app/learn/page.mdx index b6cece0e8085f..e0a0634c85f0f 100644 --- a/www/apps/book/app/learn/page.mdx +++ b/www/apps/book/app/learn/page.mdx @@ -16,7 +16,7 @@ Medusa ships with three main tools: 2. A [Framework](./fundamentals/framework/page.mdx) for building custom functionalities specific to your business, product, or industry. This includes tools for introducing custom API endpoints, business logic, and data models; building workflows and automations; and integrating with third-party services. 3. A customizable admin dashboard for merchants to configure and operate their store. -When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that brings your business's vision to life. +When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that bring your business's vision to life. --- diff --git a/www/apps/book/app/learn/production/worker-mode/page.mdx b/www/apps/book/app/learn/production/worker-mode/page.mdx index 15d24123913d3..40a095895cdf6 100644 --- a/www/apps/book/app/learn/production/worker-mode/page.mdx +++ b/www/apps/book/app/learn/production/worker-mode/page.mdx @@ -21,7 +21,7 @@ You don't need to set up different projects for each instance. Instead, you can 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) +![Medusa worker mode architecture diagram illustrating the separation of responsibilities: the server instance handling HTTP requests, API calls, and real-time operations while the dedicated worker instance processes background tasks like data imports, email sending, and resource-intensive operations to maintain optimal server performance](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) --- diff --git a/www/apps/book/app/learn/update/page.mdx b/www/apps/book/app/learn/update/page.mdx index 4c3135fefd6ae..75232d198794f 100644 --- a/www/apps/book/app/learn/update/page.mdx +++ b/www/apps/book/app/learn/update/page.mdx @@ -107,7 +107,7 @@ npx medusa db:migrate In the Medusa codebase, our team uses the following [TSDoc](https://tsdoc.org/) tags to indicate changes made in the latest version for a specific piece of code: - `@deprecated`: Indicates that a piece of code is deprecated and will be removed in a future version. The tag's message will include details on what to use instead. However, our updates are always backward-compatible, allowing you to update your codebase at your own pace. -- `@version`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version. +- `@since`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version. --- diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index e522a42138832..09281cde991d9 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -1,7 +1,7 @@ export const generatedEditDates = { - "app/learn/fundamentals/scheduled-jobs/page.mdx": "2024-12-09T10:51:40.570Z", + "app/learn/fundamentals/scheduled-jobs/page.mdx": "2025-07-25T15:56:17.926Z", "app/learn/fundamentals/workflows/page.mdx": "2024-12-09T14:45:17.837Z", - "app/learn/deployment/page.mdx": "2025-06-20T08:36:29.097Z", + "app/learn/deployment/page.mdx": "2025-07-18T15:25:33.249Z", "app/learn/page.mdx": "2025-06-27T11:39:15.941Z", "app/learn/fundamentals/modules/commerce-modules/page.mdx": "2025-04-17T08:51:32.723Z", "app/learn/fundamentals/workflows/retry-failed-steps/page.mdx": "2025-03-28T07:15:19.388Z", @@ -12,64 +12,63 @@ export const generatedEditDates = { "app/learn/fundamentals/admin-customizations/page.mdx": "2024-10-07T12:41:39.218Z", "app/learn/fundamentals/workflows/workflow-timeout/page.mdx": "2025-04-24T13:15:14.472Z", "app/learn/fundamentals/workflows/parallel-steps/page.mdx": "2025-03-24T06:53:36.918Z", - "app/learn/fundamentals/medusa-container/page.mdx": "2024-12-09T11:02:38.225Z", - "app/learn/fundamentals/api-routes/page.mdx": "2024-12-04T11:02:57.134Z", - "app/learn/fundamentals/modules/modules-directory-structure/page.mdx": "2024-12-09T10:32:46.839Z", + "app/learn/fundamentals/medusa-container/page.mdx": "2025-07-25T13:18:36.859Z", + "app/learn/fundamentals/api-routes/page.mdx": "2025-07-25T15:19:33.365Z", + "app/learn/fundamentals/modules/modules-directory-structure/page.mdx": "2025-07-25T15:40:20.362Z", "app/learn/fundamentals/events-and-subscribers/page.mdx": "2025-05-16T13:40:16.111Z", - "app/learn/fundamentals/modules/container/page.mdx": "2025-05-21T15:07:12.059Z", - "app/learn/fundamentals/workflows/execute-another-workflow/page.mdx": "2024-12-09T15:56:22.895Z", + "app/learn/fundamentals/modules/container/page.mdx": "2025-07-31T14:24:04.087Z", + "app/learn/fundamentals/workflows/execute-another-workflow/page.mdx": "2025-08-01T07:28:51.036Z", "app/learn/fundamentals/modules/loaders/page.mdx": "2025-06-16T13:34:16.462Z", - "app/learn/fundamentals/admin/widgets/page.mdx": "2024-12-09T16:43:24.260Z", + "app/learn/fundamentals/admin/widgets/page.mdx": "2025-07-25T15:08:07.035Z", "app/learn/fundamentals/data-models/page.mdx": "2025-03-18T07:55:56.252Z", "app/learn/fundamentals/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z", "app/learn/fundamentals/api-routes/protected-routes/page.mdx": "2025-06-19T16:04:36.064Z", - "app/learn/fundamentals/workflows/add-workflow-hook/page.mdx": "2024-12-09T14:42:39.693Z", + "app/learn/fundamentals/workflows/add-workflow-hook/page.mdx": "2025-07-18T11:33:15.959Z", "app/learn/fundamentals/events-and-subscribers/data-payload/page.mdx": "2025-05-01T15:30:08.421Z", "app/learn/fundamentals/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z", "app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx": "2025-06-02T14:47:54.394Z", "app/learn/fundamentals/workflows/conditions/page.mdx": "2025-01-27T08:45:19.027Z", "app/learn/fundamentals/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", - "app/learn/fundamentals/admin/page.mdx": "2025-04-18T10:28:47.328Z", - "app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2025-03-28T07:02:34.467Z", - "app/learn/fundamentals/workflows/constructor-constraints/page.mdx": "2025-04-24T13:18:24.184Z", - "app/learn/fundamentals/data-models/write-migration/page.mdx": "2025-03-24T06:41:48.915Z", + "app/learn/fundamentals/admin/page.mdx": "2025-07-25T12:46:15.466Z", + "app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2025-08-01T07:16:21.736Z", + "app/learn/fundamentals/workflows/constructor-constraints/page.mdx": "2025-08-01T13:11:18.823Z", + "app/learn/fundamentals/data-models/write-migration/page.mdx": "2025-07-25T13:53:00.692Z", "app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2025-04-25T14:16:41.124Z", "app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", "app/learn/fundamentals/modules/options/page.mdx": "2025-03-18T15:12:34.510Z", - "app/learn/fundamentals/data-models/relationships/page.mdx": "2025-03-18T07:52:07.421Z", + "app/learn/fundamentals/data-models/relationships/page.mdx": "2025-07-16T09:51:22.141Z", "app/learn/fundamentals/workflows/compensation-function/page.mdx": "2025-04-24T13:16:00.941Z", - "app/learn/fundamentals/modules/service-factory/page.mdx": "2025-03-18T15:14:13.486Z", + "app/learn/fundamentals/modules/service-factory/page.mdx": "2025-07-31T13:27:53.791Z", "app/learn/fundamentals/modules/module-links/page.mdx": "2024-09-30T08:43:53.126Z", - "app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx": "2025-07-25T15:54:56.135Z", "app/learn/fundamentals/api-routes/parameters/page.mdx": "2025-02-14T08:34:03.184Z", - "app/learn/fundamentals/api-routes/http-methods/page.mdx": "2024-10-21T13:30:21.367Z", - "app/learn/fundamentals/admin/tips/page.mdx": "2025-05-26T14:58:56.390Z", + "app/learn/fundamentals/api-routes/http-methods/page.mdx": "2025-07-25T15:12:29.347Z", + "app/learn/fundamentals/admin/tips/page.mdx": "2025-08-01T13:14:23.246Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2025-03-11T08:54:26.281Z", - "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z", - "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-05-09T07:56:04.125Z", + "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-07-25T06:58:26.149Z", + "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-07-18T15:20:25.735Z", "app/learn/fundamentals/modules/isolation/page.mdx": "2025-05-21T15:10:15.499Z", "app/learn/fundamentals/data-models/index/page.mdx": "2025-03-18T07:59:07.798Z", - "app/learn/fundamentals/custom-cli-scripts/page.mdx": "2024-10-23T07:08:55.898Z", + "app/learn/fundamentals/custom-cli-scripts/page.mdx": "2025-07-25T15:32:47.587Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-03-18T15:06:27.864Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/page.mdx": "2024-12-09T15:52:01.019Z", - "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-02-11T15:56:03.835Z", - "app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-01-31T13:19:02.587Z", + "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-07-30T13:43:44.636Z", + "app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-07-23T15:32:18.008Z", "app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z", "app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z", "app/learn/fundamentals/modules/service-constraints/page.mdx": "2025-03-18T15:12:46.006Z", - "app/learn/fundamentals/api-routes/responses/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/fundamentals/api-routes/responses/page.mdx": "2025-08-01T14:15:34.787Z", "app/learn/fundamentals/api-routes/validation/page.mdx": "2025-03-24T06:52:47.896Z", "app/learn/fundamentals/api-routes/errors/page.mdx": "2025-06-19T16:09:08.563Z", - "app/learn/fundamentals/admin/constraints/page.mdx": "2024-10-21T13:30:21.366Z", - "app/learn/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx": "2025-03-18T15:07:22.640Z", - "app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2025-03-24T06:54:21.249Z", + "app/learn/fundamentals/admin/constraints/page.mdx": "2025-07-21T08:20:43.223Z", + "app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2025-07-23T15:32:18.008Z", "app/learn/fundamentals/module-links/custom-columns/page.mdx": "2025-03-11T13:29:54.752Z", "app/learn/fundamentals/module-links/directions/page.mdx": "2025-03-17T12:52:06.161Z", "app/learn/fundamentals/module-links/page.mdx": "2025-04-17T08:50:17.036Z", "app/learn/fundamentals/module-links/query/page.mdx": "2025-06-26T16:01:59.548Z", "app/learn/fundamentals/modules/db-operations/page.mdx": "2025-04-25T14:26:25.000Z", "app/learn/fundamentals/modules/multiple-services/page.mdx": "2025-03-18T15:11:44.632Z", - "app/learn/fundamentals/modules/page.mdx": "2025-03-18T07:51:09.049Z", + "app/learn/fundamentals/modules/page.mdx": "2025-07-18T15:31:32.371Z", "app/learn/debugging-and-testing/instrumentation/page.mdx": "2025-06-16T10:40:52.922Z", "app/learn/fundamentals/api-routes/additional-data/page.mdx": "2025-04-17T08:50:17.036Z", "app/learn/fundamentals/workflows/variable-manipulation/page.mdx": "2025-04-24T13:14:43.967Z", @@ -77,8 +76,8 @@ export const generatedEditDates = { "app/learn/customization/custom-features/module/page.mdx": "2025-05-06T16:10:50.090Z", "app/learn/customization/custom-features/workflow/page.mdx": "2024-12-09T14:36:29.482Z", "app/learn/customization/extend-features/extend-create-product/page.mdx": "2025-01-06T11:18:58.250Z", - "app/learn/customization/custom-features/page.mdx": "2024-12-09T10:46:28.593Z", - "app/learn/customization/customize-admin/page.mdx": "2024-12-09T11:02:38.801Z", + "app/learn/customization/custom-features/page.mdx": "2025-07-21T08:17:33.542Z", + "app/learn/customization/customize-admin/page.mdx": "2025-07-21T08:19:07.699Z", "app/learn/customization/customize-admin/route/page.mdx": "2025-02-11T15:56:03.835Z", "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", @@ -90,15 +89,15 @@ export const generatedEditDates = { "app/learn/customization/integrate-systems/service/page.mdx": "2024-12-09T11:02:39.594Z", "app/learn/customization/next-steps/page.mdx": "2025-04-17T08:50:17.036Z", "app/learn/fundamentals/modules/infrastructure-modules/page.mdx": "2025-05-21T14:31:51.644Z", - "app/learn/introduction/architecture/page.mdx": "2025-05-21T14:31:51.644Z", + "app/learn/introduction/architecture/page.mdx": "2025-07-18T15:52:35.513Z", "app/learn/fundamentals/data-models/infer-type/page.mdx": "2025-03-18T07:41:01.936Z", "app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx": "2024-12-09T14:38:06.385Z", "app/learn/fundamentals/environment-variables/page.mdx": "2025-05-26T15:06:07.800Z", "app/learn/build/page.mdx": "2025-04-25T12:34:33.914Z", "app/learn/deployment/general/page.mdx": "2025-06-20T08:36:05.063Z", - "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2024-11-25T16:19:32.169Z", - "app/learn/installation/page.mdx": "2025-05-16T13:44:27.118Z", - "app/learn/fundamentals/data-models/check-constraints/page.mdx": "2024-12-06T14:34:50.384Z", + "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2025-08-01T14:59:59.501Z", + "app/learn/installation/page.mdx": "2025-07-23T14:28:50.404Z", + "app/learn/fundamentals/data-models/check-constraints/page.mdx": "2025-07-25T13:50:21.065Z", "app/learn/fundamentals/module-links/link/page.mdx": "2025-04-07T08:03:14.513Z", "app/learn/fundamentals/workflows/store-executions/page.mdx": "2025-04-17T08:29:10.166Z", "app/learn/fundamentals/plugins/create/page.mdx": "2025-04-17T08:29:09.910Z", @@ -106,22 +105,27 @@ export const generatedEditDates = { "app/learn/customization/reuse-customizations/page.mdx": "2025-01-22T10:01:57.665Z", "app/learn/update/page.mdx": "2025-01-27T08:45:19.030Z", "app/learn/fundamentals/module-links/query-context/page.mdx": "2025-02-12T16:59:20.963Z", - "app/learn/fundamentals/admin/environment-variables/page.mdx": "2025-05-26T15:02:25.624Z", + "app/learn/fundamentals/admin/environment-variables/page.mdx": "2025-08-01T13:16:25.172Z", "app/learn/fundamentals/api-routes/parse-body/page.mdx": "2025-04-17T08:29:10.145Z", - "app/learn/fundamentals/admin/routing/page.mdx": "2025-02-24T09:50:37.495Z", + "app/learn/fundamentals/admin/routing/page.mdx": "2025-07-25T07:35:18.038Z", "app/learn/resources/contribution-guidelines/admin-translations/page.mdx": "2025-02-11T16:57:46.726Z", "app/learn/resources/contribution-guidelines/docs/page.mdx": "2025-05-26T15:55:02.974Z", "app/learn/resources/usage/page.mdx": "2025-02-26T13:35:34.824Z", - "app/learn/configurations/medusa-config/page.mdx": "2025-06-10T13:18:26.064Z", - "app/learn/configurations/ts-aliases/page.mdx": "2025-02-11T16:57:46.683Z", - "app/learn/production/worker-mode/page.mdx": "2025-03-11T15:21:50.906Z", - "app/learn/fundamentals/module-links/read-only/page.mdx": "2025-05-13T15:04:12.107Z", - "app/learn/fundamentals/data-models/properties/page.mdx": "2025-03-18T07:57:17.826Z", + "app/learn/configurations/medusa-config/page.mdx": "2025-07-14T09:28:54.302Z", + "app/learn/configurations/ts-aliases/page.mdx": "2025-07-23T15:32:18.008Z", + "app/learn/production/worker-mode/page.mdx": "2025-07-18T15:19:45.352Z", + "app/learn/fundamentals/module-links/read-only/page.mdx": "2025-07-25T07:58:54.327Z", + "app/learn/fundamentals/data-models/properties/page.mdx": "2025-07-31T08:22:20.431Z", "app/learn/fundamentals/framework/page.mdx": "2025-06-26T14:26:22.120Z", - "app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx": "2025-04-25T14:26:25.000Z", + "app/learn/fundamentals/api-routes/retrieve-custom-links/page.mdx": "2025-07-14T10:24:32.582Z", "app/learn/fundamentals/workflows/errors/page.mdx": "2025-04-25T14:26:25.000Z", "app/learn/fundamentals/api-routes/override/page.mdx": "2025-05-09T08:01:24.493Z", "app/learn/fundamentals/module-links/index/page.mdx": "2025-05-23T07:57:58.958Z", "app/learn/fundamentals/module-links/index-module/page.mdx": "2025-06-19T16:02:05.665Z", - "app/learn/introduction/build-with-llms-ai/page.mdx": "2025-06-27T12:07:08.147Z" + "app/learn/introduction/build-with-llms-ai/page.mdx": "2025-07-22T16:19:11.668Z", + "app/learn/installation/docker/page.mdx": "2025-07-23T15:34:18.530Z", + "app/learn/fundamentals/generated-types/page.mdx": "2025-07-25T13:17:35.319Z", + "app/learn/introduction/from-v1-to-v2/page.mdx": "2025-07-30T08:13:48.592Z", + "app/learn/debugging-and-testing/debug-workflows/page.mdx": "2025-07-30T13:45:14.117Z", + "app/learn/fundamentals/data-models/json-properties/page.mdx": "2025-07-31T14:25:01.268Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index 753fc9c7ee7f2..fe0ae55afd294 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -25,7 +25,18 @@ export const generatedSidebars = [ "type": "link", "path": "/learn/installation", "title": "Installation", - "children": [], + "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/installation/docker", + "title": "Install with Docker", + "children": [], + "chapterTitle": "1.2.1. Install with Docker", + "number": "1.2.1." + } + ], "chapterTitle": "1.2. Installation", "number": "1.2." }, @@ -48,6 +59,16 @@ export const generatedSidebars = [ "children": [], "chapterTitle": "1.4. AI Assistants and LLMs", "number": "1.4." + }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "title": "From v1 to v2", + "path": "/learn/introduction/from-v1-to-v2", + "children": [], + "chapterTitle": "1.5. From v1 to v2", + "number": "1.5." } ], "chapterTitle": "1. Getting Started", @@ -492,6 +513,16 @@ export const generatedSidebars = [ "chapterTitle": "3.5.2. Properties", "number": "3.5.2." }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/data-models/json-properties", + "title": "JSON Properties", + "children": [], + "chapterTitle": "3.5.3. JSON Properties", + "number": "3.5.3." + }, { "loaded": true, "isPathHref": true, @@ -499,8 +530,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/data-models/relationships", "title": "Relationships", "children": [], - "chapterTitle": "3.5.3. Relationships", - "number": "3.5.3." + "chapterTitle": "3.5.4. Relationships", + "number": "3.5.4." }, { "loaded": true, @@ -509,8 +540,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/data-models/manage-relationships", "title": "Manage Relationships", "children": [], - "chapterTitle": "3.5.4. Manage Relationships", - "number": "3.5.4." + "chapterTitle": "3.5.5. Manage Relationships", + "number": "3.5.5." }, { "loaded": true, @@ -519,8 +550,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/data-models/index", "title": "Define Index", "children": [], - "chapterTitle": "3.5.5. Define Index", - "number": "3.5.5." + "chapterTitle": "3.5.6. Define Index", + "number": "3.5.6." }, { "loaded": true, @@ -529,8 +560,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/data-models/check-constraints", "title": "Check Constraints", "children": [], - "chapterTitle": "3.5.6. Check Constraints", - "number": "3.5.6." + "chapterTitle": "3.5.7. Check Constraints", + "number": "3.5.7." }, { "loaded": true, @@ -539,8 +570,8 @@ export const generatedSidebars = [ "path": "/learn/fundamentals/data-models/write-migration", "title": "Migrations", "children": [], - "chapterTitle": "3.5.7. Migrations", - "number": "3.5.7." + "chapterTitle": "3.5.8. Migrations", + "number": "3.5.8." } ], "chapterTitle": "3.5. Data Models", @@ -809,9 +840,9 @@ export const generatedSidebars = [ "isPathHref": true, "type": "link", "path": "/learn/fundamentals/workflows/execute-another-workflow", - "title": "Execute Another Workflow", + "title": "Execute Nested Workflows", "children": [], - "chapterTitle": "3.7.13. Execute Another Workflow", + "chapterTitle": "3.7.13. Execute Nested Workflows", "number": "3.7.13." }, { @@ -823,6 +854,16 @@ export const generatedSidebars = [ "children": [], "chapterTitle": "3.7.14. Multiple Step Usage", "number": "3.7.14." + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "path": "/learn/debugging-and-testing/debug-workflows", + "title": "Debug Workflows", + "children": [], + "chapterTitle": "3.7.15. Debug Workflows", + "number": "3.7.15." } ], "chapterTitle": "3.7. Workflows", @@ -921,6 +962,16 @@ export const generatedSidebars = [ ], "chapterTitle": "3.11. Custom CLI Scripts", "number": "3.11." + }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/generated-types", + "title": "Auto-Generated Types", + "children": [], + "chapterTitle": "3.12. Auto-Generated Types", + "number": "3.12." } ], "chapterTitle": "3. Framework", @@ -1117,21 +1168,20 @@ export const generatedSidebars = [ "type": "link", "path": "/learn/debugging-and-testing/testing-tools/modules-tests", "title": "Modules Tests", - "children": [ - { - "loaded": true, - "isPathHref": true, - "type": "link", - "path": "/learn/debugging-and-testing/testing-tools/modules-tests/module-example", - "title": "Example", - "children": [], - "chapterTitle": "7.3.1. Example", - "number": "7.3.1." - } - ], + "children": [], "chapterTitle": "7.3. Modules Tests", "number": "7.3." }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/debugging-and-testing/debug-workflows", + "title": "Debug Workflows", + "children": [], + "chapterTitle": "7.4. Debug Workflows", + "number": "7.4." + }, { "loaded": true, "isPathHref": true, @@ -1146,12 +1196,12 @@ export const generatedSidebars = [ "path": "/resources/integrations/guides/sentry", "title": "Guide: Sentry", "children": [], - "chapterTitle": "7.4.1. Guide: Sentry", - "number": "7.4.1." + "chapterTitle": "7.5.1. Guide: Sentry", + "number": "7.5.1." } ], - "chapterTitle": "7.4. Instrumentation", - "number": "7.4." + "chapterTitle": "7.5. Instrumentation", + "number": "7.5." }, { "loaded": true, @@ -1160,8 +1210,8 @@ export const generatedSidebars = [ "path": "/learn/debugging-and-testing/logging", "title": "Logging", "children": [], - "chapterTitle": "7.5. Logging", - "number": "7.5." + "chapterTitle": "7.6. Logging", + "number": "7.6." } ], "chapterTitle": "7. Debugging & Testing", diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index bb0978b30df86..50e2c4ed7b5b8 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -193,6 +193,12 @@ const nextConfig = { destination: `${process.env.NEXT_PUBLIC_CLOUD_URL || "https://localhost:3001"}/cloud/:path*`, basePath: false, }, + { + source: "/mcp", + destination: + process.env.NEXT_MCP_SERVER_URL || "https://localhost:3001/mcp", + basePath: false, + }, ], } }, diff --git a/www/apps/book/package.json b/www/apps/book/package.json index 442c4ef050356..fae488601cdc2 100644 --- a/www/apps/book/package.json +++ b/www/apps/book/package.json @@ -16,13 +16,13 @@ "dependencies": { "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@medusajs/icons": "2.8.5", - "@next/mdx": "15.3.1", + "@medusajs/icons": "2.8.8", + "@next/mdx": "15.3.5", "@stefanprobst/rehype-extract-toc": "^3.0.0", "clsx": "^2.1.0", "docs-ui": "*", "docs-utils": "*", - "next": "15.3.1", + "next": "15.3.5", "react": "rc", "react-dom": "rc", "rehype-mdx-code-props": "^2.0.0", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index baccd30e187e8..6257e076ff5ff 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -140,6 +140,39 @@ module.exports = defineConfig({ The `projectConfig` object contains essential configurations related to the Medusa application, such as database and CORS configurations. +### cookieOptions + +This option is available since Medusa [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5). + +The `projectConfig.cookieOptions` configuration defines cookie options to be passed to `express-session` when creating the session cookie. This configuration is useful when simulating a production environment locally, where you may need to set options like `secure` or `sameSite`. + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + cookieOptions: { + sameSite: "lax", + }, + // ... + }, + // ... +}) +``` + +#### Properties + +Aside from the following options, you can pass any property that the [express-session's cookie option accepts](https://www.npmjs.com/package/express-session). + +- secure: (\`boolean\`) +- sameSite: (\`lax\` | \`strict\` | \`none\`) +- maxAge: (\`number\`) The maximum age of the cookie in milliseconds set in the \`Set-Cookie\` header. +- httpOnly: (\`boolean\`) Whether to set the \`HttpOnly Set-Cookie\` attribute. +- priority: (\`low\` | \`medium\` | \`high\`) The value of the \[Priority Set-Cookie attribute]\(https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1) +- domain: (\`string\`) The value of the \`Domain Set-Cookie\` attribute. By default, no domain is set, and most clients will consider the cookie to apply to the current domain only. +- path: (\`string\`) The value of the \`Path Set-Cookie\` attribute +- signed: (\`boolean\`) Whether to sign the cookie. + ### databaseDriverOptions The `projectConfig.databaseDriverOptions` configuration is an object of additional options used to configure the PostgreSQL connection. For example, you can support TLS/SSL connection using this configuration's `ssl` property. @@ -677,7 +710,7 @@ The value for this configuration can be one of the following: ```ts title="medusa-config.ts" module.exports = defineConfig({ projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", + workerMode: process.env.WORKER_MODE as "shared" | "worker" | "server" || "shared", // ... }, // ... @@ -962,17 +995,30 @@ npx medusa db:migrate # Using TypeScript Aliases -By default, Medusa doesn't support TypeScript aliases in production. +In this chapter, you'll learn how to use TypeScript aliases in your Medusa application. + +## Support for TypeScript Aliases + +By default, Medusa doesn't support TypeScript aliases in production. That means you may get build errors in production if you use them in your development. + +If you prefer using TypeScript aliases, this section will guide you through the steps to enable them in your Medusa application. + +### Step 1: Install Required Dependencies -If you prefer using TypeScript aliases, install following development dependencies: +Start by installing the following development dependencies: ```bash npm2yarn npm install --save-dev tsc-alias rimraf ``` -Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. +Where: + +- `tsc-alias` resolves TypeScript aliases. +- `rimraf` removes files and directories. + +### Step 2: Update `package.json` -Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: +Then, add a new `resolve:aliases` script to your `package.json` and update the existing `build` script: ```json title="package.json" { @@ -984,7 +1030,11 @@ Then, add a new `resolve:aliases` script to your `package.json` and update the ` } ``` -You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: +### Step 3: Update `tsconfig.json` + +Next, configure the TypeScript aliases you want to use in your `tsconfig.json` file by adding a `paths` property under `compilerOptions`. + +For example, to import anything under the `src` directory using type aliases, add the following in `tsconfig.json`: ```json title="tsconfig.json" { @@ -997,12 +1047,110 @@ You can now use TypeScript aliases in your Medusa application. For example, add } ``` -Now, you can import modules, for example, using TypeScript aliases: +### Step 4: Use TypeScript Aliases + +Then, you can use the `@` alias in your application code. + +For example, if you have a service in `src/modules/brand/service.ts`, you can import it like this: ```ts import { BrandModuleService } from "@/modules/brand/service" ``` +*** + +## Support TypeScript Aliases for Admin Customizations + +Medusa also doesn't support TypeScript aliases in the admin customizations by default. However, you can also configure your Medusa application to use TypeScript aliases in your admin customizations. + +### Step 1: Update `src/admin/tsconfig.json` + +Update `src/admin/tsconfig.json` to include `baseUrl` and `paths` configuration: + +```json title="src/admin/tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + + // other options... + } +} +``` + +The `baseUrl` option sets the base directory to `src/admin`, and the `paths` option defines the `@` alias to allow importing files from the `src/admin` directory using aliases. + +### Step 2: Update `medusa-config.ts` + +Next, update the `vite` configuration in `medusa-config.ts` to include the `resolve.alias` configuration: + +```ts title="medusa-config.ts" +import path from "path" + +module.exports = defineConfig({ + // ... + admin: { + vite: () => ({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src/admin"), + }, + }, + }), + }, +}) +``` + +Learn more about the `vite` configuration in the [Medusa configuration](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md) chapter. + +### Step 3: Use TypeScript Aliases in Admin Customizations + +You can now use the `@` alias in your admin customizations, just like you do in your main application code. + +For example, if you have a component in `src/admin/components/Container.tsx`, you can import it in a widget like this: + +```ts +import Container from "@/components/Container" +``` + +### Match TSConfig and Vite Alias Configuration + +Make sure that the `@` alias points to the same path as in your `src/admin/tsconfig.json`. + +For example, if you set the `@/*` alias to point to `./components/*`: + +```json title="src/admin/tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./components/*"] + } + } +} +``` + +Then, the `vite` alias configuration would be: + +```ts title="medusa-config.ts" +import path from "path" + +module.exports = defineConfig({ + // ... + admin: { + vite: () => ({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src/admin/components"), + }, + }, + }), + }, +}) +``` + # Guide: Create Brand API Route @@ -1376,7 +1524,7 @@ In the upcoming chapters, you'll follow step-by-step guides to build custom feat By following these guides, you'll add brands to the Medusa application that you can associate with products. -To build a custom feature in Medusa, you need three main tools: +To build a custom feature in Medusa, you need three main [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) tools: - [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. - [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. @@ -1539,10 +1687,10 @@ In the previous chapters, you've customized your Medusa application to [add bran After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: -- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. -- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). +- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), into existing pages. For example, you can add a widget to the product details page that shows the product's brand. +- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). For example, you can create a new page in the dashboard that lists all brands in the store. -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard +Within these customizations, you can send requests to custom API routes, allowing admin users to view and manage custom resources on the dashboard. *** @@ -3332,6 +3480,402 @@ Medusa provides the tooling to create a plugin package, test it in a local Medus To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +# Debug Workflows + +In this chapter, you'll learn about the different ways you can debug workflows in Medusa. + +Debugging workflows is essential to ensure your custom features work as expected. It helps you identify unexpected issues and bugs in your workflow logic. + +## Approaches to Debug Workflows + +There are several ways to debug workflows in Medusa: + +|Approach|When to Use| +|---|---| +|Write integration tests|To ensure your workflow produces the expected results and handles edge cases.| +|Add breakpoints|To inspect specific steps in your workflow and understand the data flow.| +|Log messages|To check values during execution with minimal overhead.| +|View Workflow Executions in Medusa Admin|To monitor stored workflow executions and long-running workflows, especially in production environments.| + +*** + +## Approach 1: Write Integration Tests + +Integration tests run your workflow in a controlled environment to verify its behavior and outcome. By writing integration tests, you ensure your workflow produces the expected results and handles edge cases. + +### When to Use Integration Tests + +It's recommended to always write integration tests for your workflows. This helps you catch issues early and ensures your custom logic works as intended. + +### How to Write Integration Tests for Workflows + +Refer to the [Integration Tests](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) chapter to learn how to write integration tests for your workflows and find examples of testing workflows. + +*** + +## Approach 2: Add Breakpoints + +Breakpoints allow you to pause workflow execution at specific steps and inspect the data. They're useful for understanding the data flow in your steps and identifying issues. + +### When to Use Breakpoints + +Use breakpoints when you need to debug specific steps in your workflow, rather than the entire workflow. You can verify that the step is behaving as expected and is producing the correct output. + +### Where Can You Add Breakpoints + +Since Medusa stores an internal representation of the workflow constructor on application startup, breakpoints within the workflow's constructor won't work during execution. Learn more in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter. + +Instead, you can add breakpoints in: + +- A step function. +- A step's compensation function. +- The `transform` callback function of a step. + +For example: + +### Step Function + +```ts highlights={[["11"], ["12"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // Add a breakpoint here to inspect the message + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + +### Compensation Function + +```ts highlights={[["18"], ["19"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + }, + async () => { + // Add a breakpoint here to inspect the compensation logic + console.log("Compensating step 1") + } +) + +const step2 = createStep( + "step-2", + async () => { + throw new Error("This is an error in step 2") + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + step2() + + return new WorkflowResponse({ + response, + }) + } +) +``` + +### Transform Callback + +```ts highlights={[["28"], ["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + const transformedMessage = transform( + { response }, + (data) => { + // Add a breakpoint here to inspect the transformed data + const upperCase = data.response.toUpperCase() + return upperCase + } + ) + + return new WorkflowResponse({ + response: transformedMessage, + }) + } +) +``` + +### How to Add Breakpoints + +If your code editor supports adding breakpoints, you can add them in your step and compensation functions, or the `transform` callback function. When the workflow execution reaches the breakpoint, your code editor will pause execution, allowing you to inspect the data and walk through the code. + +If you're using VS Code or Cursor, learn how to add breakpoints in the [VS Code documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_breakpoints). For other code editors, refer to their respective documentation. + +*** + +## Approach 3: Log Messages + +Logging messages is a simple yet effective way to debug code. By logging messages, you can check values during execution with minimal overhead. + +### When to Use Logging + +Use logging when debugging workflows and you want to check values during execution without the overhead of setting up breakpoints. + +Logging is also useful when you want to verify variable values between steps or in a `transform` callback function. + +### How to Log Messages + +Since Medusa stores an internal representation of the workflow constructor on application startup, you can't directly log messages in the workflow's constructor. + +Instead, you can log messages in: + +- A step function. +- A step's compensation function. +- The `transform` callback function of a step. + +You can log messages with `console.log`. In step and compensation functions, you can also use the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) to log messages with different log levels (info, warn, error). + +For example: + +### Step Function + +```ts highlights={[["14"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const logger = container.resolve("logger") + const message = "Hello from step 1!" + + logger.info(`Step 1 output: ${message}`) + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + +### Compensation Function + +```ts highlights={[["22"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const logger = container.resolve("logger") + const message = "Hello from step 1!" + + logger.info(`Step 1 output: ${message}`) + + return new StepResponse( + message + ) + }, + async (_, { container }) => { + const logger = container.resolve("logger") + logger.warn("Compensating step 1") + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + +### Transform Callback + +```ts highlights={[["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const response = step1() + + const transformedMessage = transform( + { response }, + (data) => { + const upperCase = data.response.toUpperCase() + console.log("Transformed Data:", upperCase) + return upperCase + } + ) + + return new WorkflowResponse({ + response: transformedMessage, + }) + } +) +``` + +If you execute the workflow, you'll see the logged message in your console. + +Learn more about logging in the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) chapter. + +*** + +## Approach 4: Monitor Workflow Executions in Medusa Admin + +The Medusa Admin has a [Workflows](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md) settings page that provides a user-friendly interface to view stored workflow executions. + +### When to Use Admin Monitoring + +Use the Medusa Admin to monitor [stored workflow executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) when debugging unexpected issues and edge cases, especially in production environments and long-running workflows that run in the background. + +By viewing the workflow executions through the Medusa Admin, you can: + +- View the status of stored workflow executions. +- Inspect input and output data for each execution and its steps. +- Identify any issues or errors in the workflow execution. + +### How to Monitor Workflow Executions in the Admin + +The Workflows settings page in the Medusa Admin shows you the history of stored workflow executions only. Workflow executions are stored if a workflow is [long-running](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), or if the `store` and `retentionTime` options are set on the workflow. + +For example, to store workflow executions: + +### Prerequisites + +- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) + +```ts highlights={[["22"], ["23"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = "Hello from step 1!" + + return new StepResponse( + message + ) + } +) + +export const myWorkflow = createWorkflow( + { + name: "my-workflow", + retentionTime: 99999, + store: true, + }, + () => { + const response = step1() + + return new WorkflowResponse({ + response, + }) + } +) +``` + +Refer to the [Store Workflow Executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) chapter to learn more. + +You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md). Each execution will show you the status, input, and output data. + + # Configure Instrumentation In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. @@ -4251,13 +4795,15 @@ The next chapters provide examples of writing integration tests for API routes a # 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. +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 Framework. + +For other debugging approaches, refer to the [Debug Workflows](https://docs.medusajs.com/learn/debugging-and-testing/debug-workflows/index.html.md) chapter. ### Prerequisites - [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -## Write Integration Test for Workflow +## Write Integration Test for a Workflow Consider you have the following workflow defined at `src/workflows/hello-world.ts`: @@ -4305,7 +4851,7 @@ medusaIntegrationTestRunner({ jest.setTimeout(60 * 1000) ``` -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test passes if the workflow returns the string `"Hello, World!"`. ### Jest Timeout @@ -4318,28 +4864,28 @@ jest.setTimeout(60 * 1000) *** -## Run Test +## Run Tests Run the following command to run your tests: ```bash npm2yarn -npm run test:integration +npm run test:integration:http ``` -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). +If you don't have a `test:integration:http` 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 `integrations/http` directory. +This runs your Medusa application and runs the tests available under the `integration-tests/http` directory. *** ## Test That a Workflow Throws an Error -You might want to test that a workflow throws an error in certain cases. To test this: +You might want to verify that a workflow throws an error in certain edge cases. To test that a workflow throws an error: - Disable the `throwOnError` option when executing the workflow. - Use the returned `errors` property to check what errors were thrown. -For example, if you have a step that throws this error: +For example, if you have the following step in your workflow that throws a `MedusaError`: ```ts title="src/workflows/hello-world.ts" import { MedusaError } from "@medusajs/framework/utils" @@ -4359,7 +4905,7 @@ import { helloWorldWorkflow } from "../../src/workflows/hello-world" medusaIntegrationTestRunner({ testSuite: ({ getContainer }) => { describe("Test hello-world workflow", () => { - it("returns message", async () => { + it("should throw error when item doesn't exist", async () => { const { errors } = await helloWorldWorkflow(getContainer()) .run({ throwOnError: false, @@ -4375,56 +4921,127 @@ medusaIntegrationTestRunner({ jest.setTimeout(60 * 1000) ``` -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, which is the error thrown. If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +*** -# Example: Integration Tests for a Module +## Test Long-Running Workflows -In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. +Since [long-running workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) run asynchronously, testing them requires a different approach than synchronous workflows. -### Prerequisites +When testing long-running workflows, you need to: -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +1. Set the asynchronous steps as successful manually. +2. Subscribe to the workflow's events to listen for the workflow execution's completion. +3. Verify the output of the workflow after it has completed. -## Write Integration Test for Module +For example, consider you have the following long-running workflow defined at `src/workflows/long-running-workflow.ts`: -Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: +```ts title="src/workflows/long-running-workflow.ts" highlights={[["15"]]} +import { + createStep, + createWorkflow, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -```ts title="src/modules/blog/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) -class BlogModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") } -} +) -export default BlogModuleService +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) + +const longRunningWorkflow = createWorkflow( + "long-running", + function () { + step1() + step2() + const message = step3() + + return new WorkflowResponse({ + message, + }) + } +) + +export default longRunningWorkflow ``` -To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: +`step2` in this workflow is an asynchronous step that you need to set as successful manually in your test. -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import MyCustom from "../models/my-custom" +You can write the following test to ensure that the long-running workflow completes successfully: -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - describe("BlogModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() +```ts title="integration-tests/http/long-running-workflow.spec.ts" highlights={longRunningWorkflowHighlights1} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import longRunningWorkflow from "../../src/workflows/long-running-workflow" +import { Modules, TransactionHandlerType } from "@medusajs/framework/utils" +import { StepResponse } from "@medusajs/framework/workflows-sdk" - expect(message).toEqual("Hello, World!") +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test long-running workflow", () => { + it("returns message", async () => { + const container = getContainer() + const { transaction } = await longRunningWorkflow(container) + .run() + + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + let workflowOk: any + const workflowCompletion = new Promise((ok) => { + workflowOk = ok + }) + + const subscriptionOptions = { + workflowId: "long-running", + transactionId: transaction.transactionId, + subscriberId: "long-running-subscriber", + } + + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + workflowOk(data.result.message) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } + }, + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-2", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done!"), + }) + + const afterSubscriber = await workflowCompletion + + expect(afterSubscriber).toBe("Finished three steps") }) }) }, @@ -4433,21 +5050,268 @@ moduleIntegrationTestRunner({ jest.setTimeout(60 * 1000) ``` -You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. +In this test, you: + +1. Execute the long-running workflow and get the transaction details from the `run` method's result. +2. Resolve the [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md)'s service from the Medusa container. +3. Create a promise to wait for the workflow's completion. +4. Subscribe to the workflow's events using the Workflow Engine Module's `subscribe` method. + - The `subscriber` function is called whenever an event related to the workflow occurs. On the `onFinish` event that indicates the workflow has completed, you resolve the promise with the workflow's result. +5. Set the asynchronous step as successful using the `setStepSuccess` method of the Workflow Engine Module. +6. Wait for the promise to resolve, which indicates that the workflow has completed successfully. +7. Finally, you assert that the workflow's result matches the expected output. + +If you run the integration test, it will execute the long-running workflow and verify that it completes and returns the expected result. + +### Example with Multiple Asynchronous Steps + +If your long-running workflow has multiple asynchronous steps, you must set each of them as successful in your test before the workflow can complete. + +Here's how the test would look like if you had two asynchronous steps: + +```ts title="integration-tests/http/long-running-workflow-multiple-steps.spec.ts" highlights={longRunningWorkflowHighlights2} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import longRunningWorkflow from "../../src/workflows/long-running-workflow" +import { Modules, TransactionHandlerType } from "@medusajs/framework/utils" +import { StepResponse } from "@medusajs/framework/workflows-sdk" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test long-running workflow with multiple async steps", () => { + it("returns message", async () => { + const container = getContainer() + const { transaction } = await longRunningWorkflow(container) + .run() + + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + let workflowOk: any + const workflowCompletion = new Promise((ok) => { + workflowOk = ok + }) + + const subscriptionOptions = { + workflowId: "long-running", + transactionId: transaction.transactionId, + subscriberId: "long-running-subscriber", + } + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + workflowOk(data.result.message) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } + }, + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-2", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done!"), + }) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId: transaction.transactionId, + stepId: "step-3", + workflowId: "long-running", + }, + stepResponse: new StepResponse("Done with step 3!"), + }) + + const afterSubscriber = await workflowCompletion + + expect(afterSubscriber).toBe("Finished three steps") + }) + }) + }, +}) +``` + +In this example, you set both `step-2` and `step-3` as successful before waiting for the workflow to complete. *** -## Run Test +## Test Database Operations in Workflows -Run the following command to run your module integration tests: +In real use cases, you'll often test workflows that perform database operations, such as creating a brand. -```bash npm2yarn -npm run test:integration:modules +When you test such workflows, you may need to: + +- Verify that the database operations were performed correctly. For example, that a brand was created with the expected properties. +- Perform database actions before testing the workflow. For example, creating a brand before testing a workflow that deletes it. + +This section provides examples of both scenarios. + +### Verify Database Operations in Workflow Test + +To retrieve data from the database after running a workflow, you can resolve and use either the module's service (for example, the Brand Module's service) or [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +For example, the following test verifies that a brand was created by a workflow: + +```ts title="integration-tests/http/workflow-brand.spec.ts" highlights={workflowBrandHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { createBrandWorkflow } from "../../src/workflows/create-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test create brand workflow", () => { + it("creates a brand", async () => { + const container = getContainer() + const { result: brand } = await createBrandWorkflow(container) + .run({ + input: { + name: "Test Brand", + }, + }) + + const brandModuleService = container.resolve(BRAND_MODULE) + + const createdBrand = await brandModuleService.retrieveBrand(brand.id) + expect(createdBrand).toBeDefined() + expect(createdBrand.name).toBe("Test Brand") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) ``` -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). +In this test, you run the workflow, which creates a brand. Then, you retrieve the brand from the database using the Brand Module's service and verify that it was created with the expected properties. -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +### Perform Database Actions Before Testing Workflow + +You can perform database actions before testing workflows in the `beforeAll` or `beforeEach` hooks of your test suite. In those hooks, you can create data that is useful for your workflow tests. + +Learn more about test hooks in [Jest's Documentation](https://jestjs.io/docs/setup-teardown). + +You can perform the database actions before testing a workflow by either: + +- Using the module's service (for example, the Brand Module's service). +- Using an existing workflow that performs the database actions. + +#### Use Module's Service + +For example, the following test creates a brand using the Brand Module's service before running the workflow that deletes it: + +```ts title="integration-tests/http/workflow-brand-delete.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { deleteBrandWorkflow } from "../../src/workflows/delete-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + let brandId: string + + beforeAll(async () => { + const container = getContainer() + + const brandModuleService = container.resolve(BRAND_MODULE) + + const brand = await brandModuleService.createBrands({ + name: "Test Brand", + }) + + brandId = brand.id + }) + + describe("Test delete brand workflow", () => { + it("deletes a brand", async () => { + const container = getContainer() + const { result } = await deleteBrandWorkflow(container) + .run({ + input: { + id: brandId, + }, + }) + + expect(result.success).toBe(true) + + const brandModuleService = container.resolve(BRAND_MODULE) + await expect(brandModuleService.retrieveBrand(brandId)) + .rejects.toThrow() + }) + }) + }, +}) +``` + +In this example, you: + +1. Use the `beforeAll` hook to create a brand before running the workflow that deletes it. +2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand. +3. Verify that the brand was deleted successfully by checking that retrieving it throws an error. + +#### Use Existing Workflow + +Alternatively, if you already have a workflow that performs the database operations, you can use that workflow in the `beforeAll` or `beforeEach` hook. This is useful if the database operations are complex and are already encapsulated in a workflow. + +For example, you can modify the `beforeAll` hook to use the `createBrandWorkflow`: + +```ts title="integration-tests/http/workflow-brand-delete.spec.ts" highlights={workflowBrandDeleteHighlights2} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { deleteBrandWorkflow } from "../../src/workflows/delete-brand" +import { createBrandWorkflow } from "../../src/workflows/create-brand" +import { BRAND_MODULE } from "../../src/modules/brand" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + let brandId: string + + beforeAll(async () => { + const container = getContainer() + + const { result: brand } = await createBrandWorkflow(container) + .run({ + input: { + name: "Test Brand", + }, + }) + + brandId = brand.id + }) + + describe("Test delete brand workflow", () => { + it("deletes a brand", async () => { + const container = getContainer() + const { result } = await deleteBrandWorkflow(container) + .run({ + input: { + id: brandId, + }, + }) + + expect(result.success).toBe(true) + + const brandModuleService = container.resolve(BRAND_MODULE) + await expect(brandModuleService.retrieveBrand(brandId)) + .rejects.toThrow() + }) + }) + }, +}) +``` + +In this example, you: + +1. Use the `beforeAll` hook to run the `createBrandWorkflow`, which creates a brand before running the workflow that deletes it. +2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand. +3. Verify that the brand was deleted successfully by checking that retrieving it throws an error. # Write Tests for Modules @@ -4460,9 +5324,26 @@ In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's ## moduleIntegrationTestRunner Utility -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. +`moduleIntegrationTestRunner` creates integration tests for a module's service. 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`: +For example, consider a Blog Module with a `BlogModuleService` that has a `getMessage` method: + +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + async getMessage(): Promise { + return "Hello, World!" + } +} + +export default BlogModuleService +``` + +To create an integration test for the module's service, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: ```ts title="src/modules/blog/__tests__/service.spec.ts" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" @@ -4475,7 +5356,13 @@ moduleIntegrationTestRunner({ moduleModels: [Post], resolve: "./src/modules/blog", testSuite: ({ service }) => { - // TODO write tests + describe("BlogModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) }, }) @@ -4484,10 +5371,10 @@ jest.setTimeout(60 * 1000) The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: -- `moduleName`: The name of the module. +- `moduleName`: The registration 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. +- `testSuite`: A function that defines [Jest](https://jestjs.io/) 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. @@ -4529,6 +5416,8 @@ moduleIntegrationTestRunner({ }) ``` +`moduleOptions` is an object of key-value pair options that your module's service receives in its constructor. + *** ## Write Tests for Modules without Data Models @@ -4556,6 +5445,52 @@ jest.setTimeout(60 * 1000) *** +## Inject Dependencies in Module Tests + +Some modules have injected dependencies, such as the [Event Module's service](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md). When writing tests for those modules, you need to inject the dependencies that the module's service requires to avoid errors. + +You can inject dependencies as mock dependencies that simulate the behavior of the original service. This way you avoid unexpected behavior or results, such as sending real events or making real API calls. + +To inject dependencies, pass the `injectedDependencies` property to the `moduleIntegrationTestRunner` function. + +For example: + +```ts title="src/modules/blog/__tests__/service.spec.ts" highlights={mockDependenciesHighlights} +import { MockEventBusService, moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import Post from "../models/post" +import { Modules } from "@medusajs/framework/utils" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./src/modules/blog", + injectedDependencies: { + [Modules.EVENT_BUS]: new MockEventBusService(), + }, + testSuite: ({ service }) => { + describe("BlogModuleService", () => { + it("says hello world", async () => { + const message = await service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +`injectedDependencies`'s value is an object whose keys are registration names of the dependencies you want to inject, and the values are the mock services. + +In this example, you inject a mock Event Module service into the `BlogModuleService`. Medusa exposes a `MockEventBusService` class that you can use to mock the Event Module's service. + +For other modules, you can create a mock service that implements the same interface as the original service. Make sure to use the same registration name as the original service when injecting it. + +*** + ### 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. @@ -4608,6 +5543,7 @@ module.exports = { { jsc: { parser: { syntax: "typescript", decorators: true }, + target: "es2021", }, }, ], @@ -4645,7 +5581,7 @@ Finally, add the following scripts to `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:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" }, ``` @@ -4984,7 +5920,7 @@ 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) +![Medusa deployment architecture showing the relationship between three main components: the Medusa server (backend API), Medusa Admin dashboard (for store management), and customer-facing storefronts, all connected to facilitate complete ecommerce functionality](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. @@ -4996,7 +5932,7 @@ You must deploy the Medusa application before the storefront, as it connects to 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) +![Medusa server deployment infrastructure diagram illustrating the backend services ecosystem: the Node.js Medusa server connected to essential services including PostgreSQL database for data storage, and Redis for caching and session management](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). @@ -5045,11 +5981,11 @@ Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.h # Admin Development Constraints -This chapter lists some constraints of admin widgets and UI routes. +This chapter lists some development constraints of admin widgets and UI routes. ## Arrow Functions -Widget and UI route components must be created as arrow functions. +Widget and UI route components must be created as arrow functions. Otherwise, Medusa doesn't register them correctly. ```ts highlights={arrowHighlights} // Don't @@ -5067,7 +6003,7 @@ const ProductWidget = () => { ## Widget Zone -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. Otherwise, Medusa doesn't register the widget correctly. ```ts highlights={zoneHighlights} // Don't @@ -5143,10 +6079,16 @@ If you receive a type error on `import.meta.env`, create the file `src/admin/vit ```ts title="src/admin/vite-env.d.ts" /// + +declare const __BASE__: string +declare const __BACKEND_URL__: string +declare const __STOREFRONT_URL__: string ``` This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +Note that the `__BASE__`, `__BACKEND_URL__`, and `__STOREFRONT_URL__` variables are global variables available in your admin customizations. Learn more in the [Tips for Admin Customizations](https://docs.medusajs.com/learn/fundamentals/admin/tips#global-variables-in-admin-customizations/index.html.md) chapter. + *** ## Check Node Environment in Admin Customizations @@ -5201,18 +6143,28 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` +To fix possible type errors, create the file `src/admin/vite-env.d.ts` and add the global variables: + +```ts title="src/admin/vite-env.d.ts" +/// + +declare const __BACKEND_URL__: string +declare const __BASE__: string +declare const __STOREFRONT_URL__: string +``` + # Admin Development In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it. -## What is the Medusa Admin? +To explore the Medusa Admin's commerce features, check out the [User Guide](https://docs.medusajs.com/user-guide/index.html.md). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin. -The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management featuers related to products, orders, customers, and more. +## What is the Medusa Admin? -To explore more what you can do with the Medusa Admin, check out the [User Guide](https://docs.medusajs.com/user-guide/index.html.md). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin. +The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management features related to products, orders, customers, and more. -The Medusa Admin is built with [Vite](https://vite.dev/). When you [install the Medusa application](https://docs.medusajs.com/learn/installation/index.html.md), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`. +The Medusa Admin is built with [Vite v5](https://v5.vite.dev/). When you [install the Medusa application](https://docs.medusajs.com/learn/installation/index.html.md), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`. If you don't have an admin user, use the [Medusa CLI](https://docs.medusajs.com/resources/medusa-cli/commands/user/index.html.md) to create one. @@ -5229,7 +6181,7 @@ The next chapters will cover these two topics in detail. ### What You Can't Customize in the Medusa Admin -You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). +You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). You also can't customize the login page, the authentication flow, or change the Medusa logo used in the admin dashboard. If your use case requires heavy customization of the admin dashboard, you can build a custom admin dashboard using Medusa's [Admin API routes](https://docs.medusajs.com/api/admin). @@ -5250,9 +6202,9 @@ To build admin customizations that match the Medusa Admin's designs and layouts, # 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. +The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. This gives you more flexibility in routing-related customizations using React Router's utilities, hooks, and components. -In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. +In this chapter, you'll learn about routing-related customizations that you can use in your widgets, UI routes, and settings pages using React Router. `react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. @@ -5286,13 +6238,20 @@ This adds a widget to a product's details page with a link to the Orders page. T *** -## Admin Route Loader +## Fetch Data with Route Loaders Route loaders are available starting from Medusa v2.5.1. -In your UI route or any other custom admin route, you may need to retrieve data to use it in your route component. For example, you may want to fetch a list of products to display on a custom page. +In your UI routes and settings pages, you may need to retrieve data to use in your route component. For example, you may want to fetch a list of products to display on a custom page. + +The recommended approach is to fetch data within the UI route component asynchronously using the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md). + +However, if you need the data to be fetched before the route is rendered, such as if you're [setting breadcrumbs dynamically](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes#set-breadcrumbs-dynamically/index.html.md), you can use a route loader. -To do that, you can export a `loader` function in the route file, which is a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader). In this function, you can fetch and return data asynchronously. Then, in your route component, you can use the [useLoaderData](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) hook from React Router to access the data. +To fetch data with a route loader: + +1. Define and export a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader) function in the UI route's file. In this function, you can fetch and return data asynchronously. +2. In your UI route's component, you can use the [useLoaderData hook from React Router](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) to access the data returned by the `loader` function. For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: @@ -5331,9 +6290,17 @@ In this example, you first export a `loader` function that can be used to fetch Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component. -### Route Parameters +### Route Loaders Block Rendering + +Route loaders block the rendering of your UI route until the data is fetched, which may negatively impact the user experience. So, only use route loaders when the route component needs essential data before rendering, or if you're preparing data that doesn't require sending API requests. + +Otherwise, use the JS SDK with Tanstack (React) Query in the UI route component as explained in the [Tips](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) chapter to fetch data asynchronously and update the UI when the data is available. -You can also access route params in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: +![Timeline comparison of a UI route with and without a route loader](https://res.cloudinary.com/dza7lstvk/image/upload/v1753428567/Medusa%20Book/ui-route-loading_vycev8.jpg) + +### Access Route Parameters in Loader + +You can access route parameters in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: ```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} import { Container, Heading } from "@medusajs/ui" @@ -5368,16 +6335,10 @@ const CustomPage = () => { export default CustomPage ``` -Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object of type `LoaderFunctionArgs` from React Router. This object has a `params` property that contains the route parameters. +Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object that has a `params` property containing the route parameters. In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook. -### When to Use Route Loaders - -A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. - -Only use route loaders when the route component needs data essential before rendering. Otherwise, use the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md). This way, you can fetch data asynchronously and update the UI when the data is available. You can also use a loader to prepare some initial data that's used in the route component before the data is retrieved. - *** ## Other React Router Utilities @@ -5396,6 +6357,8 @@ export const handle = { } ``` +You can also use the `handle` object to define a breadcrumb for the route. Learn more in the [UI Route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) chapter. + ### React Router Components and Hooks 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. @@ -5538,6 +6501,16 @@ In your admin customizations, you can use the following global variables: - `__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`. +If you get type errors while using these variables, you can create the file `src/admin/vite-env.d.ts` with the following content: + +```ts title="src/admin/vite-env.d.ts" +/// + +declare const __BASE__: string +declare const __BACKEND_URL__: string +declare const __STOREFRONT_URL__: string +``` + *** ## Admin Translations @@ -5553,7 +6526,7 @@ In this chapter, you’ll learn how to create a UI route in the admin dashboard. ## What is a UI Route? -The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. +The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions. For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. @@ -5641,7 +6614,7 @@ The above example adds a new sidebar item with the label `Custom Route` and an i ### Nested UI Routes -Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: +Consider that alongside the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: ![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) @@ -5673,8 +6646,8 @@ This UI route is shown in the sidebar as an item nested in the parent "Custom Ro Some caveats for nested UI routes in the sidebar: - Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. -- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. -- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. +- Nested routes in settings pages aren't shown in the sidebar to follow the admin's design conventions. +- The `icon` configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions. ### Route Under Existing Admin Route @@ -5768,7 +6741,125 @@ export default CustomPage You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params). -If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. +If you run the Medusa application and go to `http://localhost:9000/app/custom/123`, you'll see `123` printed in the page. + +*** + +## Set UI Route Breadcrumbs + +The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes. + +To set the breadcrumbs of a UI route, export a `handle` object with a `breadcrumb` property in the UI route's file: + +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the UI route."]]} +import { Container, Heading } from "@medusajs/ui" + +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} + +export default CustomPage + +export const handle = { + breadcrumb: () => "Custom Route", +} +``` + +The `breadcrumb`'s value is a function that returns the breadcrumb label as a string, or a React JSX element. + +### Set Breadcrumbs for Nested UI Routes + +If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route. + +For example, if you have the following UI route at `src/admin/routes/custom/nested/page.tsx` that's nested under the previous one: + +```tsx title="src/admin/routes/custom/nested/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the nested UI route."]]} +import { Container, Heading } from "@medusajs/ui" + +const NestedCustomPage = () => { + return ( + +
+ This is my nested custom route +
+
+ ) +} + +export default NestedCustomPage + +export const handle = { + breadcrumb: () => "Nested Custom Route", +} +``` + +Then, when you open the nested route at `http://localhost:9000/app/custom/nested`, you'll see the breadcrumbs as `Custom Route > Nested Custom Route`. Each breadcrumb is clickable, allowing users to navigate back to the parent route. + +### Set Breadcrumbs Dynamically + +In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically. + +To do that, you can: + +1. Define and export a `loader` function in the UI route file that fetches the data needed for the breadcrumb. +2. Receive the data in the `breadcrumb` function and return the dynamic label. + +For example, create a UI route at `src/admin/routes/brands/[id]/page.tsx` with the following content: + +```tsx title="src/admin/routes/brands/[id]/page.tsx" highlights={dynamicBreadcrumbsHighlights} +import { Container, Heading } from "@medusajs/ui" +import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom" +import { sdk } from "../../../lib/sdk" + +type BrandResponse = { + brand: { + name: string + } +} + +const BrandPage = () => { + const { brand } = useLoaderData() as Awaited + + return ( + +
+ {brand.name} +
+
+ ) +} + +export default BrandPage + +export async function loader({ params }: LoaderFunctionArgs) { + const { id } = params + const { brand } = await sdk.client.fetch(`/admin/brands/${id}`) + + return { + brand, + } +} + +export const handle = { + breadcrumb: ( + { data }: UIMatch + ) => data.brand.name || "Brand", +} +``` + +In the `loader` function, you retrieve the brands from a custom API route and return them. + +Then, in the `handle.breadcrumb` function, you receive `data` prop containing the brand information returned by the `loader` function. You can use this data to return a dynamic breadcrumb label. + +When you open the UI route at `http://localhost:9000/app/brands/123`, the breadcrumb will show the brand's name, such as `Acme`. + +You also use the `useLoaderData` hook to access the data returned by the `loader` function in the UI route component. Learn more in the [Routing Customizations](https://docs.medusajs.com/learn/fundamentals/admin/routing#fetch-data-with-route-loaders/index.html.md) chapter. *** @@ -5785,13 +6876,13 @@ For more customizations related to routes, refer to the [Routing Customizations # Admin Widgets -In this chapter, you’ll learn more about widgets and how to use them. +In this chapter, you’ll learn about widgets and how to use them. ## What is an Admin Widget? -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. +The Medusa Admin's pages are customizable for inserting widgets of custom content in pre-defined injection zones. For example, you can add a widget on the product details page that allows admin users to sync products to a third-party service. -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +You create these widgets as React components that render the content and functionality of the widget. *** @@ -5801,7 +6892,10 @@ For example, you can add a widget on the product details page that allow admin u - [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. +You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file must export: + +1. A React component that renders the widget. This will be the file's default export. +2. The widget’s configurations indicating where to insert the widget. For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: @@ -5830,11 +6924,11 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. +In the example above, the widget is injected at the top of a product’s details. -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. +You export the `ProductWidget` component, which displays the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md) to customize the dashboard with the same components used to build it. -In the example above, the widget is injected at the top of a product’s details. +To export the widget's configuration, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts an object as a parameter with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. The widget component must be created as an arrow function. @@ -5850,9 +6944,9 @@ Then, open a product’s details page. You’ll find your custom widget at the t *** -## Props Passed in Detail Pages +## Props Passed to Widgets on Detail Pages -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. +Widgets that are injected into a detail page receive a `data` prop, which is the main data of the details page. For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: @@ -5891,15 +6985,62 @@ The props type is `DetailWidgetProps`, and it accepts as a type argument the exp *** -## Injection Zone +## Injection Zones List -Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. +Refer to the [Admin Widget Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) reference for the full list of injection zones and their props. *** ## 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. +While the Medusa Admin uses the [Medusa UI](https://docs.medusajs.com/ui/index.html.md) components, it also expands on them for styling and design purposes. + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to the [Admin Components](https://docs.medusajs.com/resources/admin-components/index.html.md) guide. You'll find components like `Header`, `JSON View`, and more that match the Medusa Admin's design. + +*** + +## Show Widgets Conditionally + +In some cases, you may want to show a widget only if certain conditions are met. For example, you may want to show a widget only if the product has a brand. + +To prevent the widget from showing, return an empty fragment from the widget component: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + if (!data.metadata?.brand) { + return <> // Don't show the widget if the product has no brand + } + + return ( + +
+ + Brand: {data.metadata.brand} + +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In the above example, you return an empty fragment if the product has no brand. Otherwise, you show the brand name in the widget. # Pass Additional Data to Medusa's API Route @@ -6366,7 +7507,7 @@ In this chapter, you'll learn about how to add new API routes for each HTTP meth ## HTTP Method Handler -An API route is created for every HTTP method you export a handler function for in a route file. +An API route is created for every HTTP method you export a handler function for in a `route.ts` or `route.js` file. Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. @@ -6397,10 +7538,10 @@ export const POST = async ( } ``` -This adds two API Routes: +This file adds two API Routes: -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. +- A `GET` API route at `http://localhost:9000/hello-world`. +- A `POST` API route at `http://localhost:9000/hello-world`. # Middlewares @@ -6413,7 +7554,7 @@ A middleware is a function executed when a request is sent to an API Route. It's Middlewares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. -![Diagram showcasing how a middleware is executed when a request is sent to an API route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg) +![API middleware execution flow diagram showing how HTTP requests first pass through middleware functions for authentication, validation, and data processing before reaching the actual route handler, providing a secure and flexible request processing pipeline in Medusa applications](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg) As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). @@ -6836,25 +7977,25 @@ Some examples of when you might want to replicate an API route include: # API Routes -In this chapter, you’ll learn what API Routes are and how to create them. +In this chapter, you’ll learn what API routes are and how to create them. ## What is an API Route? -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. +An API route is a REST endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. -The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. +The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. *** ## How to Create an API Route? -An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. +You can create an API route in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. ![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). +Each file exports API route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). -For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: +For example, to create a `GET` API route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: ```ts title="src/api/hello-world/route.ts" import type { @@ -6872,7 +8013,7 @@ export const GET = ( } ``` -### Test API Route +### Test the API Route To test the API route above, start the Medusa application: @@ -6880,17 +8021,25 @@ To test the API route above, start the Medusa application: npm run dev ``` -Then, send a `GET` request to the `/hello-world` API Route: +Then, send a `GET` request to the `/hello-world` API route: ```bash curl http://localhost:9000/hello-world ``` +You should receive the following response: + +```json +{ + "message": "[GET] Hello world!" +} +``` + *** -## When to Use API Routes +## Next Chapters: Learn More About API Routes -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. +Follow the next chapters to learn about the different HTTP methods you can use in API routes, parameters and validation, protecting routes, and more. # API Route Parameters @@ -7480,7 +8629,7 @@ In this chapter, you'll learn how to send a response in your API route. ## Send a JSON Response -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +To send a JSON response, use the `json` method of the `MedusaResponse` object that is passed as the second parameter of your API route handler. For example: @@ -7536,7 +8685,9 @@ The response of this API route has the status code `201`. To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. -For example, to create an API route that returns an event stream: +### Example: Server-Sent Events (SSE) + +For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`: ```ts highlights={streamHighlights} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" @@ -7552,9 +8703,14 @@ export const GET = async ( }) const interval = setInterval(() => { - res.write("Streaming data...\n") + res.write("data: Streaming data...\n\n") }, 3000) + req.on("close", () => { + clearInterval(interval) + res.end() + }) + req.on("end", () => { clearInterval(interval) res.end() @@ -7564,10 +8720,16 @@ export const GET = async ( The `writeHead` method accepts two parameters: -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. +1. The first parameter is the response's status code. +2. The second parameter is an object of key-value pairs to set the response headers. + +This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. +### Tip: Fetching Stream with JS SDK + +The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream. + +Learn more in the [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) documentation. *** @@ -7619,7 +8781,7 @@ The API routes that restrict the fields and relations you can retrieve are: ### 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 these routes, you need to override the allowed fields and relations to be retrieved. You can do this by applying a [global 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: @@ -7632,10 +8794,9 @@ export default defineMiddlewares({ routes: [ { matcher: "/store/customers/me", - method: "GET", middlewares: [ (req, res, next) => { - req.allowed?.push("b2b_company") + (req.allowed ??= []).push("b2b_company") next() }, ], @@ -7657,6 +8818,8 @@ curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. +This approach only works using a global middleware. It doesn't work in a route middleware. + # Request Body and Query Parameter Validation @@ -7909,17 +9072,22 @@ To see different examples and learn more about creating a validation schema, ref # Custom CLI Scripts -In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. +In this chapter, you'll learn how to create and execute custom scripts using Medusa's CLI tool. ## What is a Custom CLI Script? -A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. +A custom CLI script is a function that you can execute using Medusa's CLI tool. It is useful when you need a script that has access to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and can be executed using Medusa's CLI. + +For example, you can create a custom CLI script that: + +- [Seeds data into the database](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/seed-data/index.html.md). +- Runs a script before starting the Medusa application. *** ## How to Create a Custom CLI Script? -To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. +To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must export a function by default. For example, create the file `src/scripts/my-script.ts` with the following content: @@ -7942,13 +9110,13 @@ export default async function myScript({ container }: ExecArgs) { } ``` -The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. +The function receives as a parameter an object with a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. *** -## How to Run Custom CLI Script? +## How to Run a Custom CLI Script? -To run the custom CLI script, run the Medusa CLI's `exec` command: +To run a custom CLI script, run the Medusa CLI's `exec` command: ```bash npx medusa exec ./src/scripts/my-script.ts @@ -7960,7 +9128,7 @@ npx medusa exec ./src/scripts/my-script.ts Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. -For example: +For example, create the following CLI script that logs the command line arguments: ```ts import { ExecArgs } from "@medusajs/framework/types" @@ -7970,12 +9138,36 @@ export default async function myScript({ args }: ExecArgs) { } ``` -Then, pass the arguments in the `exec` command after the file path: +Then, run the script with the `exec` command and pass arguments after the script's path. ```bash npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` +*** + +## Run Custom Script on Application Startup + +In some cases, you may need to perform an action when the Medusa application starts. + +If the action is related to a module, you should use a [loader](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md). Otherwise, you can create a custom CLI script and run it before starting the Medusa application. + +To run a custom script on application startup, modify the `dev` and `start` commands in `package.json` to execute your script first. + +For example: + +```json title="package.json" +{ + "scripts": { + "startup": "medusa exec ./src/scripts/startup.ts", + "dev": "npm run startup && medusa develop", + "start": "npm run startup && medusa start" + } +} +``` + +The `startup` script will run every time you run the Medusa application. + # Seed Data with Custom CLI Script @@ -8236,6 +9428,68 @@ 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. +*** + +## Examples + +This section covers common use cases where check constraints are particularly useful. + +### 1. Enforce Text Length + +Ensure that text properties meet minimum or maximum length requirements: + +```ts +const User = model.define("user", { + username: model.text(), + password: model.text(), +}) +.checks([ + { + name: "password_length_check", + expression: (columns) => `LENGTH(${columns.password}) >= 8`, + }, +]) +``` + +In the above example, the check constraint fails if the `password` property is less than 8 characters long. + +### 2. Validate Email Format + +Ensure email addresses contain the `@` symbol: + +```ts +const Customer = model.define("customer", { + email: model.text(), +}) +.checks([ + { + name: "email_format_check", + expression: (columns) => `${columns.email} LIKE '%@%'`, + }, +]) +``` + +In the above example, the check constraint fails if the `email` property does not contain the `@` symbol. + +### 3. Enforce Date Ranges + +Ensure dates fall within valid ranges: + +```ts +const Event = model.define("event", { + start_date: model.dateTime(), + end_date: model.dateTime(), +}) +.checks([ + { + name: "date_order_check", + expression: (columns) => `${columns.end_date} >= ${columns.start_date}`, + }, +]) +``` + +In the above example, the check constraint fails if the `end_date` is earlier than the `start_date`. + # Data Model Database Index @@ -8389,6 +9643,243 @@ class BlogModuleService extends MedusaService({ Post }) { ``` +# JSON Properties in Data Models + +In this chapter, you'll learn how to use and manage [JSON properties](https://docs.medusajs.com/learn/fundamentals/data-models/properties#json/index.html.md) in data models. + +## What is a JSON Property? + +A JSON property in a data model is a flexible object that can contain various types of values. + +For example, you can create a `Brand` data model with a `metadata` JSON property to store additional information about the brand: + +```ts highlights={[["6"]]} +import { model } from "@medusajs/framework/utils" + +const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), + metadata: model.json(), +}) +``` + +### Accepted Values in JSON Property + +JSON properties are made up of key-value pairs. The keys are strings, and the values can be one of the following types: + +- **Strings**: Text values. + - Empty strings remove the property from the JSON object. Learn more in the [Remove a Property from the JSON Property](#remove-a-property-from-the-json-property) section. +- **Numbers**: Numeric values. +- **Booleans**: `true` or `false` values. +- **Nested Objects**: Objects within objects. +- **Arrays**: Lists of values of any of the above types. + +For example, a `metadata` JSON property can look like this: + +```json +{ + "category": "electronics", + "views": 1500, + "is_featured": true, + "tags": ["new", "sale"], + "details": { + "warranty": "2 years", + "origin": "USA" + } +} +``` + +### What are JSON Properties Useful For? + +JSON properties allow you to store flexible and dynamic data structures that can evolve over time without requiring changes to the database schema. + +Most data models in Medusa's Commerce Modules have a `metadata` property that is a JSON object. `metadata` allows you to store custom information that is not part of the core data model. + +Some examples of data to store in JSON properties: + +- Custom gift message for line items in an order. +- Product's ID in a third-party system. +- Brand's category or tags. + +### What are JSON Properties Not Useful For? + +JSON properties are not suitable for structured data that requires strict validation or relationships with other data models. + +For example, if you want to re-use brands across different products, it's better to create a `Brand` data model and [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the `Product` data model instead of storing brand information in the product's `metadata` JSON property. + +*** + +## How to Manage JSON Properties? + +### How Medusa Updates JSON Properties + +Consider a Brand Module with a `Brand` data model as shown [in the previous section](#what-is-a-json-property). The [module's service](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md) will extend `MedusaService`, which generates methods like [updateBrands](https://docs.medusajs.com/resources/service-factory-reference/methods/update/index.html.md) to update a brand. + +When you pass a JSON property in the `updateBrands` method, Medusa will merge the provided JSON object with the existing one in the database. So, only the properties you pass will be updated, and the rest will remain unchanged. + +The following sections show examples of how to add, update, and remove properties in a JSON property. + +The following examples assume you have a `brandModuleService` that is resolved from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). For example: + +```ts +const brandModuleService = container.resolve(BRAND_MODULE) +``` + +### Add a Property to the JSON Property + +Continuing with the Brand example, to add a `category` property to the `metadata` property, pass the new property in the `update` or `create` methods: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + category: "electronics", + }, +}) +``` + +The brand record will now have the `metadata` property updated to include the new `category` property: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics" + } +} +``` + +If you want to add another `is_featured` property later, you can do so by passing it in the update method again without affecting the existing properties: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + is_featured: true, + }, +}) +``` + +The brand record will now have the `metadata` property updated to include both `category` and `is_featured` properties: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "is_featured": true + } +} +``` + +### Update an Existing Property in the JSON Property + +To update an existing property in the JSON property, pass the updated value in the `update` method. + +Continuing with the Brand example, to update the `category` property in the `metadata`: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + category: "home appliances", + }, +}) +``` + +The brand record will now have the `metadata` property updated to reflect the new `category` value, and existing properties will remain unchanged: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "home appliances", + "is_featured": true + } +} +``` + +### Caveat: Updating Nested Objects + +If you want to update a nested object within the JSON property, you need to provide the entire nested object in the update method. + +For example, consider that you have a `details` object within the `metadata`: + +```json +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "details": { + "warranty": "1 year", + "origin": "China" + } + } +} +``` + +To update the `warranty` property within the `details` object, you need to provide the entire `details` object in the update method: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + details: { + warranty: "2 years", + origin: "China", + }, + }, +}) +``` + +The brand record will now have the `metadata` property updated with the new `warranty` value: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "electronics", + "details": { + "warranty": "2 years", + "origin": "China" + } + } +} +``` + +### Remove a Property from the JSON Property + +To remove a property from the JSON property, you can pass an empty string as the value of the property in the update method. + +For example, to remove the `is_featured` property from the `metadata`: + +```ts +const brand = await brandModuleService.updateBrands({ + id: "brand_123", + metadata: { + is_featured: "", + }, +}) +``` + +The brand record will now have the `metadata` property updated to remove the `is_featured` property: + +```json title="Result" +{ + "id": "brand_123", + "metadata": { + "category": "home appliances" + } +} +``` + +### Manage JSON Properties in API Routes + +The instructions above also apply when managing JSON properties in API routes, since they typically use a service's `update` method under the hood. + +Refer to the [API reference](https://docs.medusajs.com/api/store#manage-metadata) to learn more about managing JSON properties, such as `metadata`, in API routes. + + # Manage Relationships 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. @@ -8765,6 +10256,28 @@ const Post = model.define("post", { export default Post ``` +#### Limit Text Length + +To limit the allowed length of a `text` property, use the [checks method](https://docs.medusajs.com/learn/fundamentals/data-models/check-constraints/index.html.md). + +For example, to limit the `name` property to a maximum of 50 characters: + +```ts highlights={textLengthHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + name: model.text(), + // ... +}) +.checks([ + (columns) => `${columns.name.length} <= 50`, +]) + +export default Post +``` + +This will add a database check constraint that ensures the `name` property of a record does not exceed 50 characters. If a record with a longer `name` is attempted to be inserted, an error will be thrown. + ### number The `number` method defines a number property. @@ -8877,7 +10390,7 @@ export default Post ### json -The `json` method defines a property whose value is a stringified JSON object. +The `json` method defines a property whose value is stored as a stringified JSON object in the database. For example: @@ -8892,6 +10405,8 @@ const Post = model.define("post", { export default Post ``` +Learn more in the [JSON Properties](https://docs.medusajs.com/learn/fundamentals/data-models/json-properties/index.html.md) chapter. + ### array The `array` method defines an array of strings property. @@ -9097,7 +10612,9 @@ import { model } from "@medusajs/framework/utils" const User = model.define("user", { id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), }) const Email = model.define("email", { @@ -9112,15 +10629,61 @@ In the example above, a user has one email, and an email belongs to one user. The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. +Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. ### Optional Relationship To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). +### One-to-One Relationship in the Database + +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + ### One-sided One-to-One Relationship -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one. + +You can do this either from the `hasOne` or the `belongsTo` side. + +#### hasOne Side + +By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table. + +If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method: + +- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model. +- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side. + +For example: + +```ts highlights={oneToOneForeignKeyHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + foreignKey: true, + mappedBy: undefined, + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), +}) +``` + +In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model. + +The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model. + +#### belongsTo Side + +To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter. For example: @@ -9139,14 +10702,9 @@ const Email = model.define("email", { }) ``` -### One-to-One Relationship in the Database - -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. +In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model. -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +The `User` data model does not have a relationship property pointing to the `Email` data model. *** @@ -9166,7 +10724,9 @@ import { model } from "@medusajs/framework/utils" const Store = model.define("store", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), }) const Product = model.define("product", { @@ -9294,38 +10854,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties: *** -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - -*** - ## Cascades When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. @@ -9395,7 +10923,7 @@ You can also write migrations manually. To do that, create a file in the `migrat For example: -```ts title="src/modules/blog/migrations/Migration202507021059.ts" +```ts title="src/modules/blog/migrations/Migration202507021059_create_author.ts" import { Migration } from "@mikro-orm/migrations" export class Migration202507021059 extends Migration { @@ -9417,6 +10945,12 @@ In the example above, the `up` method creates the table `author`, and the `down` Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. +### Migration File Naming + +Migrations are executed in the ascending order of their file names. So, it's recommended to prefix the migration file name with the timestamp of when the migration was created. This ensures that migrations are executed in the order they were created. + +For example, if you create a migration on July 2, 2025, at 10:59 AM, the file name should be `Migration202507021059_create_brand.ts`. This way, the migration will be executed after any previous migrations that were created before this date and time. + *** ## Run the Migration @@ -9457,6 +10991,64 @@ So, always rollback the migration before deleting it. 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). +*** + +## Data Migration Scripts + +In some use cases, you may need to perform data migration after updates to the database. For example, after you added a `Site` data model to the Blog Module, you want to assign all existing posts and authors to a default site. Another example is updating data stored in a third-party system. + +In those scenarios, you can instead create a data migration script. They are asynchronous function that the Medusa application executes once when you run the `npx medusa db:migrate command`. + +### How to Create a Data Migration Script + +You can create data migration scripts in a TypeScript or JavaScript file under the `src/migration-scripts` directory. The file must export an asynchronous function that will be executed when the `db:migrate` command is executed. + +For example, to create a data migration script for the Blog Module example, create the file `src/migration-scripts/migrate-blog-data.ts` with the following content: + +```ts title="src/migration-scripts/migrate-blog-data.ts" highlights={dataMigrationHighlights} +import { MedusaModule } from "@medusajs/framework/modules-sdk" +import { ExecArgs } from "@medusajs/framework/types" +import { BLOG_MODULE } from "../modules/blog" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export default async function migrateBlogData({ container }: ExecArgs) { + // Check that the blog module exists + if (!MedusaModule.isInstalled(BLOG_MODULE)) { + return + } + + await migrateBlogDataWorkflow(container).run({}) +} + +const migrateBlogDataWorkflow = createWorkflow( + "migrate-blog-data", + () => { + // Assuming you have these steps + createDefaultSiteStep() + + assignBlogDataToSiteStep() + } +) +``` + +In the above example, you default export an asynchronous function that receives an object parameter with the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) property. + +In the function, you first ensure that the Blog Module is installed to avoid errors otherwise. Then, you run a workflow that you've created in the same file that performs the necessary data migration. + +#### Test Data Migration Script + +To test out the data migration script, run the migration command: + +```bash +npx medusa db:migrate +``` + +Medusa will run any pending migrations and migration scripts, including your script. + +If the script runs successfully, Medusa won't run the script again. + +If there are errors in the script, you'll receive an error in the migration script logs. Medusa will keep running the script every time you run the migration command until it runs successfully. + # Environment Variables @@ -10646,6 +12238,85 @@ To learn more about the different concepts useful for building plugins, check ou - [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) +# Automatically Generated Types + +In this chapter, you'll learn about the types Medusa automatically generates under the `.medusa` directory and how you should use them. + +## What are Automatically Generated Types? + +Medusa automatically generates TypeScript types for: + +1. Data models collected in the [Query's graph](https://docs.medusajs.com/learn/fundamentals/module-links/query#querying-the-graph/index.html.md). These types provide you with auto-completion and type checking when using Query. + - Generated data model types are located in the `.medusa/types/query-entry-points.d.ts` file. +2. Modules registered in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). These types provide you with auto-completion and type checking when resolving modules from the container. + - Generated module registration names are located in the `.medusa/types/modules-bindings.d.ts` file. + +![Diagram showcasing the directory structure of the .medusa directory, highlighting the types folder and its contents.](https://res.cloudinary.com/dza7lstvk/image/upload/v1753448927/Medusa%20Book/generated-types-dir_bmvdts.jpg) + +*** + +## How to Trigger Type Generation? + +The Medusa application generates these types automatically when you run the application with the `dev` command: + +```bash npm2yarn +npm run dev +``` + +So, if you add a new data model or module and you don't find it in auto-completion or type checking, you can run the `dev` command to regenerate the types. + +*** + +## How to Use the Generated Types? + +The generated types are only meant for auto-completion and type checking. + +For example, consider you have a Brand Module with a `Brand` data model. Due to the auto-generated types, you can do the following: + +### Query + +```ts +const { data: [brand] } = await query.graph({ + entity: "brand", + fields: ["*"], + filters: { + id: "brand_123", + }, +}) + +// brands has the correct type, so you can access its properties with auto-completion and type checking +console.log(brand.name) +``` + +### Container + +```ts +const brandModuleService = req.scope.resolve("brand") + +// brandModuleService has the correct type, so you can access its methods with auto-completion and type checking +const brand = await brandModuleService.retrieveBrand("brand_123") +``` + +### Don't Import the Generated Types + +The generated types are only meant to help you in your development process by providing auto-completion and type checking. You should not import them directly in your code. + +Since you don't commit the `.medusa` directory to your version control system or your production environment, importing these types may lead to build errors. + +Instead, if you need to use a data model's type in your customizations, you can use the [InferTypeOf](https://docs.medusajs.com/learn/fundamentals/data-models/infer-type/index.html.md) utility function, which infers the type of a data model based on its properties. + +For example, if you want to use the `Brand` data model's type in your customizations, you can do the following: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" // relative path to the model + +export type BrandType = InferTypeOf +``` + +You can then use the `BrandType` type in your customizations, such as in workflow inputs or service method outputs. + + # Medusa Container In this chapter, you’ll learn about the Medusa container and how to use it. @@ -10795,6 +12466,16 @@ 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). +*** + +## Container Registration Keys Auto-Completion + +When you resolve a resource from the Medusa container, you can use auto-completion to find the registration key of the resource. + +This is possible due to Medusa's auto-generated types, which are generated in the `.medusa` directory when you run the `dev` command. + +Learn more in the [Auto-Generated Types](https://docs.medusajs.com/learn/fundamentals/generated-types/index.html.md) chapter. + # Add Columns to a Link Table @@ -12842,7 +14523,7 @@ export default defineLink( field: "id", }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { @@ -12975,7 +14656,7 @@ export default defineLink( field: "id", }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { @@ -13049,7 +14730,7 @@ export default defineLink( isList: true, }, { - linkable: BlogModule.linkable.post.id, + ...BlogModule.linkable.post.id, primaryKey: "product_id", }, { @@ -13297,9 +14978,9 @@ Your workflow can use services of both custom and Commerce Modules, supporting y # Module Container -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 about the module container and how to resolve resources from it. -Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container only used by the resources of that module. +Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container used only by the resources of that module. So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container. @@ -13309,13 +14990,13 @@ Find a list of resources or dependencies registered in a module's container in [ *** -## Resolve Resources +## Resolve Resources from the Module Container -### Services +### Resolve in Services -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. To resolve a resource, add the resource's registration name as a property of the object. -For example: +For example, to resolve the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the container: ```ts highlights={[["4"], ["10"]]} import { Logger } from "@medusajs/framework/types" @@ -13337,11 +15018,13 @@ export default class BlogModuleService { } ``` -### Loader +You can then use the logger in the service's methods. -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +### Resolve in Loaders -For example: +[Loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) accept an object parameter with the property `container`. Its value is the module's container that can be used to resolve resources using its `resolve` method. + +For example, to resolve the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) in a loader: ```ts highlights={[["9"]]} import { @@ -13360,6 +15043,72 @@ export default async function helloWorldLoader({ } ``` +You can then use the logger in the loader's code. + +*** + +## Caveat: Resolving Module Services in Loaders + +Consider a module that has a main service `BrandModuleService`, and an internal service `CmsService`. Medusa will register both of these services in the module's container. + +However, loaders are executed before any services are initialized and registered in the module's container. So, you can't resolve the `BrandModuleService` and `CmsService` in a loader. + +Instead, if your main service extends the `MedusaService` [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you can resolve the internal services generated for each data model passed to the `MedusaService` function. + +For example, if the `BrandModuleService` is defined as follows: + +```ts +import { MedusaService } from "@medusajs/framework/utils" +import Brand from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { +} + +export default BrandModuleService +``` + +Then, you can resolve the `brandService` that allows you to manage brands in the module's loader: + +```ts +import { + LoaderOptions, +} from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const brandService = container.resolve("brandService") + + const brands = await brandService.list() + + console.log("[helloWorldLoader]: Brands:", brands) +} +``` + +Refer to the [Service Factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for details on the available methods in the generated services. + +*** + +## Alternative to Resolving Other Modules' Services + +Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), you can't resolve resources that belong to other modules from the module's container. For example, you can't resolve the Product Module's service in the Blog Module's service. + +Instead, to build commerce features that span multiple modules, you can create [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). In those workflows, you can resolve services of all modules registered in the Medusa application, including the services of the Product and Blog modules. + +Then, you can execute the workflows in [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + +Learn more and find examples in the [Module Isolation](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) chapter. + +*** + +## Avoid Circular Dependencies + +When resolving resources in a module's services, make sure you don't create circular dependencies. For example, if `BlogModuleService` resolves `CmsService`, and `CmsService` resolves `BlogModuleService`, it will cause a circular dependency error. + +Instead, you should generally only resolve services within the main service. For example, `BlogModuleService` can resolve `CmsService`, but `CmsService` should not resolve `BlogModuleService`. + # Perform Database Operations in a Service @@ -14530,29 +16279,29 @@ You can now resolve the MongoDB Module's main service in your customizations to # Modules Directory Structure -In this document, you'll learn about the expected files and directories in your module. +In this chapter, 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). +The `index.ts` file is in the root of your module's directory and exports the module's definition as explained in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter. *** ## 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). +A module must have a main service that contains the module's business logic. It's created in the `service.ts` file at the root of your module directory as explained in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter. *** ## Other Directories -The following directories are optional and their content are explained more in the following chapters: +The following directories are optional and you can choose to create them based on your module's functionality: -- `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. +- `models`: Holds the [data models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) representing tables in the database. +- `migrations`: Holds the [migration](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) files used to reflect changes on the database. +- `loaders`: Holds the [script files to run on the application's startup](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) when Medusa loads the module. # Multiple Services in a Module @@ -14882,7 +16631,7 @@ A data model represents a table in the database. You create data models using Me You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) +![Blog module directory structure showing the organized file hierarchy with the newly created 'models' folder containing the 'post.ts' data model file, which defines the data model for blog posts using Medusa's data modeling language](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) ```ts title="src/modules/blog/models/post.ts" import { model } from "@medusajs/framework/utils" @@ -15194,9 +16943,9 @@ In this chapter, you’ll learn about what the service factory is and how to use Medusa provides a service factory that your module’s main service can extend. -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. +The service factory generates data management methods for your data models, saving you time on implementing these methods manually. -Your service provides data-management functionalities of your data models. +Your service provides data-management functionality for your data models. *** @@ -15221,7 +16970,7 @@ export default BlogModuleService ### MedusaService Parameters -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. +The `MedusaService` function accepts one parameter, which is an object of data models for which to generate data-management methods. In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. @@ -15229,7 +16978,7 @@ In the example above, since the `BlogModuleService` extends `MedusaService`, it The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. +The method names are the operation name, suffixed by the data model's key in the object parameter passed to `MedusaService`. For example, the following methods are generated for the service above: @@ -15341,7 +17090,7 @@ await blogModuleService.softDeletePosts({ ### Using a Constructor -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +If you implement a `constructor` in your service, make sure to call `super` and pass it `...arguments`. For example: @@ -15360,6 +17109,48 @@ class BlogModuleService extends MedusaService({ export default BlogModuleService ``` +*** + +## Generated Internal Services + +The service factory also generates internal services for each data model passed to the `MedusaService` function. These services are registered in the module's container and can be resolved using their camel-cased names. + +For example, if the `BlogModuleService` is defined as follows: + +```ts +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ +} + +export default BlogModuleService +``` + +Then, you'll have a `postService` registered in the module's container that allows you to manage posts. + +Generated internal services have the same methods as the `BlogModuleService`, such as `create`, `retrieve`, `update`, and `delete`, but without the data model name suffix. + +These services are useful when you need to perform database operations in loaders, as they are executed before the module's services are registered. Learn more in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) documentation. + +For example, you can create a loader that logs the number of posts in the database: + +```ts +import { LoaderOptions } from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const postService = container.resolve("postService") + + const [_, count] = await postService.listAndCount() + + console.log(`[helloWorldLoader]: There are ${count} posts in the database.`) +} +``` + # Create a Plugin @@ -15833,19 +17624,25 @@ For example, in a plugin, you can define a module that integrates a third-party The next chapter explains how you can create and publish a plugin. -# Scheduled Jobs Number of Executions +# Scheduled Job 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 +## Default Number of Scheduled Job Executions -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. +By default, a scheduled job is executed whenever it matches its specified pattern. For example, if you set a scheduled job to run every five minutes, it will run every five minutes until you stop the Medusa application. + +*** + +## Configure Number of Scheduled Job Executions + +To execute a scheduled job a specific number of times only, you can configure it with the `numberOfExecutions` option. Its value is the number of 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.") + console.log("I'll be executed only three times.") } export const config = { @@ -15858,9 +17655,13 @@ export const config = { 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. +So, Medusa will execute this job only 3 times, once every minute, and then it won't be executed anymore during the current runtime. -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +### Configuration is Per Application Runtime + +Medusa tracks the number of executions for a scheduled job during its current runtime. Once the application stops, the next time you start it, the counter will be reset to `0`. + +So, if you restart the Medusa application, the scheduled job will be executed again until it reaches the number of executions specified. # Scheduled Jobs @@ -15871,12 +17672,12 @@ In this chapter, you’ll learn about scheduled jobs and how to use them. 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. +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 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 the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, create a [custom CLI script](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) and execute it using 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) or [custom CLI scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts#run-custom-script-on-application-startup/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. *** @@ -15911,7 +17712,7 @@ 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!`. +This scheduled job executes every minute and logs into the terminal the message `Greeting!`. ### Test the Scheduled Job @@ -15931,11 +17732,11 @@ 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. +In a realistic scenario like syncing products to an ERP once a day, you should create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and execute it in a scheduled job. -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. +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: +You can learn how to create a workflow in the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapter, 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" @@ -15961,11 +17762,17 @@ The next time you start the Medusa application, it will run this job every day a In this chapter, you'll learn how to expose a hook in your workflow. -## When to Expose a Hook +Refer to the [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) chapter to learn what a workflow hook is and how to consume Medusa's workflow hooks. + +## When to Expose a Hook? + +Medusa exposes hooks in many of its workflows to allow you to inject custom functionality. -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. +You can also expose your own hooks in your workflows to allow other developers to consume them. This is useful when you're creating a workflow in a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) and you want plugin users to extend the workflow's functionality. -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. +For example, you are creating a blog plugin and you want to allow developers to perform custom validation before a blog post is created. You can expose a hook in your workflow that developers can consume to perform their custom validation. + +If your workflow is not in a plugin, you probably don't need to expose a hook as you can perform the necessary actions directly in the workflow. *** @@ -15975,26 +17782,26 @@ To expose a hook in your workflow, use `createHook` from the Workflows SDK. For example: -```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} +```ts title="src/workflows/create-blog-post/index.ts" highlights={hookHighlights} import { createStep, createHook, createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { createProductStep } from "./steps/create-product" +import { createPostStep } from "./steps/create-post" -export const myWorkflow = createWorkflow( - "my-workflow", +export const createBlogPostWorkflow = createWorkflow( + "create-blog-post", function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } + const validate = createHook( + "validate", + { post: input } ) + const post = createPostStep(input) - return new WorkflowResponse(product, { - hooks: [productCreatedHook], + return new WorkflowResponse(post, { + hooks: [validate], }) } ) @@ -16002,29 +17809,61 @@ export const myWorkflow = createWorkflow( The `createHook` function accepts two parameters: -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. +1. The first is a string indicating the hook's name. Developers consuming the hook will use this name to access the hook. +2. The second is the input to pass to the hook handler. Developers consuming the hook will receive this input in the hook handler. -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. +You must also return the hook in the workflow's response by passing a `hooks` property to the `WorkflowResponse`'s second parameter object. Its value is an array of the workflow's hooks. ### How to Consume the Hook? -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +To consume the hook of the workflow, create the file `src/workflows/hooks/create-blog-post.ts` with the following content: -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +```ts title="src/workflows/hooks/create-blog-post.ts" highlights={handlerHighlights} +import { MedusaError } from "@medusajs/framework/utils" +import { createBlogPostWorkflow } from "../create-blog-post" -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { +createBlogPostWorkflow.hooks.validate( + async ({ post }, { container }) => { // TODO perform an action + if (!post.additional_data.custom_title) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Custom title is required" + ) + } } ) ``` -The hook is available on the workflow's `hooks` property using its name `productCreated`. +The hook is available on the workflow's `hooks` property using its name `validate`. You invoke the hook, passing a step function (the hook handler) as a parameter. +The hook handler is essentially a step function. You can perform in it any actions you perform in a step. For example, you can throw an error, which would stop the workflow execution. + +You can also access the Medusa container in the hook handler to perform actions like using Query or module services. + +For example: + +```ts title="src/workflows/hooks/create-blog-post.ts" +import { createBlogPostWorkflow } from "../create-blog-post" + +createBlogPostWorkflow.hooks.validate( + async ({ post }, { container }) => { + const query = container.resolve("query") + + const { data: existingPosts } = await query.graph({ + entity: "post", + fields: ["*"], + }) + + // TODO do something with existing posts... + } +) +``` + +Learn more about the hook handler in the [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) chapter. + # Compensation Function @@ -16746,11 +18585,9 @@ Instead, refer to the [Error Handling](https://docs.medusajs.com/learn/fundament *** -## Step Constraints - -### Returned Values +## Returned Value Constraints -A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. +Data returned from workflows and steps are serialized, allowing Medusa to store them in the database. So, you must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object, from workflows and steps. Values of other types, such as Maps, aren't allowed. @@ -16794,6 +18631,69 @@ const step1 = createStep( ) ``` +### Buffer Example + +In some cases, you may need to return a buffer. For example, when your workflow generates a file and you want to return it as a buffer. + +In those cases, you can return an object containing the buffer as a property. Then, in customizations that execute the workflow, you can recreate the buffer from the serialized data. + +For example, consider the following workflow that returns a buffer: + +```ts +import { + createWorkflow, + createStep, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + (_, { container }) => { + const buffer = Buffer.from("Hello, World!") + + return new StepResponse({ + buffer, + }) + } +) + +const myWorkflow = createWorkflow( + "hello-world", + function () { + const step1Response = step1() + + return new WorkflowResponse({ + buffer: step1Response.buffer, + }) + } +) +``` + +Then, in an API route that executes this workflow, you can recreate the buffer from the serialized data using `Buffer.from`: + +```ts +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() + + const buffer = Buffer.from(result.buffer) + + res.setHeader("Content-Type", "application/octet-stream") + res.setHeader("Content-Disposition", "attachment; filename=hello.txt") + res.send(buffer) +} +``` + # Error Handling in Workflows @@ -17040,15 +18940,17 @@ In this example, if the `actionThatThrowsError` step throws an error, the `moreA You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section. -# Execute Another Workflow +# Execute Nested Workflows -In this chapter, you'll learn how to execute a workflow in another. +In this chapter, you'll learn how to execute a workflow in another workflow. -## Execute in a Workflow +## How to Execute a Workflow in Another? -To execute a workflow in another, use the `runAsStep` method that every workflow has. +In many cases, you may have a workflow that you want to re-use in another workflow. This is most common when you build custom workflows and you want to utilize Medusa's [existing workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md). -For example: +Executing a workflow within another is slightly different from how you usually execute a workflow. Instead of invoking the workflow, passing it the container, then running its `run` method, you use the `runAsStep` method of the workflow. This will pass the Medusa container and workflow context to the nested workflow. + +For example, to execute the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) in your custom workflow: ```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" import { @@ -17074,17 +18976,21 @@ const workflow = createWorkflow( ) ``` -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +The `runAsStep` method accepts an `input` property to pass input to the workflow. -The object has an `input` property to pass input to the workflow. +### Returned Data + +Notice that you don't need to use `await` when executing the nested workflow, as it's not a promise in this scenario. + +You also receive the workflow's output as a return value from the `runAsStep` method. This is different from the usual workflow response, where you receive the output in a `result` property. *** -## Preparing Input Data +## Prepare Input Data -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. +Since Medusa creates an internal representation of your workflow's constructor function, you can't manipulate data directly in the workflow constructor. You can learn more about this in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter. -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +If you need to perform some data manipulation to prepare the nested workflow's input data, use `transform` from the Workflows SDK. For example: @@ -17123,15 +19029,15 @@ const workflow = createWorkflow( ) ``` -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`. +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as input to the `createProductsWorkflow`. + +Learn more about `transform` in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter. *** ## 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). +Similar to the [previous section](#prepare-input-data), you can't use conditional statements directly in the workflow constructor. Instead, you can use the `when-then` function from the Workflows SDK to run a workflow conditionally. For example: @@ -17167,7 +19073,29 @@ const workflow = createWorkflow( ) ``` -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. +In this example, you use `when-then` to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + +Learn more about `when-then` in the [When-Then Conditions](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) chapter. + +*** + +## Errors in Nested Workflows + +A nested workflow behaves similarly to a step in a workflow. So, if the nested workflow fails, it will throw an error that stops the parent workflow's execution and compensates previous steps. + +In addition, if another step fails after the nested workflow, the nested workflow's steps will be compensated as part of the compensation process. + +Learn more about handling errors in workflows in the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter. + +*** + +## Nested Long-Running Workflows + +When you execute a long-running workflow within another workflow, the parent workflow becomes a long-running workflow as well. + +So, the parent workflow will wait for the nested workflow to finish before continuing its execution. + +Refer to the [Long-Running Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) chapter for more information on how to handle long-running workflows. # Long-Running Workflows @@ -17248,6 +19176,7 @@ A workflow is also considered long-running if: - One of its steps has its `async` configuration set to `true` and doesn't return a step response. - One of its steps has its `retryInterval` option set as explained in the [Retry Failed Steps chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). +- One of its [nested workflows](https://docs.medusajs.com/learn/fundamentals/workflows/execute-another-workflow/index.html.md) is a long-running workflow. *** @@ -17308,9 +19237,6 @@ export const setStepSuccessStep = createStep( workflowId: "hello-world", }, stepResponse: new StepResponse("Done!"), - options: { - container, - }, }) } ) @@ -17330,9 +19256,6 @@ The `setStepSuccess` method of the workflow engine's main service accepts as a p - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. - stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status. -- options: (\`Record\\`) Options to pass to the step. - - - container: (\`MedusaContainer\`) An instance of the Medusa Container ### Change Step Status to Failed @@ -17374,9 +19297,6 @@ export const setStepFailureStep = createStep( workflowId: "hello-world", }, stepResponse: new StepResponse("Failed!"), - options: { - container, - }, }) } ) @@ -17465,7 +19385,7 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions. -# Multiple Step Usage in Workflow +# Multiple Step Usage in Workflows In this chapter, you'll learn how to use a step multiple times in a workflow. @@ -17475,7 +19395,7 @@ In some cases, you may need to use a step multiple times in the same workflow. The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. -Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: +Steps must have a unique ID, which you pass as the first parameter of the `createStep` function. ```ts const useQueryGraphStep = createStep( @@ -17484,7 +19404,7 @@ const useQueryGraphStep = createStep( ) ``` -This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: +So, when a step is used multiple times in a workflow, it's registered multiple times in the workflow with the same ID, which causes an error. ```ts const helloWorkflow = createWorkflow( @@ -17514,7 +19434,7 @@ When you execute a step in a workflow, you can chain a `config` method to it to Use the `config` method to change a step's ID for a single execution. -So, this is the correct way to write the example above: +For example, this is the correct way to write the example above: ```ts highlights={highlights} const helloWorkflow = createWorkflow( @@ -17534,7 +19454,7 @@ const helloWorkflow = createWorkflow( ) ``` -The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. +The `config` method accepts an object with a `name` property. Its value is the new ID for the step to use for this execution only. The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. @@ -18529,6 +20449,404 @@ 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/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. +# Install Medusa with Docker + +In this chapter, you'll learn how to install and run a Medusa application using Docker. + +The main supported installation method is using [create-medusa-app](https://docs.medusajs.com/learn/installation/index.html.md). However, since it requires prerequisites like PostgreSQL, Docker may be a preferred and easier option for some users. You can follow this guide to set up a Medusa application with Docker in this case. + +You can follow this guide on any operating system that supports Docker, including Windows, macOS, and Linux. + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) +- [Docker Compose](https://docs.docker.com/compose/install/) +- [Git CLI tool](https://git-scm.com/downloads) + +## 1. Clone Medusa Starter Repository + +To get started, clone the Medusa Starter repository that a Medusa application is based on: + +```bash +git clone https://github.com/medusajs/medusa-starter-default.git --depth=1 my-medusa-store +``` + +This command clones the repository into a directory named `my-medusa-store`. + +*** + +## 2. Create `docker-compose.yml` + +In the cloned repository, create a file named `docker-compose.yml` with the following content: + +```yaml title="docker-compose.yml" +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: medusa_postgres + restart: unless-stopped + environment: + POSTGRES_DB: medusa-store + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - medusa_network + + # Redis + redis: + image: redis:7-alpine + container_name: medusa_redis + restart: unless-stopped + ports: + - "6379:6379" + networks: + - medusa_network + + # Medusa Server + # This service runs the Medusa backend application + # and the admin dashboard. + medusa: + build: . + container_name: medusa_backend + restart: unless-stopped + depends_on: + - postgres + - redis + ports: + - "9000:9000" + environment: + - NODE_ENV=development + - DATABASE_URL=postgres://postgres:postgres@postgres:5432/medusa-store + - REDIS_URL=redis://redis:6379 + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + networks: + - medusa_network + +volumes: + postgres_data: + +networks: + medusa_network: + driver: bridge +``` + +You define three services in this file: + +- `postgres`: The PostgreSQL database service that stores your Medusa application's data. +- `redis`: The Redis service that stores session data. +- `medusa`: The Medusa service that runs the server and the admin dashboard. It connects to the PostgreSQL and Redis services. + +You can add environment variables either in the `environment` section of the `medusa` service or in a separate `.env` file. + +### Recommendations for Multiple Local Projects + +If this isn't the first Medusa project you're setting up with Docker on your machine, make sure to: + +- Change the `container_name` for each service to avoid conflicts. + - For example, use `medusa_postgres_myproject`, `medusa_redis_myproject`, and `medusa_backend_myproject` instead of the current names. +- Change the volume names to avoid conflicts. + - For example, use `postgres_data_myproject` instead of `postgres_data`. +- Change the network name to avoid conflicts. + - For example, use `medusa_network_myproject` instead of `medusa_network`. +- Change the ports to avoid conflicts with other projects. For example: + - Use `"5433:5432"` for PostgreSQL. + - Use `"6380:6379"` for Redis. + - Use `"9001:9000"` for the Medusa server. +- Update the `DATABASE_URL` and `REDIS_URL` environment variables accordingly. + +*** + +## 3. Create `start.sh` + +Next, you need to create a script file that [runs database migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) and starts the Medusa development server. + +Create the file `start.sh` with the following content: + +### Yarn + +```shell title="start.sh" +#!/bin/sh + +# Run migrations and start server +echo "Running database migrations..." +npx medusa db:migrate + +echo "Seeding database..." +yarn seed || echo "Seeding failed, continuing..." + +echo "Starting Medusa development server..." +yarn dev +``` + +### NPM + +```shell title="start.sh" +#!/bin/sh + +# Run migrations and start server +echo "Running database migrations..." +npx medusa db:migrate + +echo "Seeding database..." +npm run seed || echo "Seeding failed, continuing..." + +echo "Starting Medusa development server..." +npm run dev +``` + +Make sure to give the script executable permissions: + +Setting permissions isn't necessary on Windows, but it's recommended to run this command on Unix-based systems like macOS and Linux. + +```bash +chmod +x start.sh +``` + +*** + +## 4. Create `Dockerfile` + +The `Dockerfile` defines how the Medusa service is built. + +Create a file named `Dockerfile` with the following content: + +### Yarn + +```dockerfile title="Dockerfile" +# Development Dockerfile for Medusa +FROM node:20-alpine + +# Set working directory +WORKDIR /server + +# Copy package files and yarn config +COPY package.json yarn.lock .yarnrc.yml ./ + +# Install all dependencies using yarn +RUN yarn install + +# Copy source code +COPY . . + +# Expose the port Medusa runs on +EXPOSE 9000 + +# Start with migrations and then the development server +CMD ["./start.sh"] +``` + +### NPM + +```dockerfile title="Dockerfile" +# Development Dockerfile for Medusa +FROM node:20-alpine + +# Set working directory +WORKDIR /server + +# Copy package files and npm config +COPY package.json package-lock.json ./ + +# Install all dependencies using npm +RUN npm install + +# Copy source code +COPY . . + +# Expose the port Medusa runs on +EXPOSE 9000 + +# Start with migrations and then the development server +CMD ["./start.sh"] +``` + +In the `Dockerfile`, you use the `node:20-alpine` image as the base since Medusa requires Node.js v20 or later. + +Then, you set the working directory to `/server`, copy the necessary files, install dependencies, expose the `9000` port that Medusa uses, and run the `start.sh` script to start the server. + +While it's more common to use `/app` as the working directory, it's highly recommended to use `/server` for the Medusa service to avoid conflicts with Medusa Admin customizations. Learn more in [this troubleshooting guide](https://docs.medusajs.com/resources/troubleshooting/medusa-admin/no-widget-route#errors-in-docker/index.html.md). + +*** + +## 5. Install Dependencies + +The Medusa Starter repository has a `yarn.lock` file that was generated by installing dependencies with Yarn v1.22.19. + +If you're using a different Yarn version, or you're using NPM, you need to install the dependencies again to ensure compatibility with the Docker setup. + +To install the dependencies, run the following command: + +```bash npm2yarn +npm install +``` + +This will update `yarn.lock` or generate a `package-lock.json` file, depending on your package manager. + +*** + +## 6. Update Scripts in `package.json` + +Next, you need to update the `scripts` section in your `package.json` file to start the development server using Docker. + +Add the following scripts in `package.json`: + +```json title="package.json" +{ + "scripts": { + // Other scripts... + "docker:up": "docker compose up --build -d", + "docker:down": "docker compose down" + } +} +``` + +Where: + +- `docker:up` starts the development server in a Docker container as a background process. +- `docker:down` stops and removes the Docker containers. + +*** + +## 7. Update Medusa Configuration + +If you try to run the Medusa application now with Docker, you'll encounter an SSL error as the server tries to connect to the PostgreSQL database. + +To resolve the error, add the following configurations in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { loadEnv, defineConfig } from "@medusajs/framework/utils" + +loadEnv(process.env.NODE_ENV || "development", process.cwd()) + +module.exports = defineConfig({ + projectConfig: { + // ... + databaseDriverOptions: { + ssl: false, + sslmode: "disable", + }, + }, +}) +``` + +You add the [projectConfig.databaseDriverOptions](https://docs.medusajs.com/learn/configurations/medusa-config#databasedriveroptions/index.html.md) to disable SSL for the PostgreSQL database connection. + +*** + +## 8. Add `.dockerignore` + +To ensure only the necessary files are copied into the Docker image, create a `.dockerignore` file with the following content: + +```dockerignore title=".dockerignore" +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git +.gitignore +README.md +.env.test +.nyc_output +coverage +.DS_Store +*.log +dist +build +``` + +*** + +## 9. Create `.env` File + +You can add environment variables either in the `environment` section of the `medusa` service in `docker-compose.yml` or in a separate `.env` file. + +If you don't want to use a `.env` file, you can remove the `env_file` section from the `medusa` service in `docker-compose.yml`. + +Otherwise, copy the `.env.template` file to `.env` and update the values as needed. + +*** + +## 10. Start the Medusa Application with Docker + +All configurations are now ready. You can start the Medusa application using Docker by running the following command: + +```bash npm2yarn +npm run docker:up +``` + +Docker will pull the necessary images, start the PostgreSQL and Redis services, build the Medusa service, and run the development server in a Docker container. + +You can check the logs to ensure everything is running smoothly with the following command: + +```bash +docker compose logs -f +``` + +Once you see the following message, the Medusa server and admin are ready: + +```shell +✔ Server is ready on port: 9000 – 3ms +info: Admin URL → http://localhost:9000/app +``` + +You can now access the Medusa server at `http://localhost:9000` and the Medusa Admin dashboard at `http://localhost:9000/app`. + +*** + +## Create Admin User + +To create an admin user, run the following command: + +```bash +docker compose run --rm medusa npx medusa user -e admin@example.com -p supersecret +``` + +Make sure to replace `admin@example.com` and `supersecret` with your desired email and password. + +You can now log in to the Medusa Admin dashboard at `http://localhost:9000/app` using the email and password you just created. + +*** + +## Stop the Medusa Application Running in Docker + +To stop the Medusa application running in Docker, run the following command: + +```bash npm2yarn +npm run docker:down +``` + +This command stops and removes the Docker containers created by the `docker-compose.yml` file. + +This doesn't delete any data in your application or its database. You can start the server again using the `docker:up` command. + +*** + +## Check Logs + +You can check the logs of the Medusa application running in Docker using the following command: + +```bash +docker compose logs -f medusa +``` + +This command shows the logs of the `medusa` service, allowing you to see any errors or messages from the Medusa application. + +*** + +## Learn More about your Medusa Application + +You can learn more about your Medusa application and its setup in the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). + + # Install Medusa In this chapter, you'll learn how to install and run a Medusa application. @@ -18537,6 +20855,8 @@ In this chapter, you'll learn how to install and run a Medusa application. A Medusa application is made up of a Node.js server and an admin dashboard. You can optionally install the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) separately either while installing the Medusa application or at a later point. +While this is the recommended way to create a Medusa application, you can alternatively [install a Medusa application with Docker](https://docs.medusajs.com/learn/installation/docker/index.html.md). + ### Prerequisites - [Node.js v20+](https://nodejs.org/en/download) @@ -18551,11 +20871,13 @@ npx create-medusa-app@latest my-medusa-store Where `my-medusa-store` is the name of the project's directory and PostgreSQL database created for the project. When you run the command, you'll be asked whether you want to install the Next.js Starter Storefront. +To customize the default installation behavior, such as specify a database URL, refer to the [create-medusa-app reference](https://docs.medusajs.com/resources/create-medusa-app/index.html.md). + After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to. If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`. -![Diagram showcasing an overview of the installation directories](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg) +![Directory structure overview after Medusa installation showing the main project folder containing the Medusa backend application and admin dashboard, alongside the separate storefront directory for the customer-facing Next.js application](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg) ### Successful Installation Result @@ -18567,7 +20889,7 @@ If you also installed the Next.js Starter Storefront, it'll be running at `http: You can stop the servers for the Medusa application and Next.js Starter Storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development). -![Diagram showcasing the server and applications running after successful installation](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg) +![Post-installation running services overview: Medusa backend server and admin dashboard running on localhost:9000, Next.js Starter Storefront running on localhost:8000, with PostgreSQL database and other essential services active and ready for development](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg) ### Troubleshooting Installation Errors @@ -18682,7 +21004,7 @@ In a common Medusa application, requests go through four layers in the stack. In These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) +![Medusa application architecture diagram illustrating the HTTP layer flow: External clients (storefront and admin) send requests to API routes, which execute workflows containing business logic, which then interact with modules to perform data operations on PostgreSQL databases](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) *** @@ -18692,7 +21014,7 @@ The Medusa application injects into each module, including your [custom modules] Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) +![Database layer architecture diagram showing how Medusa modules establish connections to PostgreSQL databases through injected database connections, enabling data persistence and retrieval operations](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) *** @@ -18734,7 +21056,7 @@ All of the third-party services mentioned above can be replaced to help you buil 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) +![Complete Medusa architecture overview showing the full technology stack: client applications (storefront and admin) connecting through HTTP layer to workflows, which coordinate with commerce and Infrastructure Modules to manage data operations, third-party integrations, and database persistence](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) # Build with AI Assistants and LLMs @@ -18785,6 +21107,32 @@ You can add new lines using the `Shift + Enter` shortcut. *** +## MCP Remote Server + +The Medusa documentation provides a remote Model Context Protocol (MCP) server that allows you to find information from the Medusa documentation right in your IDEs or AI tools, such as Cursor. + +Medusa hosts a Streamable HTTP MCP server available at `https://docs.medusajs.com/mcp`. you can add it to AI agents that support connecting to MCP servers. + +### Cursor + +Click here to add the Medusa MCP server to Cursor. + +To manually connect to the Medusa MCP server in Cursor, add the following to your `.cursor/mcp.json` file or Cursor settings, as explained in the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol): + +```json +{ + "mcpServers": { + "medusa": { + "url": "https://docs.medusajs.com/mcp" + } + } +} +``` + +### VSCode + +*** + ## Plain Text Documentation The Medusa documentation is available in plain text format, which allows LLMs and AI tools to easily parse and understand the content. @@ -18798,2406 +21146,2295 @@ You can access the following plain text documentation files: You can provide these files to your AI tools or LLM editors like [Cursor](https://docs.cursor.com/context/@-symbols/@-docs). This will help them understand the Medusa documentation and provide better assistance when building customizations or answering questions. -# Introduction - -Medusa is a digital commerce platform with a built-in Framework for customization. - -Medusa ships with three main tools: - -1. A suite of [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) with core commerce functionalities, such as tracking inventory, calculating cart totals, accepting payments, managing orders, and much more. -2. A [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) for building custom functionalities specific to your business, product, or industry. This includes tools for introducing custom API endpoints, business logic, and data models; building workflows and automations; and integrating with third-party services. -3. A customizable admin dashboard for merchants to configure and operate their store. +# From Medusa v1 to v2: Conceptual Differences -When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that brings your business's vision to life. +In this chapter, you'll learn about the differences and changes between concepts in Medusa v1 to v2. -*** +## What to Expect in This Chapter -## Who should use Medusa +This chapter is designed to help developers migrate from Medusa v1 to v2 by understanding the conceptual differences between the two versions. -Medusa is for businesses and teams looking for a digital commerce platform with the tools to implement unique requirements that other platforms aren't built to support. +This chapter will cover: -Businesses of all sizes can use Medusa, from small start ups to large enterprises. Also, technical teams of all sizes can build with Medusa; all it takes is a developer to manage and deploy Medusa projects. +- The general steps to update your project from Medusa v1 to v2. +- The changes in tools and plugins between Medusa v1 and v2. +- The high-level changes in the concepts and commerce features between Medusa v1 and v2. -Below are some stories from companies that use Medusa: +By following this chapter, you'll learn about the general changes you need to make in your project, with links to read more about each topic. Only topics documented in the v1 documentation are covered. -- [Use Case: D2C](https://medusajs.com/blog/matt-sleeps/): How Matt Sleeps built a unique D2C experience with Medusa -- [Use Case: OMS](https://medusajs.com/blog/makro-pro/): How Makro Pro Built an OMS with Medusa -- [Use Case: Marketplace](https://medusajs.com/blog/foraged/): How Foraged built a custom marketplace with Medusa -- [Use Case: POS](https://medusajs.com/blog/tekla-pos/): How Tekla built a global webshop and a POS system with Medusa -- [Use Case: B2B](https://medusajs.com/blog/visionary/): How Visionary built B2B commerce with Medusa -- [Use Case: Platform](https://medusajs.com/blog/catalog/): How Catalog built a B2B platform for SMBs with Medusa +This chapter is also useful for developers who are already familiar with Medusa v1 and want to learn about the main differences from Medusa v2. However, it doesn't cover all the new and improved concepts in Medusa v2. Instead, it's highly recommended to read the rest of this documentation to learn about them. *** -## Who is this documentation for +## Prerequisites -This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way. +### Node.js Version -By following this documentation, you'll be able to create custom commerce experiences that would otherwise take large engineering teams months to build. +While Medusa v1 supported Node.js v16+, Medusa v2 requires Node.js v20+. So, make sure to update your Node.js version if it's older. -### How to use the documentation +Refer to the [Node.js documentation](https://nodejs.org/en/docs/) for instructions on how to update your Node.js version. -This documentation is split into the following sections: +### New Database -|Section|Description| -|---|---|---| -|Main Documentation|The documentation you're currently reading. It's recommended to follow the chapters in this documentation to understand the core concepts of Medusa and how to use them before jumping into the other sections.| -|Product|Documentation for the | -|Build|Recipes| -|Tools|Guides on how to setup and use Medusa's CLI tools, | -|API Routes References|References of the | -|References|Useful during your development with Medusa to learn about different APIs and how to use them. Its references include the | -|User Guide|Guides that introduce merchants and store managers to the Medusa Admin dashboard and helps them understand how to use the dashboard to manage their store.| -|Cloud|Learn about Cloud, our managed services offering for Medusa applications. Find guides on how to deploy your Medusa application, manage organizations, and more.| +Medusa v2 makes big changes to the database. So, your existing database will not be compatible with the database for your v2 project. -To get started, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). +If you want to keep your product catalog, you should export the products from the admin dashboard, as explained in [this V1 User Guide](https://docs.medusajs.com/v1/user-guide/products/export/index.html.md). Then, you can import them into your new v2 project from the [Medusa Admin](https://docs.medusajs.com/user-guide/products/import/index.html.md). -*** +For other data types, you'll probably need to migrate them manually through custom scripts. [Custom CLI scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) may be useful for this. -## Useful Links +*** -- Need Help? Refer to our [GitHub repository](https://github.com/medusajs/medusa) for [issues](https://github.com/medusajs/medusa/issues) and [discussions](https://github.com/medusajs/medusa/discussions). -- [Join the community on Discord](https://discord.gg/medusajs). -- Have questions or need more support? Contact our [sales team](https://medusajs.com/contact/). -- Facing issues in your development? Refer to our [troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md). +## How to Upgrade from Medusa v1 to v2 +In this section, you'll learn how to upgrade your Medusa project from v1 to v2. -# Worker Mode of Medusa Instance +To create a fresh new Medusa v2 project, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). -In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. +It's highly recommended to fully go through this chapter before you actually update your application, as some v1 features may have been removed or heavily changed in v2. By doing so, you'll formulate a clearer plan for your migration process and its feasibility. -## What is Worker Mode? +### 1. Update Dependencies in package.json -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. +The first step is to update the dependencies in your `package.json`. -In a production environment, you should deploy two separate instances of your Medusa application: +A basic v2 project has the following dependencies in `package.json`: -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. +```json +{ + "dependencies": { + "@medusajs/admin-sdk": "2.8.2", + "@medusajs/cli": "2.8.2", + "@medusajs/framework": "2.8.2", + "@medusajs/medusa": "2.8.2", + "@mikro-orm/core": "6.4.3", + "@mikro-orm/knex": "6.4.3", + "@mikro-orm/migrations": "6.4.3", + "@mikro-orm/postgresql": "6.4.3", + "awilix": "^8.0.1", + "pg": "^8.13.0" + }, + "devDependencies": { + "@medusajs/test-utils": "2.8.2", + "@mikro-orm/cli": "6.4.3", + "@swc/core": "1.5.7", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.13", + "@types/node": "^20.0.0", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.2.25", + "jest": "^29.7.0", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.2", + "vite": "^5.2.11", + "yalc": "^1.0.0-pre.53" + } +} +``` + +The main changes are: + +- You need to install the following Medusa packages (All these packages use the same version): + - `@medusajs/admin-sdk` + - `@medusajs/cli` + - `@medusajs/framework` + - `@medusajs/medusa` + - `@medusajs/test-utils` (as a dev dependency) +- You need to install the following extra packages: + - Database packages: + - `@mikro-orm/core@6.4.3` + - `@mikro-orm/knex@6.4.3` + - `@mikro-orm/migrations@6.4.3` + - `@mikro-orm/postgresql@6.4.3` + - `@mikro-orm/cli@6.4.3` (as a dev dependency) + - `pg^8.13.0` + - Framework packages: + - `awilix@^8.0.1` + - Development and Testing packages: + - `@swc/core@1.5.7` + - `@swc/jest@^0.2.36` + - `@types/node@^20.0.0` + - `jest@^29.7.0` + - `ts-node@^10.9.2` + - `typescript@^5.6.2` + - `vite@^5.2.11` + - `yalc@^1.0.0-pre.53` +- Other packages, such as `@types/react` and `@types/react-dom`, are necessary for admin development and TypeScript support. + +Notice that Medusa now uses MikroORM instead of TypeORM for database functionalities. + +Once you're done, run the following command to install the new dependencies: -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. +```bash npm2yarn +npm install +``` -This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. +In Medusa v1, you needed to install Medusa modules like the Cache, Event, or Pricing modules. -![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) +These modules are now available out of the box, and you don't need to install or configure them separately. -*** +### 2. Update Script in package.json -## How to Set Worker Mode +Medusa v2 comes with changes and improvements to its CLI tool. So, update your `package.json` with the following scripts: -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: +```json +{ + "scripts": { + "build": "medusa build", + "seed": "medusa exec ./src/scripts/seed.ts", + "start": "medusa start", + "dev": "medusa develop", + "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=false --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" + } +} +``` -- `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. +Where: -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. +- `build`: Builds the Medusa application for production. +- `seed`: Seeds the database with initial data. +- `start`: Starts the Medusa server in production. +- `dev`: Starts the Medusa server in development mode. +- `test:integration:http`: Runs HTTP integration tests. +- `test:integration:modules`: Runs module integration tests. +- `test:unit`: Runs unit tests. -For example, set the worker mode in `medusa-config.ts` to the following: +You'll learn more about the [changes in the CLI tool later in this chapter](#medusa-cli-changes). You can also refer to the following documents to learn more about these changes: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", - // ... - }, - // ... -}) -``` +- [Medusa CLI reference](https://docs.medusajs.com/resources/medusa-cli/index.html.md) +- [Build Medusa Application](https://docs.medusajs.com/learn/build/index.html.md) +- [Integration and Module Tests](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md). -You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. +### 3. TSConfig Changes -Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: +In Medusa v1, you had multiple TSConfig configuration files for different customization types. For example, you had `tsconfig.admin.json` for admin customizations and `tsconfig.server.json` for server customizations. -### Server Medusa Instance +In Medusa v2, you only need one [root tsconfig.json](https://github.com/medusajs/medusa-starter-default/blob/master/tsconfig.json) file in your project. For admin customizations, you create a [src/admin/tsconfig.json file](https://github.com/medusajs/medusa-starter-default/blob/master/src/admin/tsconfig.json). Refer to each of those links for the recommended configurations. -```bash -WORKER_MODE=server -``` +### 4. Update Configuration File -### Worker Medusa Instance +In Medusa v1, you configured your application in the `medusa-config.js` file. Medusa v2 supports this file as `medusa-config.ts`, so make sure to rename it. -```bash -WORKER_MODE=worker -``` +`medusa-config.ts` now exports configurations created with the `defineConfig` utility. It also uses the [loadEnv](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md) utility to load environment variables based on the current environment. -### Disable Admin in Worker Mode +For example, this is the configuration file for a basic Medusa v2 project: -Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. +```ts title="medusa-config.ts" +import { loadEnv, defineConfig } from "@medusajs/framework/utils" -To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: +loadEnv(process.env.NODE_ENV || "development", process.cwd()) -```ts title="medusa-config.ts" module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false, + projectConfig: { + databaseUrl: process.env.DATABASE_URL, + http: { + storeCors: process.env.STORE_CORS!, + adminCors: process.env.ADMIN_CORS!, + authCors: process.env.AUTH_CORS!, + jwtSecret: process.env.JWT_SECRET || "supersecret", + cookieSecret: process.env.COOKIE_SECRET || "supersecret", + }, }, - // ... }) ``` -Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. +You can refer to the full list of configurations in the [Medusa Configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md) chapter. The following table highlights the main changes between v1 and v2: -Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: +|Medusa v1|Medusa v2| +|---|---| +|\`projectConfig.store\_cors\`|projectConfig.http.storeCors| +|\`projectConfig.admin\_cors\`|projectConfig.http.adminCors| +|\`projectConfig.auth\_cors\`|projectConfig.http.authCors| +|\`projectConfig.cookie\_secret\`|projectConfig.http.cookieSecret| +|\`projectConfig.jwt\_secret\`|projectConfig.http.jwtSecret| +|\`projectConfig.database\_database\`|projectConfig.databaseName| +|\`projectConfig.database\_url\`|projectConfig.databaseUrl| +|\`projectConfig.database\_schema\`|projectConfig.databaseSchema| +|\`projectConfig.database\_logging\`|projectConfig.databaseLogging| +|\`projectConfig.database\_extra\`|projectConfig.databaseDriverOptions| +|\`projectConfig.database\_driver\_options\`|projectConfig.databaseDriverOptions| +|\`projectConfig.redis\_url\`|projectConfig.redisUrl| +|\`projectConfig.redis\_prefix\`|projectConfig.redisPrefix| +|\`projectConfig.redis\_options\`|projectConfig.redisOptions| +|\`projectConfig.session\_options\`|projectConfig.sessionOptions| +|\`projectConfig.http\_compression\`|projectConfig.http.compression| +|\`projectConfig.jobs\_batch\_size\`|No longer supported.| +|\`projectConfig.worker\_mode\`|projectConfig.workerMode| +|\`modules\`|Array of modules| -### Server Medusa Instance +#### Plugin Changes -```bash -ADMIN_DISABLED=false -``` +While the `plugins` configuration hasn't changed, plugins available in Medusa v1 are not compatible with Medusa v2. These are covered later in the [Plugin Changes](#plugin-changes) section. -### Worker Medusa Instance +#### Module Changes -```bash -ADMIN_DISABLED=true -``` +In Medusa v1, you had to configure modules like Inventory, Stock Location, Pricing, and Product. These modules are now available out of the box, and you don't need to install or configure them separately. +For the Cache and Event modules, refer to the [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) and [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) documentations to learn how to configure them in v2 if you had them configured in v1. -# Translate Medusa Admin +#### Feature Flags -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. +Some features like product categories and tax inclusive pricing were disabled behind feature flags. -{/* vale docs.We = NO */} +All of these features are now available out-of-the-box. So, you don't need to enable them in your configuration file anymore. -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. +#### Admin Configurations -{/* vale docs.We = YES */} +In v1, the admin dashboard was installed as a plugin with configurations. In v2, the Medusa Admin is available out-of-the-box with different configurations. -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). +The [Medusa Admin Changes](#medusa-admin-changes) section covers the changes in the Medusa Admin configurations and customizations. -*** +### 5. Setup New Database -## How to Contribute Translation +Now that you have updated your dependencies and configuration file, you need to set up the database for your v2 project. -1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: +This will not take into account entities and data customizations in your v1 project, as you still need to change those. Instead, it will only create the database and tables for your v2 project. + +First, change your database environment variables to the following: ```bash -git clone https://github.com/medusajs/medusa.git +DATABASE_URL=postgres://localhost/$DB_NAME +DB_NAME=medusa-v2 ``` -If you already have it cloned, make sure to pull the latest changes from the `develop` branch. +You can change `medusa-v2` to any database name you prefer. -2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: +Then, run the following commands to create the database and tables: -```bash -yarn install +```bash npm2yarn +npx medusa db:setup ``` -3. Create a branch that you'll use to open the pull request later: +This command will create the database and tables for your v2 project. -```bash -git checkout -b feat/translate- -``` +After that, you can start your Medusa application with the `dev` command. Note that you may have errors if you need to make implementation changes that are covered in the rest of this guide, so it's better to wait until you finish the v2 migration process before starting the Medusa application. -Where `` is your language name. For example, `feat/translate-da`. +### (Optional) 6. Seed with Demo Data -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`. +If you want to seed your Medusa v2 project with demo data, you can copy the content of [this file](https://github.com/medusajs/medusa-starter-default/blob/master/src/scripts/seed.ts) to your `src/scripts/seed.ts` file. -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: +Then, run the following command to seed the database: -```bash title="packages/admin/dashboard" -yarn i18n:validate da.json +```bash npm2yarn +npm run seed ``` -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" +This will seed your database with demo data. -export default { - // other languages... - da: { - translation: da, - }, -} -``` +*** -The language's key in the object is the ISO-2 name of the language. +## Medusa Admin Changes -7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: +In this section, you'll learn about the changes in the Medusa Admin between v1 and v2. -```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} -import { da } from "date-fns/locale" -// other imports... +This section doesn't cover changes related to Medusa Admin customizations. They're covered later in the [Admin Customization Changes](#admin-customization-changes) section. -export const languages: Language[] = [ - // other languages... - { - code: "da", - display_name: "Danish", - ltr: true, - date_locale: da, - }, -] -``` +The Medusa Admin is now available out-of-the-box. It's built with [Vite v5](https://vite.dev/) and runs at `http://localhost:9000/app` by default when you start your Medusa application. -`languages` is an array having the following properties: +### Admin Configurations -- `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. +You previously configured the admin dashboard when you added it as a plugin in Medusa v1. -8. Once you're done, push the changes into your branch and open a pull request on GitHub. +In Medusa v2, you configure the Medusa Admin within the `defineConfig` utility in `medusa-config.ts`. `defineConfig` accepts an `admin` property to configure the Medusa Admin: -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. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // admin options... + }, +}) +``` +You can refer to the [Medusa Configuration](https://docs.medusajs.com/learn/configurations/medusa-config#admin-configurations-admin/index.html.md) chapter to learn about all the admin configurations. The following table highlights the main changes between v1 and v2: -# Docs Contribution Guidelines +|Medusa v1|Medusa v2| +|---|---| +|\`serve\`|admin.disable| +|\`autoRebuild\`|No longer supported. The Medusa Admin is always built when you run the | +|\`backend\`|admin.backendUrl| +|\`outDir\`|No longer supported. The Medusa Admin is now built in the | +|\`develop\`|No longer supported. The | -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. +### Admin Webpack Configurations -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). +In v1, you were able to modify Webpack configurations of the admin dashboard. -## What Can You Contribute? +Since Medusa Admin is now built with Vite, you can modify the Vite configurations with the `admin.vite` configuration. Learn more in the [Medusa Configuration](https://docs.medusajs.com/learn/configurations/medusa-config#vite/index.html.md) chapter. -You can contribute to the Medusa documentation in the following ways: +### Admin CLI Tool -- 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. +In Medusa v1, you used the `medusa-admin` CLI tool to build and run the admin dashboard. -*** +In Medusa v2, the Medusa Admin doesn't have a CLI tool. Instead, running `medusa build` and `medusa develop` also builds and runs the Medusa Admin, respectively. -## Documentation Workspace +In addition, you can build the Medusa Admin separately from the Medusa application using the `--admin-only` option. Learn more in the [Build Medusa Application](https://docs.medusajs.com/learn/build#separate-admin-build/index.html.md) chapter. -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: +## Medusa CLI Changes -- `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. +The Medusa CLI for v2 is now in the `@medusajs/cli` package. However, you don't need to install it globally. You can just use `npx medusa` in your Medusa projects. -### Setup the Documentation Workspace +Refer to the [Medusa CLI reference](https://docs.medusajs.com/resources/medusa-cli/index.html.md) for the full list of commands and options. The following table highlights the main changes between v1 and v2: -### Prerequisites +|Medusa v1|Medusa v2| +|---|---| +|\`migrations run\`|db:migrate| +|\`migrations revert\`|db:rollback| +|\`migrations show\`|No longer supported.| +|\`seed\`|No longer supported. However, you can create a | +|\`start-cluster\`|start --cluster \| -- [Node.js v20+](https://nodejs.org/en/download) -- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install) +*** -In the `www` directory, run the following command to install the dependencies: +## Plugin Changes -```bash -yarn install -``` +Medusa v2 supports plugins similar to Medusa v1, but with changes in its usage, development, and configuration. -Then, run the following command to build packages under the `www/packages` directory: +In Medusa v1, you created plugins that contained customizations like services that integrated third-party providers, custom API routes, and more. -```bash -yarn build -``` +In Medusa v2, a plugin can contain customizations like modules that integrate third-party providers, custom API routes, workflows, and more. The plugin development experience has also been improved to resolve big pain points that developers faced in v1. -After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server. +Refer to the [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) chapter to learn more about plugins in Medusa v2. -*** +The rest of this section will cover some of the main changes in plugins between v1 and v2. -## Documentation Content +### Medusa Plugins Alternative -All documentation projects are built with Next.js. The content is writtin in MDX files. +In v1, Medusa provided a set of plugins that you could use in your project. For example, the Stripe and SendGrid plugins. -### Medusa Main Docs Content +In v2, some of these plugins are now available as module providers out-of-the-box. For example, the Stripe and SendGrid module providers. Other plugins may no longer be available, but you can still find guides to create them. -The content of the Medusa main docs are under the `www/apps/book/app` directory. +The following table highlights the alternatives for the Medusa v1 plugins: -### Medusa Resources Content +|Medusa v1|v2 Alternative| +|---|---| +|Algolia|Guide| +|Brightpearl|Not available, but you can follow the | +|Contenful|Guide| +|Discount Generator|Not available, but you can build it | +|IP Lookup|Not available, but you can build it | +|Klarna|Not available, but you can integrate it as a | +|Local File|Local File Module Provider| +|Mailchimp|Guide| +|MinIO|S3 (compatible APIs) File Module Provider| +|MeiliSearch|Not available, but you can integrate it | +|PayPal|Not available, but you can integrate it as a | +|Restock Notification|Guide| +|S3|S3 (compatible APIs) File Module Provider| +|Segment|Guide| +|SendGrid|SendGrid Module Provider| +|Shopify|Not available, but you can build it | +|Slack|Guide| +|Spaces (DigitalOcean)|S3 (compatible APIs) File Module Provider| +|Strapi|Not available, but you can integrate it | +|Stripe|Stripe Payment Module Provider| +|Twilio|Guide| +|Wishlist|Guide| + +You can also find Medusa and community integrations in the [Integrations](https://medusajs.com/integrations/) page. + +### Plugin Options + +Similar to Medusa v1, you can pass options to plugins in Medusa v2. + +However, plugin options are now only passed to modules and module providers created in a plugin. + +So, if you previously accessed options in a plugin's subscriber, for example, that's not possible anymore. You need to access the options in a module or module provider instead, then use its service in the plugin's subscriber. + +For example, this is how you can access options in a plugin's subscriber in v2: -The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -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. +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const customModuleService = container.resolve("custom") -### API Reference + const options = customModuleService.getOptions() -The API reference's content is split into two types: + // Use the options in your logic... +} -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. +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` -### Medusa UI Documentation +Learn more in the [Create Plugin](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md) chapter. -The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. +#### enableUI Option -The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. +Plugins in v1 accepted an `enableUI` option to configure whether a plugin's admin customizations should be shown. -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. +In v2, this option is no longer supported. All admin customizations in a plugin will be shown in the Medusa Admin. *** -## 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). +## Tool Changes -*** +This section covers changes to tools that were available in Medusa v1. -## How to Contribute +|Medusa v1|Medusa v2| +|---|---| +|JS Client|JS SDK| +|Medusa React|No longer supported. Instead, you can | +|Next.js Starter Template|Next.js Starter Storefront| +|Medusa Dev CLI|No longer supported.| -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. +## Changes in Concepts and Development -### Base Branch +In the next sections, you'll learn about the changes in specific concepts and development practices between Medusa v1 and v2. -When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. +### Entities, Services, and Modules -Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. +In Medusa v1, entities, services, and modules were created separately: -### Branch Name +- You create an entity to add a new table to the database. +- You create a service to add new business logic to the Medusa application. +- You create a module to add new features to the Medusa application. It may include entities and services. -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/`. +In Medusa v2, you create entities (now called data models) and services in a module. You can't create them separately anymore. The data models define new tables to add to the database, and the service provides data-management features for those data models. -### Pull Request Conventions +In this section, you'll learn about the most important changes related to these concepts. You can also learn more in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter. -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`. +#### Modules -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”. +A module is a reusable package of functionalities related to a single domain or integration. For example, Medusa provides a Product Module for product-related data models and features. -*** +So, if in Medusa v1 you had a `Brand` entity and a service to manage it, in v2, you create a Brand Module that defines a `Brand` data model and a service to manage it. -## Images +To learn how to create a module, refer to the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter. -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. +![Diagram showcasing the directory structure difference between Medusa v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748277500/Medusa%20Book/modules-v1-v2_dsnzyl.jpg) -*** +#### Data Models -## NPM and Yarn Code Blocks +In Medusa v1, you created data models (entities) using TypeORM. -If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. +In Medusa v2, you use Medusa's Data Model Language (DML) to create data models. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. For example: -````md -```bash npm2yarn -npm run start +```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(), +}) ``` -```` -The code snippet must be written using NPM. +Learn more about data models in the [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) chapters. -### Global Option +#### Migrations -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: +In Medusa v1, you had to write migrations manually to create or update tables in the database. Migrations were based on TypeORM. + +In Medusa v2, you can use the Medusa CLI to generate migrations based on MikroORM. For example: ```bash npm2yarn -npm install @medusajs/cli -g +npx medusa db:generate brand ``` -*** +This generates migrations for data models in the Brand Module. Learn more in the [Migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) chapter. -## Linting with Vale +#### Services -Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. +In Medusa v1, you created a service with business logic related to a feature within your Medusa project. For example, you created a `BrandService` at `src/services/brand.ts` to manage the `Brand` entity. -### Result of Vale PR Checks +In Medusa v2, you can only create a service in a module, and the service either manages the module's data models in the database, or connects to third-party services. -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. +For example, you create a `BrandService` in the Brand Module at `src/modules/brand/service.ts`: -### Run Vale Locally +```ts title="src/modules/brand/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" -If you want to check your work locally, you can do that by: +class BrandModuleService extends MedusaService({ + Brand, +}) { -1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. -2. Changing to the `www/vale` directory: +} -```bash -cd www/vale +export default BrandModuleService ``` -3\. Running the `run-vale` script: +The service has automatically generated data-management methods by extending `MedusaService` from the Modules SDK. So, you now have methods like `retrieveBrand` and `createBrands` available in the service. -```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 -``` +Learn more in the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter. -{/* TODO need to enable MDX v1 comments first. */} +When you register the module in the Medusa application, the service is registered in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to use its methods in workflows, subscribers, scheduled jobs, and API routes. -{/* ### Linter Exceptions +#### Repositories -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: +In Medusa v1, you used the repository of a data model in a service to provide data-management features. For example, you used the `BrandRepository` to manage the `Brand` entity. Repositories were also based on TypeORM. -```md - +In Medusa v2, you generally don't need repositories for basic data-management features, as they're generated by the service factory. However, for more complex use cases, you can use the data model repository based on MikroORM. -content that shouldn't be scanned for errors here... +For example: - -``` +```ts title="src/modules/brand/service.ts" +import { InferTypeOf, DAL } from "@medusajs/framework/types" +import Post from "./models/post" -You can also disable specific rules. For example: +type Post = InferTypeOf -```md - +type InjectedDependencies = { + postRepository: DAL.RepositoryService +} -Medusa supports Node versions 14 and 16. +class BlogModuleService { + protected postRepository_: DAL.RepositoryService - + constructor({ + postRepository, + }: InjectedDependencies) { + super(...arguments) + this.postRepository_ = postRepository + } +} + +export default BlogModuleService ``` -If you use this in your PR, you must justify its usage. */} +Learn more in the [Database Operations](https://docs.medusajs.com/learn/fundamentals/modules/db-operations/index.html.md) chapter. -*** +#### Module Isolation -## Linting with ESLint +In Medusa v1, you had access to all entities and services in the Medusa application. While this approach was flexible, it introduced complexities, was difficult to maintain, and resulted in hacky workarounds. -Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. +In Medusa v2, modules are isolated. This means that you can only access entities and services within the module. This isolation allows you to integrate modules into your application without side effects, while still providing you with the necessary flexibility to build your use cases. -### Linting Content with ESLint +The [Module Isolation](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) chapter explains this concept in detail. The rest of this section gives a general overview of how module isolation affects your Medusa v1 customizations. -Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. +#### Extending Entities -If you want to check content ESLint errors locally and fix them, you can do that by: +In Medusa v1, you were able to extend entities by creating a new entity that extended the original one. For example, you could create a custom `Product` entity that extended the original `Product` entity to add a `brand` column. -1\. Install the dependencies in the `www` directory: +In Medusa v2, you can no longer extend entities. Instead, you need to create a new data model that contains the columns you want to add. Then, you can create a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that links your data model to the one you want to extend. -```bash -yarn install -``` +For example, you create a Brand Module that has a `Brand` data model. Then, you create a Module Link that links the `Brand` data model to the `Product` data model in the Product Module: -2\. Run the turbo command in the `www` directory: +```ts title="src/links/product-brand.ts" +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -```bash -turbo run lint:content +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) ``` -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`. +You can then associate brands with a product, retrieve them in API routes and custom functionalities, and more. -If you want to check code ESLint errors locally and fix them, you can do that by: +Learn more in the [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) chapter. -1\. Install the dependencies in the `www` directory: +#### Extending Services -```bash -yarn install -``` +In Medusa v1, you were able to extend services by creating a new service that extended the original one. For example, you could create a custom `ProductService` that extended the original `ProductService` to add a new method. -2\. Run the turbo command in the `www` directory: +In Medusa v2, you can no longer extend services. Instead, you need to [create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with a service that contains the methods you want to add. Then, you can: -```bash -yarn lint -``` +- Build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use both services to achieve a custom feature. +- Consume [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to run custom actions in existing workflows. +- For more complex use cases, you can re-create an existing workflow and use your custom module's service in it. -This will fix any fixable errors, and show errors that require your action. +For example, if you extended the `CartService` in v1 to add items with custom prices to the cart, you can instead build a custom workflow that uses your custom module to retrieve an item's price, then add it to the cart using the existing `addToCartWorkflow`: -{/* TODO need to enable MDX v1 comments first. */} +```ts +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { addToCartWorkflow } from "@medusajs/medusa/core-flows" +import { + getCustomPriceStep, +} from "./steps/get-custom-price" -{/* ### ESLint Exceptions +type AddCustomToCartWorkflowInput = { + cart_id: string + item: { + variant_id: string + quantity: number + metadata?: Record + } +} -If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: +export const addCustomToCartWorkflow = createWorkflow( + "add-custom-to-cart", + ({ cart_id, item }: AddCustomToCartWorkflowInput) => { + // assuming this step uses a custom module to get the price + const price = getCustomPriceStep({ + variant: item.variant_id, + currencyCode: "usd", + quantity: item.quantity, + }) -~~~md - + const itemToAdd = transform({ + item, + price, + }, (data) => { + return [{ + ...data.item, + unit_price: data.price, + }] + }) -```js -console.log("This block isn't linted") + addToCartWorkflow.runAsStep({ + input: { + items: itemToAdd, + cart_id, + }, + }) + } +) ``` -```js -console.log("This block is linted") -``` -~~~ +Refer to the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapters to learn more about workflows in Medusa v2. -You can also disable specific rules. For example: +#### Integrating Third-Party Services -~~~md - +In Medusa v1, you integrated third-party services by creating a service under `src/services` and using it in your customizations. -```js -console.log("This block can use semicolons"); -``` +In Medusa v2, you can integrate third-party services by creating a module with a service that contains the methods to interact with the third-party service. You can then use the module's service in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build custom features. -```js -console.log("This block can't use semi colons") -``` -~~~ */} +![Directory structure change between v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748278103/Medusa%20Book/integrations-v1-v2_cjzkus.jpg) +*** -# Usage Information +### Medusa and Module Containers -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. +In Medusa v1, you accessed dependencies from the container in all your customizations, such as services, API routes, and subscribers. -*** +In Medusa v2, there are two containers: -## Purpose +|Container|Description|Accessed By| +|---|---|---| +|Medusa container|Main container that contains Framework and commerce resources, such as services of registered modules.|| +|Module container|Container of a module. It contains some resources from the Framework, and resources implemented in the module.|Services and loaders in the module.| -As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa. +You can view the list of resources in each container in the [Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) reference. -We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions. +*** -However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production. +### Workflow Changes -Collecting this data allows us to understand certain details such as: +In Medusa v2, workflows are the main way to implement custom features spanning across modules and systems. -- What operating system do most Medusa developers use? -- What version of Medusa is widely used? -- What parts of the Medusa Admin are generally undiscovered by our users? -- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data? -- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support? +Workflows have been optimized for data reliability, flexibility, and orchestration across systems. You can learn more in the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapters. -*** +This section highlights the main changes in workflows between v1 and v2. -## Medusa Application Analytics +#### Workflows SDK Imports -This section covers which data in the Medusa application are collected and how to opt out of it. +In Medusa v1, you imported all Workflows SDK functions and types from the `@medusajs/workflows-sdk` package. -### Collected Data in the Medusa Application +In Medusa v2, you import them from the `@medusajs/framework/workflows-sdk` package. For example: -The following data is being collected on your Medusa application: +```ts title="src/workflows/hello-world.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +``` -- Unique project ID generated with UUID. -- Unique machine ID generated with UUID. -- Operating system information including Node version or operating system platform used. -- The version of the Medusa application and Medusa CLI are used. +#### Workflow Return Value -Data is only collected when the Medusa application is run with the command `medusa start`. +In Medusa v1, you returned any value from a workflow, such as a string or an object. -### How to Opt Out +In Medusa v2, you must return an instance of `WorkflowResponse` from a workflow. The data passed to `WorkflowResponse`'s constructor is returned to the caller of the workflow. -If you prefer to disable data collection, you can do it either by setting the following environment variable to true: +For example: -```bash -MEDUSA_DISABLE_TELEMETRY=true -``` - -Or, you can run the following command in the root of your Medusa application project to disable it: +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -```bash -npx medusa telemetry --disable -``` +export const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + return new WorkflowResponse("Hello, world!") + } +) -*** +// in API route, for example: +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -## Admin Analytics +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + // message is "Hello, world!" + const { result: message } = await helloWorldWorkflow(req.scope) + .run() -This section covers which data in the admin are collected and how to opt out of it. + res.json({ + message, + }) +} +``` -### Collected Data in Admin +#### New Workflow Features -Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data. +- [Use when-then in workflows to run steps if a condition is satisfied](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). +- [Consume hooks to run custom steps in existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +- [Create long-running workflows that run asynchronously in the background](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md). -The following data is being collected on your admin: +*** -- The name of the store. -- The email of the user. -- The total number of products, orders, discounts, and users. -- The number of regions and their names. -- The currencies used in the store. -- Errors that occur while using the admin. +### API Route Changes -### How to Enable Anonymization +API routes are generally similar in Medusa v1 and v2, but with minor changes. -To enable anonymization of your data from the Medusa Admin: +You can learn more about creating API routes in the [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) chapters. This section highlights the main changes in API routes between v1 and v2. -1. Go to Settings → Personal Information. -2. In the Usage insights section, click on the “Edit preferences” button. -3. Enable the "Anonymize my usage data” toggle. -4. Click on the “Submit and close” button. +#### HTTP Imports -### How to Opt-Out +In Medusa v1, you imported API-route related types and functions from the `@medusajs/medusa` package. -To opt out of analytics collection in the Medusa Admin, set the following environment variable: +In Medusa v2, you import them from the `@medusajs/framework/http` package. For example: -```bash -MEDUSA_FF_ANALYTICS=false +```ts title="src/api/store/custom/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" ``` +#### Protected API Routes -# Storefront Development +In Medusa v1, routes starting with `/store/me` and `/admin` were protected by default. -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. +In Medusa v2, routes starting with `/store/customers/me` are accessible by registered customers, and `/admin` routes are accessible by admin users. -You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. 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 an API route, you can access the logged in user or customer using the `auth_context.actor_id` property of `AuthenticatedMedusaRequest`. For example: -- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) -- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) +```ts title="src/api/store/custom/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -*** +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.auth_context?.actor_id -## Passing a Publishable API Key in Storefront Requests + // ... +} +``` -When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. +Learn more in the [Protected API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) chapter. -A publishable API key sets the scope of your request to one or more sales channels. +#### Authentication Middlewares -Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. +In Medusa v1, you had three middlewares to protect API routes: -Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). +- `authenticate` to protect API routes for admin users. +- `authenticateCustomer` to optionally authenticate customers. +- `requireCustomerAuthentication` to require customer authentication. +In Medusa v2, you can use a single `authenticate` middleware for the three use cases. For example: -# Updating Medusa +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -In this chapter, you'll learn about updating your Medusa application and packages. +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], + }, + { + matcher: "/custom/customer*", + // equivalent to requireCustomerAuthentication + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + { + matcher: "/custom/all-customers*", + // equivalent to authenticateCustomer + middlewares: [authenticate("customer", ["session", "bearer"], { + allowUnauthenticated: true, + })], + }, + ], +}) +``` -Medusa's current version is v{config.version.number}. {releaseNoteText} +Learn more in the [Protected API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) chapter. -## Medusa Versioning +#### Middlewares -When Medusa puts out a new release, all packages are updated to the same version. This ensures that all packages are compatible with each other, and makes it easier for you to switch between versions. +In Medusa v1, you created middlewares by exporting an object in the `src/api/middlewares.ts` file. -This doesn't apply to the design-system packages, including `@medusajs/ui`, `@medusajs/ui-presets`, and `@medusajs/ui-icons`. These packages are versioned independently. However, you don't need to install and manage them separately in your Medusa application, as they are included in the `@medusajs/admin-sdk`. If you're using them in a standalone project, such as a storefront or custom admin dashboard, refer to [this section in the Medusa UI documentation](https://docs.medusajs.com/ui/installation/standalone-project#updating-ui-packages/index.html.md) for update instructions. +In Medusa v2, you create middlewares by exporting an object created with `defineMiddlewares`, which accepts an object with the same properties as in v1. For example: -Medusa updates the version number `major.minor.patch` according to the following rules: +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -- **patch**: A patch release includes bug fixes and minor improvements. It doesn't include breaking changes. For example, if the current version is `2.0.0`, the next patch release will be `2.0.1`. -- **minor**: A minor release includes new features, fixes, improvements, and breaking changes. For example, if the current version is `2.0.0`, the next minor release will be `2.1.0`. -- **major**: A major release includes significant changes to the entire codebase and architecture. For those, the update process will be more elaborate. For example, if the current version is `2.0.0`, the next major release would be `3.0.0`. +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") -*** + next() + }, + ], + }, + ], +}) +``` -## Check Installed Version +Learn more in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. -To check the currently installed version of Medusa in your project, run the following command in your Medusa application: +#### Disable Body Parser -```bash -npx medusa -v -``` +In Medusa v1, you disabled the body parser in API routes by setting `bodyParser: false` in the route's middleware configuration. -This will show you the installed version of Medusa and the [Medusa CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md), which should be the same. +In Medusa v2, you disable the body parser by setting `bodyParser.preserveRawBody` to `true` in the route's middleware configuration. For example: -*** +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" -## Check Latest Version +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { preserveRawBody: true }, + matcher: "/custom", + }, + ], +}) +``` -The documentation shows the current version at the top right of the navigation bar. When a new version is released, you'll find a blue dot on the version number. Clicking it will take you to the [release notes on GitHub](https://github.com/medusajs/medusa/releases). +Learn more in the [Body Parser](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md) chapter. -You can also star the [Medusa repository on GitHub](https://github.com/medusajs/medusa) to receive updates about new releases on your GitHub dashboard. Our team also shares updates on new releases on our social media channels. +#### Extending Validators -*** +In Medusa v1, you passed custom request parameters to Medusa's API routes by extending a request's validator. -## Update Medusa Application +In Medusa v2, some Medusa API routes support passing additional data in the request body. You can then configure the validation of that additional data and consume them in the hooks of the workflow used in the API route. -Before updating a Medusa application, make sure to check the [release notes](https://github.com/medusajs/medusa/releases) for any breaking changes that require actions from your side. +For example: -Then, to update your Medusa application, bump the version of all `@medusajs/*` dependencies in your `package.json`. Then, re-install dependencies: +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" -```bash npm2yarn -npm install +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], +}) ``` -This will update all Medusa packages to the latest version. - -### Running Migrations +In this example, you allow passing a `brand` property as additional data to the `/admin/products` API route. -Releases may include changes to the database, such as new tables, updates to existing tables, updates after adding links, or data migration scripts. +You can learn more in the [Additional Data](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md) chapter. -So, after updating Medusa, run the following command to migrate the latest changes to your database: +If a route doesn't support passing additional data, you need to [replicate it](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) to support your custom use case. -```bash -npx medusa db:migrate -``` +*** -This will run all pending migrations, sync links, and run data migration scripts. +### Events and Subscribers Changes -### Reverting an Update +Events and subscribers are similar in Medusa v1 and v2, but with minor changes. -Before reverting an update, if you already ran the migrations, you have to first identify the modules who had migrations. Then, before reverting, run the `db:rollback` command for each of those modules. +You can learn more in the [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) chapters. This section highlights the main changes in events and subscribers between v1 and v2. -For example, if the version you updated to had migrations for the Cart and Product Modules, run the following command: +#### Emitted Events -```bash -npx medusa db:rollback cart product -``` +Medusa v2 doesn't emit the same events as v1. Refer to the [Events Reference](https://docs.medusajs.com/resources/references/events/index.html.md) for the full list of events emitted in v2. -Then, revert the update by changing the version of all `@medusajs/*` dependencies in your `package.json` to the previous version and re-installing dependencies: +#### Subscriber Type Imports -```bash npm2yarn -npm install -``` +In Medusa v1, you imported subscriber types from the `@medusajs/medusa` package. -Finally, run the migrations to sync link changes: +In Medusa v2, you import them from the `@medusajs/framework` package. For example: -```bash -npx medusa db:migrate +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" ``` -*** - -## Understanding Codebase Changes - -In the Medusa codebase, our team uses the following [TSDoc](https://tsdoc.org/) tags to indicate changes made in the latest version for a specific piece of code: +#### Subscriber Parameter Change -- `@deprecated`: Indicates that a piece of code is deprecated and will be removed in a future version. The tag's message will include details on what to use instead. However, our updates are always backward-compatible, allowing you to update your codebase at your own pace. -- `@version`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version. +In Medusa v1, a subscriber function received an object parameter that has `eventName` and `data` properties. -*** +In Medusa v2, the subscriber function receives an object parameter that has an `event` property. The `event` property contains the event name and data. For example: -## Update Plugin Project +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -If you have a Medusa plugin project, you only need to update its `@medusajs/*` dependencies in the `package.json` file to the latest version. Then, re-install dependencies: +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + // ... +} -```bash npm2yarn -npm install +export const config: SubscriberConfig = { + event: `order.placed`, +} ``` +Also, the `pluginOptions` property is no longer passed in the subscriber's parameter. Instead, you can access the options passed to a plugin within its modules' services, which you can resolve in a subscriber. -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. - -## API Key Types - -There are two types of API keys: +Learn more in the [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) chapter. -- `publishable`: A public key used in client applications, such as a storefront. -- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. +#### Subscriber Implementation Change -The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). +In Medusa v1, you implemented functionalities, such as sending confirmation email, directly within a subscriber. -*** +In Medusa v2, you should implement these functionalities in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and call the workflow in the subscriber. By using workflows, you benefit from rollback mechanism, among other features. -## API Key Expiration +For example: -An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). +```ts title="src/subscribers/order-placed.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { + sendOrderConfirmationWorkflow, +} from "../workflows/send-order-confirmation" -The associated token is no longer usable or verifiable. +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} -*** +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` -## Token Verification +#### Emitting Events -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. +In Medusa v1, you emitted events in services and API routes by resolving the Event Module's service from the container. +In Medusa v2, you should emit events in workflows instead. For example: -# Links between API Key Module and Other Modules +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -This document showcases the module links defined between the API Key Module and other Commerce Modules. +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... -## Summary + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) +``` -The API Key Module has the following links to other modules: +If you need to emit events in a service, you can add the Event Module as a dependency of your module. Then, you can resolve the Event Module's service from the module container and emit the event. This approach is only recommended for events related to under-the-hood processes. -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|ApiKey|SalesChannel|Stored - many-to-many|Learn more| +Learn more in the [Emit Events](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/emit-event/index.html.md) chapter. *** -## Sales Channel Module +### Loader Changes -You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. +In Medusa v1, you created loaders in the `src/loaders` directory to perform tasks at application startup. -![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) +In Medusa v2, loaders can only be created in a module. You can create loaders in the `src/modules//loaders` directory. That also means the loader can only access resources in the module's container. -This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. +Learn more in the [Loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) chapter. -Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). +#### Loader Parameter Changes -### Retrieve with Query +In Medusa v1, a loader function receives two parameters: `container` and `config`. If the loader was created in a module, it also received a `logger` parameter. -To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: +In Medusa v2, a loader function receives a single object parameter that has a `container` and `options` properties. The `options` property contains the properties passed to the module. -### query.graph +For example: -```ts -const { data: apiKeys } = await query.graph({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" -// apiKeys[0].sales_channels +export default async function helloWorldLoader({ + container, + options, +}: LoaderOptions) { + const logger = container.resolve("logger") + + logger.info("[HELLO MODULE] Just started the Medusa application!") +} ``` -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### Scheduled Job Changes -// ... +Scheduled jobs are similar in Medusa v1 and v2, but with minor changes. -const { data: apiKeys } = useQueryGraphStep({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) +You can learn more about scheduled jobs in the [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) chapters. This section highlights the main changes in scheduled jobs between v1 and v2. -// apiKeys[0].sales_channels -``` +#### Scheduled Job Parameter Changes -### Manage with Link +In Medusa v1, a scheduled job function received an object of parameters. -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +In Medusa v2, a scheduled job function receives only the Medusa container as a parameter. For example: -### link.create +```ts title="src/jobs/hello-world.ts" +import { MedusaContainer } from "@medusajs/framework/types" -```ts -import { Modules } from "@medusajs/framework/utils" +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") -// ... + logger.info("Greeting!") +} -await link.create({ - [Modules.API_KEY]: { - publishable_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} ``` -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The `pluginOptions` property is no longer available, as you can access the options passed to a plugin within its modules' services, which you can resolve in a scheduled job. -// ... +The `data` property is also no longer available, as you can't pass data in the scheduled job's configuration anymore. -createRemoteLinkStep({ - [Modules.API_KEY]: { - publishable_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +#### Scheduled Job Configuration Changes +In Medusa v2, the `data` property is removed from the scheduled job's configuration object. -# API Key Module +#### Scheduled Job Implementation Changes -In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. +In Medusa v1, you implemented functionalities directly in the job function. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard. +In Medusa v2, you should implement these functionalities in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and call the workflow in the scheduled job. By using workflows, you benefit from rollback mechanism, among other features. -Medusa has API-key related features available out-of-the-box through the API Key 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 API Key Module. +For example: -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" -## API Key Features +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} -- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. -- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. -- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. -- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` *** -## How to Use the API Key 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. +### Removed Concepts and Alternatives -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. +The following table highlights concepts that have been removed or changed in Medusa v2 and their alternatives: -For example: +|Medusa v1|Medusa v2| +|---|---| +|Batch Jobs and Strategies|Long-Running Workflows| +|File Service|File Module Provider| +|Notification Provider Service|Notification Module Provider| +|Search Service|Can be integrated as a | -```ts title="src/workflows/create-api-key.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +*** -const createApiKeyStep = createStep( - "create-api-key", - async ({}, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) +### Admin Customization Changes - const apiKey = await apiKeyModuleService.createApiKeys({ - title: "Publishable API key", - type: "publishable", - created_by: "user_123", - }) +This section covers changes to the admin customizations between Medusa v1 and v2. - return new StepResponse({ apiKey }, apiKey.id) - }, - async (apiKeyId, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) +#### Custom Admin Environment Variables - await apiKeyModuleService.deleteApiKeys([apiKeyId]) - } -) +In Medusa v1, you set custom environment variables to be passed to the admin dashboard by prefixing them with `MEDUSA_ADMIN_`. -export const createApiKeyWorkflow = createWorkflow( - "create-api-key", - () => { - const { apiKey } = createApiKeyStep() +In Medusa v2, you can set custom environment variables to be passed to the admin dashboard by prefixing them with `VITE_`. Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter. - return new WorkflowResponse({ - apiKey, - }) - } -) -``` +#### Admin Widgets -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +Due to design changes in the Medusa Admin, some widget injection zones may have been changed or removed. Refer to the [Admin Widgets Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) reference for the full list of injection zones in v2. -### API Route +Also, In Medusa v1, you exported in the widget's file a `config` object with the widget's configurations, such as its injection zone. -```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 { createApiKeyWorkflow } from "../../workflows/create-api-key" +In Medusa v2, you export a configuration object defined with `defineWidgetConfig` from the Admin Extension SDK. For example: -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createApiKeyWorkflow(req.scope) - .run() +```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["9"], ["10"], ["11"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" - res.send(result) +// The widget +const ProductWidget = () => { + // ... } + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -### Subscriber +The function accepts an object with a `zone` property, indicating the zone to inject the widget. -```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 { createApiKeyWorkflow } from "../workflows/create-api-key" +Refer to the [Admin Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) chapter to learn more about creating widgets in Medusa v2. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createApiKeyWorkflow(container) - .run() +### Admin UI Routes - console.log(result) -} +In Medusa v1, UI Routes were prefixed by `/a`. For example, a route created at `src/admin/routes/custom/page.tsx` would be available at `http://localhost:9000/a/custom`. -export const config: SubscriberConfig = { - event: "user.created", -} -``` +In Medusa v2, the `/a` prefix has been removed. So, that same route would be available at `http://localhost:9000/app/custom` (where `/app` is the path to the admin dashboard, not a prefix). -### Scheduled Job +Also, in v1, you exported a `config` object with the route's configurations to show the route in the dashboard's sidebar. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createApiKeyWorkflow } from "../workflows/create-api-key" +In v2, you export a configuration object defined with `defineRouteConfig` from the Admin Extension SDK. For example: -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createApiKeyWorkflow(container) - .run() +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["8"], ["9"], ["10"], ["11"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" - console.log(result) +const CustomPage = () => { + // ... } -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +export const config = defineRouteConfig({ + label: "Custom Route", + icon: ChatBubbleLeftRight, +}) -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +export default CustomPage +``` -*** +The `defineRouteConfig` function accepts an object with the following properties: +- `label`: The label of the route to show in the sidebar. +- `icon`: The icon to use in the sidebar for the route. -# Authentication Flows with the Auth Main Service +Refer to the [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) chapter to learn more about creating UI routes in Medusa v2. -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. +### Admin Setting Routes -## Authentication Methods +In Medusa v1, you created setting pages under the `src/admin/settings` directory with their own configurations. -### Register +In Medusa v2, setting pages are UI routes created under the `src/admin/routes/settings` directory. -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, if you had a `src/admin/settings/custom/page.tsx` file in v1, you should move it to `src/admin/routes/settings/custom/page.tsx` in v2. The file's content will be the same as a UI route. 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. +```tsx title="src/admin/routes/settings/custom/page.tsx" highlights={[["14"], ["15"], ["16"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -### Authenticate +const CustomSettingPage = () => { + return ( + +
+ Custom Setting Page +
+
+ ) +} -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: +export const config = defineRouteConfig({ + label: "Custom", +}) -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +export default CustomSettingPage ``` -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. +In v1, you exported a `config` object that showed a setting page as a card in the settings page. In v2, you export the same configuration object as a UI route. -*** +Learn more about creating setting pages in the [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md) chapter. -## Auth Flow 1: Basic Authentication +### notify Props in Widgets, UI Routes, and Settings -The basic authentication flow requires first using the `register` method, then the `authenticate` method: +In Medusa v1, admin widgets, UI routes, and setting pages received a `notify` prop to show notifications in the admin dashboard. -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) +This prop is no longer passed in v2. Instead, use the [toast utility from Medusa UI](https://docs.medusajs.com/ui/components/toast/index.html.md) to show notifications. -if (error) { - // registration failed - // TODO return an error - return -} +For example: -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +```tsx title="src/admin/widgets/product-details.tsx" highlights={[["7"], ["8"], ["9"]]} +import { toast } from "@medusajs/ui" +import { defineWidgetConfig } from "@medusajs/admin-sdk" -if (success && !location) { - // user is authenticated +// The widget +const ProductWidget = () => { + const handleOnClick = () => { + toast.info("Info", { + description: "The quick brown fox jumps over the lazy dog.", + }) + } + // ... } + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. +Learn more about the `toast` utility in the [Medusa UI Toasts](https://docs.medusajs.com/ui/components/toast/index.html.md) documentation. -The next section explains the flow if `location` is set. +### Sending Requests to Medusa Server -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. +In Medusa v1, you used Medusa React to send requests to the Medusa server. -![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) +Medusa v2 no longer supports Medusa React. Instead, you can use the JS SDK with Tanstack Query to send requests from your admin customizations to the Medusa server. -### Auth Identity with Same Identifier +Learn more in the [Admin Development Tips](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) chapter. -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. +### Admin Languages -There are two ways to handle this: +Medusa Admin v2 supports different languages out-of-the-box, and you can contribute with new translations. -- 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. +Refer to the [User Guide](https://docs.medusajs.com/user-guide/tips/languages/index.html.md) for the list of languages supported in the Medusa Admin. *** -## Auth Flow 2: Third-Party Service Authentication +## Commerce Features Changes -The third-party service authentication method requires using the `authenticate` method first: +In Medusa v2, commerce features are implemented as [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md). For example, the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) implements the product-related features, whereas the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) implements the cart-related features. -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) +So, it's difficult to cover all changes in commerce features between v1 and v2. Instead, this section will highlight changes to customizations that were documented in the Medusa v1 documentation. -if (location) { - // return the location for the front-end to redirect to -} +To learn about all commerce features in Medusa v2, refer to the [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) documentation. -if (!success) { - // authentication failed -} +### Providers are now Module Providers -// authentication successful -``` +In Medusa v1, you created providers for payment, fulfillment, and tax in services under `src/services`. -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. +In Medusa v2, you create these providers as module providers that belong to the Payment, Fulfillment, and Tax modules respectively. -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. +Refer to the following guides to learn how to create these module providers: -![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) +- [Payment Module Provider](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/commerce-modules/fulfillment/fulfillment-provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/commerce-modules/tax/tax-provider/index.html.md) -### Overriding Callback URL +### Overridden Cart Completion -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. +In Medusa v1, you were able to override the cart completion strategy to customize the cart completion process. -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - callback_url: "example.com", - } -) -``` +In Medusa v2, the cart completion process is now implemented in the [completeCartWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow/index.html.md). There are two ways you can customize the completion process: -### validateCallback +- [Consuming hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) like the [validate](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow#validate/index.html.md) hook. This is useful if you only want to make changes in key points of the cart completion process. + - You can view available hooks in the [completeCartWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow#hooks/index.html.md) +- For more complex use cases, you can create a new workflow with the desired functionality. Then, you can [replicate the complete cart API route](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) and use it in your storefront. -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. +### Overridden Tax Calculation -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). +In Medusa v1, you were able to override the tax calculation strategy to customize the tax calculation process. -The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: +In Medusa v2, the tax calculation process is now implemented in a [Tax Module Provider](https://docs.medusajs.com/resources/commerce-modules/tax/tax-provider/index.html.md). So, you can [create a custom tax provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) with the calculation logic you want, then [use it in a tax region](https://docs.medusajs.com/user-guide/settings/tax-regions#edit-tax-region/index.html.md). -```ts -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) +### Overridden Price Selection -if (success) { - // authentication succeeded -} -``` +In Medusa v1, you were able to override the price selection strategy to customize the price selection process. -For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. +In Medusa v2, the price selection process is now implemented in the [Pricing Module's calculate method](https://docs.medusajs.com/resources/commerce-modules/pricing/price-calculation/index.html.md). The Pricing Module allows you to set [flexible rules and tiers](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules/index.html.md) to support your use case. -If the returned `success` property is `true`, the authentication with the third-party provider was successful. +If your use case is complex and these rules are not enough, you can create a new [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with the necessary logic, then use that module in your custom workflows. -![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) +### Gift Card Features + +Medusa v1 has gift card features out-of-the-box. + +In Medusa v2, gift card features are now only available to [Cloud](https://medusajs.com/cloud/) users. *** -## Reset Password +## Deployment Changes -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. +The deployment process in Medusa v2 is similar to v1, but with some changes. For example, the Medusa server is now deployed with Medusa Admin. -For example: +Medusa also provides [Cloud](https://medusajs.com/cloud/), a managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. -```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) +Refer to the [Deployment](https://docs.medusajs.com/resources/deployment/index.html.md) documentation to learn about the deployment process for Medusa applications and Next.js Starter Storefront. -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. +# Introduction -In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. +Medusa is a digital commerce platform with a built-in Framework for customization. -If the returned `success` property is `true`, the password has reset successfully. +Medusa ships with three main tools: +1. A suite of [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) with core commerce functionalities, such as tracking inventory, calculating cart totals, accepting payments, managing orders, and much more. +2. A [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) for building custom functionalities specific to your business, product, or industry. This includes tools for introducing custom API endpoints, business logic, and data models; building workflows and automations; and integrating with third-party services. +3. A customizable admin dashboard for merchants to configure and operate their store. -# Auth Identity and Actor Types +When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that bring your business's vision to life. -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. +*** -## What is an Auth Identity? +## Who should use Medusa -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`. +Medusa is for businesses and teams looking for a digital commerce platform with the tools to implement unique requirements that other platforms aren't built to support. -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +Businesses of all sizes can use Medusa, from small start ups to large enterprises. Also, technical teams of all sizes can build with Medusa; all it takes is a developer to manage and deploy Medusa projects. -*** +Below are some stories from companies that use Medusa: -## Actor Types +- [Use Case: D2C](https://medusajs.com/blog/matt-sleeps/): How Matt Sleeps built a unique D2C experience with Medusa +- [Use Case: OMS](https://medusajs.com/blog/makro-pro/): How Makro Pro Built an OMS with Medusa +- [Use Case: Marketplace](https://medusajs.com/blog/foraged/): How Foraged built a custom marketplace with Medusa +- [Use Case: POS](https://medusajs.com/blog/tekla-pos/): How Tekla built a global webshop and a POS system with Medusa +- [Use Case: B2B](https://medusajs.com/blog/visionary/): How Visionary built B2B commerce with Medusa +- [Use Case: Platform](https://medusajs.com/blog/catalog/): How Catalog built a B2B platform for SMBs with Medusa -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. +## Who is this documentation for -For example, an auth identity of a customer has the following `app_metadata` property: +This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way. -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` +By following this documentation, you'll be able to create custom commerce experiences that would otherwise take large engineering teams months to build. -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. +### How to use the documentation -*** +This documentation is split into the following sections: -## Protect Routes by Actor Type +|Section|Description| +|---|---|---| +|Main Documentation|The documentation you're currently reading. It's recommended to follow the chapters in this documentation to understand the core concepts of Medusa and how to use them before jumping into the other sections.| +|Product|Documentation for the | +|Build|Recipes| +|Tools|Guides on how to setup and use Medusa's CLI tools, | +|API Routes References|References of the | +|References|Useful during your development with Medusa to learn about different APIs and how to use them. Its references include the | +|User Guide|Guides that introduce merchants and store managers to the Medusa Admin dashboard and helps them understand how to use the dashboard to manage their store.| +|Cloud|Learn about Cloud, our managed services offering for Medusa applications. Find guides on how to deploy your Medusa application, manage organizations, and more.| -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. +To get started, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). -For example: +*** -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +## Useful Links -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], - }, - ], -}) -``` +- Need Help? Refer to our [GitHub repository](https://github.com/medusajs/medusa) for [issues](https://github.com/medusajs/medusa/issues) and [discussions](https://github.com/medusajs/medusa/discussions). +- [Join the community on Discord](https://discord.gg/medusajs). +- Have questions or need more support? Contact our [sales team](https://medusajs.com/contact/). +- Facing issues in your development? Refer to our [troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md). -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`. -*** +# Worker Mode of Medusa Instance -## Custom Actor Types +In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. +## What is Worker Mode? -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. +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. -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). +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. -# Emailpass Auth Module Provider +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. -In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. +This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. -Using the Emailpass auth module provider, you allow users to register and login with an email and password. +![Medusa worker mode architecture diagram illustrating the separation of responsibilities: the server instance handling HTTP requests, API calls, and real-time operations while the dedicated worker instance processes background tasks like data imports, email sending, and resource-intensive operations to maintain optimal server performance](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) *** -## Register the Emailpass Auth Module Provider +## How to Set Worker Mode -The Emailpass auth provider is registered by default with the Auth Module. +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: -If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: +- `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. -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +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", + // ... + }, // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // options... - }, - }, - ], - }, - }, - ], }) ``` -### Module Options - -|Configuration|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`hashConfig\`|An object of configurations to use when hashing the user's -password. Refer to |No|\`\`\`ts -const hashConfig = \{ - logN: 15, - r: 8, - p: 1 -} -\`\`\`| - -*** - -## Related Guides - -- [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) - - -# GitHub Auth Module Provider +You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: -The Github Auth Module Provider authenticates users with their GitHub account. +### Server Medusa Instance -Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +```bash +WORKER_MODE=server +``` -*** +### Worker Medusa Instance -## Register the Github Auth Module Provider +```bash +WORKER_MODE=worker +``` -### Prerequisites +### Disable Admin in Worker Mode -- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) -- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) +Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. -Add the module to the array of providers passed to the Auth Module: +To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: ```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" - -// ... - module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false, + }, // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-github", - id: "github", - options: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: process.env.GITHUB_CALLBACK_URL, - }, - }, - ], - }, - }, - ], }) ``` -### Environment Variables +Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. -Make sure to add the necessary environment variables for the above options in `.env`: +Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: -```plain -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_CALLBACK_URL= +### Server Medusa Instance + +```bash +ADMIN_DISABLED=false ``` -### Module Options +### Worker Medusa Instance -|Configuration|Description|Required| -|---|---|---|---|---| -|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| -|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| +```bash +ADMIN_DISABLED=true +``` -*** -## Override Callback URL During Authentication +# Translate Medusa Admin -In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. +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. -The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). +{/* 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. -## Examples +{/* vale docs.We = YES */} -- [How to implement third-party / 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). +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). +*** -# Google Auth Module Provider +## How to Contribute Translation -In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. +1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: -The Google Auth Module Provider authenticates users with their Google account. +```bash +git clone https://github.com/medusajs/medusa.git +``` -Learn about the authentication flow for third-party providers in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md). +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: -## Register the Google Auth Module Provider +```bash +yarn install +``` -### Prerequisites +3. Create a branch that you'll use to open the pull request later: -- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) -- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) +```bash +git checkout -b feat/translate- +``` -Add the module to the array of providers passed to the Auth Module: +Where `` is your language name. For example, `feat/translate-da`. -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +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: -module.exports = defineConfig({ - // ... - modules: [ - { - // ... - [Modules.AUTH]: { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-google", - id: "google", - options: { - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: process.env.GOOGLE_CALLBACK_URL, - }, - }, - ], - }, - }, - }, - ], -}) +```bash title="packages/admin/dashboard" +yarn i18n:validate da.json ``` -### Environment Variables +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: -Make sure to add the necessary environment variables for the above options in `.env`: +```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} +// other imports... +import da from "./da.json" -```plain -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -GOOGLE_CALLBACK_URL= +export default { + // other languages... + da: { + translation: da, + }, +} ``` -### Module Options +The language's key in the object is the ISO-2 name of the language. -|Configuration|Description|Required| -|---|---|---|---|---| -|\`clientId\`|A string indicating the |Yes| -|\`clientSecret\`|A string indicating the |Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| +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, + }, +] +``` -## Override Callback URL During Authentication +`languages` is an array having the following properties: -In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. +- `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. -The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). - -*** - -## Examples - -- [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). +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. -# Auth Module Provider -In this guide, you’ll learn about the Auth Module Provider and how it's used. +# Docs Contribution Guidelines -## What is an Auth Module Provider? +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. -An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +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). -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. +## What Can You Contribute? -### Auth Providers List +You can contribute to the Medusa documentation in the following ways: -- [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) +- 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. *** -## How to Create an Auth Module Provider? +## Documentation Workspace -An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`. +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 module can have multiple auth provider services, where each is registered as a separate auth provider. +The workspace has the following two directories: -Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider. +- `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. -*** +### Setup the Documentation Workspace -## Configure Allowed Auth Providers of Actor Types +### Prerequisites -By default, users of all actor types can authenticate with all installed Auth Module Providers. +- [Node.js v20+](https://nodejs.org/en/download) +- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install) -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: +In the `www` directory, run the following command to install the dependencies: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], - }, - // ... - }, - // ... - }, -}) +```bash +yarn install ``` -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. +Then, run the following command to build packages under the `www/packages` directory: +```bash +yarn build +``` -# How to Use Authentication Routes +After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server. -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. +## Documentation Content -## Types of Authentication Flows +All documentation projects are built with Next.js. The content is writtin in MDX files. -### 1. Basic Authentication Flow +### Medusa Main Docs Content -This authentication flow doesn't require validation with third-party services. +The content of the Medusa main docs are under the `www/apps/book/app` directory. -[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). +### Medusa Resources Content -The steps are: +The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. -![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) +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. -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). +### API Reference -After registration, you only use the [Auth Route](#login-route) for subsequent authentication. +The API reference's content is split into two types: -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). +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. -### 2. Third-Party Service Authenticate Flow +### Medusa UI Documentation -This authentication flow authenticates the user with a third-party service, such as Google. +The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. -[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). +The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. -It requires the following steps: +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. -![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. +## 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). *** -## Register Route +## How to Contribute -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. +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). -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` +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. -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. +### Base Branch -For example, if you're registering a customer, you: +When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. -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). +Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. -### Path Parameters +### Branch Name -Its path parameters are: +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/`. -- `{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`. +### Pull Request Conventions -### Request Body Parameters +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`. -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +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”. -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +*** -### Response Fields +## Images -If the authentication is successful, you'll receive a `token` field in the response body object: +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. -```json -{ - "token": "..." -} -``` +*** -Use that token in the header of subsequent requests to send authenticated requests. +## NPM and Yarn Code Blocks -### Handling Existing Identities +If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. -An auth identity with the same email may already exist in Medusa. This can happen if: +For example: -- 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. +````md +```bash npm2yarn +npm run start +``` +```` -In these scenarios, the Register Route will return an error instead of a token: +The code snippet must be written using NPM. -```json -{ - "type": "unauthorized", - "message": "Identity with email already exists" -} +### 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 ``` -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: +## Linting with Vale -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` +Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. -You can show that error message to the customer. +### 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. -## Login Route +### Run Vale Locally -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. +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 -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' +cd www/vale ``` -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. - -### Path Parameters +3\. Running the `run-vale` script: -Its path parameters are: +```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 +``` -- `{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`. +{/* TODO need to enable MDX v1 comments first. */} -### Request Body Parameters +{/* ### Linter Exceptions -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +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: -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +```md + -#### Overriding Callback URL +content that shouldn't be scanned for errors here... -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. +You can also disable specific rules. For example: -### Response Fields +```md + -If the authentication is successful, you'll receive a `token` field in the response body object: +Medusa supports Node versions 14 and 16. -```json -{ - "token": "..." -} + ``` -Use that token in the header of subsequent requests to send authenticated requests. +If you use this in your PR, you must justify its usage. */} -If the authentication requires more action with a third-party service, you'll receive a `location` property: +*** -```json -{ - "location": "https://..." -} -``` +## Linting with ESLint -Redirect to that URL in the frontend to continue the authentication process with the third-party service. +Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. -[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). +### 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`. -## Validate Callback Route +If you want to check content ESLint errors locally and fix them, you can do that by: -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. +1\. Install the dependencies in the `www` directory: ```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +yarn install ``` -Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. +2\. Run the turbo command in the `www` directory: -### Path Parameters +```bash +turbo run lint:content +``` -Its path parameters are: +This will fix any fixable errors, and show errors that require your action. -- `{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`. +### Linting Code with ESLint -### Query Parameters +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. -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. +If you want to check code ESLint errors locally and fix them, you can do that by: -### Response Fields +1\. Install the dependencies in the `www` directory: -If the authentication is successful, you'll receive a `token` field in the response body object: +```bash +yarn install +``` -```json -{ - "token": "..." -} +2\. Run the turbo command in the `www` directory: + +```bash +yarn lint ``` -In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): +This will fix any fixable errors, and show errors that require your action. -- 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). +{/* TODO need to enable MDX v1 comments first. */} -*** +{/* ### ESLint Exceptions -## Refresh Token Route +If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: -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. +~~~md + -It requires the user's JWT token that they received from the authentication or callback routes. +```js +console.log("This block isn't linted") +``` -```bash -curl -X POST http://localhost:9000/auth/token/refresh \ --H 'Authorization: Bearer {token}' +```js +console.log("This block is linted") ``` +~~~ -### Response Fields +You can also disable specific rules. For example: -If the token was refreshed successfully, you'll receive a `token` field in the response body object: +~~~md + -```json -{ - "token": "..." -} +```js +console.log("This block can use semicolons"); ``` -Use that token in the header of subsequent requests to send authenticated requests. +```js +console.log("This block can't use semi colons") +``` +~~~ */} -*** -## Reset Password Routes +# Usage Information -To reset a user's password: +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. -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) +## Purpose -### Generate Reset Password Token Route +As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa. -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. +We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions. -```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" -}' -``` +However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production. -This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. +Collecting this data allows us to understand certain details such as: -#### Path Parameters +- What operating system do most Medusa developers use? +- What version of Medusa is widely used? +- What parts of the Medusa Admin are generally undiscovered by our users? +- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data? +- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support? -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`. +## Medusa Application Analytics -#### Request Body Parameters +This section covers which data in the Medusa application are collected and how to opt out of it. -This route accepts in the request body an object having the following property: +### Collected Data in the Medusa Application -- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. +The following data is being collected on your Medusa application: -#### Response Fields +- Unique project ID generated with UUID. +- Unique machine ID generated with UUID. +- Operating system information including Node version or operating system platform used. +- The version of the Medusa application and Medusa CLI are used. -If the authentication is successful, the request returns a `201` response code. +Data is only collected when the Medusa application is run with the command `medusa start`. -### Reset Password Route +### How to Opt Out -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. +If you prefer to disable data collection, you can do it either by setting the following environment variable to true: ```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" -}' +MEDUSA_DISABLE_TELEMETRY=true ``` -This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. +Or, you can run the following command in the root of your Medusa application project to disable it: -#### Path Parameters +```bash +npx medusa telemetry --disable +``` -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`. +## Admin Analytics -#### Pass Token in Authorization Header +This section covers which data in the admin are collected and how to opt out of it. -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. +### Collected Data in Admin -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. +Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data. -### Request Body Parameters +The following data is being collected on your admin: -This route accepts in the request body an object that has the data necessary for the provider to update the user's password. +- The name of the store. +- The email of the user. +- The total number of products, orders, discounts, and users. +- The number of regions and their names. +- The currencies used in the store. +- Errors that occur while using the admin. -For the `emailpass` provider, you must pass the following properties: +### How to Enable Anonymization -- `email`: The user's email. -- `password`: The new password. +To enable anonymization of your data from the Medusa Admin: -### Response Fields +1. Go to Settings → Personal Information. +2. In the Usage insights section, click on the “Edit preferences” button. +3. Enable the "Anonymize my usage data” toggle. +4. Click on the “Submit and close” button. -If the authentication is successful, the request returns an object with a `success` property set to `true`: +### How to Opt-Out -```json -{ - "success": "true" -} +To opt out of analytics collection in the Medusa Admin, set the following environment variable: + +```bash +MEDUSA_FF_ANALYTICS=false ``` -# How to Create an Actor Type +# Storefront Development -In this document, learn how to create an actor type and authenticate its associated data model. +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. -## 0. Create Module with Data Model +You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. 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. -Before creating an actor type, you must have a module with a data model representing the actor type. +- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) +- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) -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: +## Passing a Publishable API Key in Storefront Requests -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" +When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), -}) +A publishable API key sets the scope of your request to one or more sales channels. -export default Manager -``` +Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. -*** +Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). -## 1. Create Workflow -Start by creating a workflow that does two things: +# Updating Medusa -- Creates a record of the `Manager` data model. -- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. +In this chapter, you'll learn about updating your Medusa application and packages. -For example, create the file `src/workflows/create-manager.ts`. with the following content: +Medusa's current version is v{config.version.number}. {releaseNoteText} -```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" +## Medusa Versioning -type CreateManagerWorkflowInput = { - manager: { - first_name: string - last_name: string - email: string - } - authIdentityId: string -} +When Medusa puts out a new release, all packages are updated to the same version. This ensures that all packages are compatible with each other, and makes it easier for you to switch between versions. -const createManagerStep = createStep( - "create-manager-step", - async ({ - manager: managerData, - }: Pick, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") +This doesn't apply to the design-system packages, including `@medusajs/ui`, `@medusajs/ui-presets`, and `@medusajs/ui-icons`. These packages are versioned independently. However, you don't need to install and manage them separately in your Medusa application, as they are included in the `@medusajs/admin-sdk`. If you're using them in a standalone project, such as a storefront or custom admin dashboard, refer to [this section in the Medusa UI documentation](https://docs.medusajs.com/ui/installation/standalone-project#updating-ui-packages/index.html.md) for update instructions. - const manager = await managerModuleService.createManager( - managerData - ) +Medusa updates the version number `major.minor.patch` according to the following rules: - return new StepResponse(manager) - } -) +- **patch**: A patch release includes bug fixes and minor improvements. It doesn't include breaking changes. For example, if the current version is `2.0.0`, the next patch release will be `2.0.1`. +- **minor**: A minor release includes new features, fixes, improvements, and breaking changes. For example, if the current version is `2.0.0`, the next minor release will be `2.1.0`. +- **major**: A major release includes significant changes to the entire codebase and architecture. For those, the update process will be more elaborate. For example, if the current version is `2.0.0`, the next major release would be `3.0.0`. -const createManagerWorkflow = createWorkflow( - "create-manager", - function (input: CreateManagerWorkflowInput) { - const manager = createManagerStep({ - manager: input.manager, - }) +*** - setAuthAppMetadataStep({ - authIdentityId: input.authIdentityId, - actorType: "manager", - value: manager.id, - }) +## Check Installed Version - return new WorkflowResponse(manager) - } -) +To check the currently installed version of Medusa in your project, run the following command in your Medusa application: -export default createManagerWorkflow +```bash +npx medusa -v ``` -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. +This will show you the installed version of Medusa and the [Medusa CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md), which should be the same. *** -## 2. Define the Create API Route +## Check Latest Version -Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. +The documentation shows the current version at the top right of the navigation bar. When a new version is released, you'll find a blue dot on the version number. Clicking it will take you to the [release notes on GitHub](https://github.com/medusajs/medusa/releases). -So, create the file `src/api/manager/route.ts` with the following content: +You can also star the [Medusa repository on GitHub](https://github.com/medusajs/medusa) to receive updates about new releases on your GitHub dashboard. Our team also shares updates on new releases on our social media channels. -```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 -} +## Update Medusa Application -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." - ) - } +Before updating a Medusa application, make sure to check the [release notes](https://github.com/medusajs/medusa/releases) for any breaking changes that require actions from your side. - const { result } = await createManagerWorkflow(req.scope) - .run({ - input: { - manager: req.body, - authIdentityId: req.auth_context.auth_identity_id, - }, - }) - - res.status(200).json({ manager: result }) -} +Then, to update your Medusa application, bump the version of all `@medusajs/*` dependencies in your `package.json`. Then, re-install dependencies: + +```bash npm2yarn +npm install ``` -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: +This will update all Medusa packages to the latest version. -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. +### Running Migrations -In the API route, you create the manager using the workflow from the previous section and return it in the response. +Releases may include changes to the database, such as new tables, updates to existing tables, updates after adding links, or data migration scripts. -*** +So, after updating Medusa, run the following command to migrate the latest changes to your database: -## 3. Apply the `authenticate` Middleware +```bash +npx medusa db:migrate +``` -The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. +This will run all pending migrations, sync links, and run data migration scripts. -To do that, create the file `src/api/middlewares.ts` with the following content: +### Reverting an Update -```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +Before reverting an update, if you already ran the migrations, you have to first identify the modules who had migrations. Then, before reverting, run the `db:rollback` command for each of those modules. -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager", - method: "POST", - middlewares: [ - authenticate("manager", ["session", "bearer"], { - allowUnregistered: true, - }), - ], - }, - { - matcher: "/manager/me*", - middlewares: [ - authenticate("manager", ["session", "bearer"]), - ], - }, - ], -}) -``` +For example, if the version you updated to had migrations for the Cart and Product Modules, run the following command: -This applies middlewares on two route patterns: +```bash +npx medusa db:rollback cart product +``` -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. +Then, revert the update by changing the version of all `@medusajs/*` dependencies in your `package.json` to the previous version and re-installing dependencies: -### Retrieve Manager API Route +```bash npm2yarn +npm install +``` -For example, create the file `src/api/manager/me/route.ts` with the following content: +Finally, run the migrations to sync link changes: -```ts title="src/api/manager/me/route.ts" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import ManagerModuleService from "../../../modules/manager/service" +```bash +npx medusa db:migrate +``` -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -): Promise { - const query = req.scope.resolve("query") - const managerId = req.auth_context?.actor_id +*** - const { data: [manager] } = await query.graph({ - entity: "manager", - fields: ["*"], - filters: { - id: managerId, - }, - }, { - throwIfKeyNotFound: true, - }) +## Understanding Codebase Changes - res.json({ manager }) -} -``` +In the Medusa codebase, our team uses the following [TSDoc](https://tsdoc.org/) tags to indicate changes made in the latest version for a specific piece of code: -This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. +- `@deprecated`: Indicates that a piece of code is deprecated and will be removed in a future version. The tag's message will include details on what to use instead. However, our updates are always backward-compatible, allowing you to update your codebase at your own pace. +- `@since`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version. *** -## Test Custom Actor Type Authentication Flow - -To authenticate managers: +## Update Plugin Project -1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: +If you have a Medusa plugin project, you only need to update its `@medusajs/*` dependencies in the `package.json` file to the latest version. Then, re-install dependencies: -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' +```bash npm2yarn +npm install ``` -Copy the returned token to use it in the next request. -2. Send a `POST` request to `/manager` to create a manager: +# API Key Concepts -```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" -}' -``` +In this guide, you’ll learn about the different types of API keys, their expiration and verification. -Replace `{token}` with the token returned in the previous step. +## API Key Types -3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: +There are two types of API keys: -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` +- `publishable`: A public key used in client applications, such as a storefront. + - This API key is useful for operations that do not require authentication, such as fetching product data or categories. +- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. + - This API key is useful for operations that require authentication, such as creating orders or managing products as an admin user. -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: +The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). -```bash -curl 'http://localhost:9000/manager/me' \ --H 'Authorization: Bearer {token}' -``` +### Default Scopes and Permissions -Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. +In your Medusa application, a `publishable` API key is only useful to send requests to the [Store API routes](https://docs.medusajs.com/api/store). Learn more about it in the [Publishable API Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) guide. + +In addition, a `secret` API key allows you to access the [Admin API routes](https://docs.medusajs.com/api/admin) and perform actions as the admin user that the key was created for. The `created_by` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md) indicates the ID of the associated admin user. *** -## Delete User of Actor Type +## API Key Creation -When you delete a user of the actor type, you must update its auth identity to remove the association to the user. +When using the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) or [API routes](https://docs.medusajs.com/api/admin#api-keys), only admin users can create API keys. -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: +You can also create API keys in your customizations using the [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md). -```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 -} +## API Key Tokens -const deleteManagerStep = createStep( - "delete-manager-step", - async ( - { id }: DeleteManagerWorkflow, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") +The API key data model has a `token` property that contains the actual key used for authentication. - const manager = await managerModuleService.retrieve(id) +This token is created using the `salt` property in the data model, which is a random string generated when the API key is created. The salt is a `64`-character hexadecimal string generated randomly using the `crypto` module in Node.js. - await managerModuleService.deleteManagers(id) +For display purposes, the API key data model also has a `redacted` property that contains the first six characters of the token, followed by `...`, then the last three characters of the token. You can use this property to show the API key in the UI without revealing the full token. - return new StepResponse(undefined, { manager }) - }, - async ({ manager }, { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") +*** - await managerModuleService.createManagers(manager) - } - ) -``` +## API Key Expiration -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. +An API key expires when it’s revoked using the [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md). This method will set the following properties in the API key: -Next, in the same file, add the workflow that deletes a manager: +- `revoked_at`: The date and time when the API key was revoked. +- `revoked_by`: The ID of the user who revoked the API key. -```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" +The associated token is no longer usable or verifiable. -// ... +*** -export const deleteManagerWorkflow = createWorkflow( - "delete-manager", - ( - input: WorkflowData - ): WorkflowResponse => { - deleteManagerStep(input) +## Token Verification - 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, - }, - }, - }) +To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. - const authIdentity = transform( - { authIdentities }, - ({ authIdentities }) => { - const authIdentity = authIdentities[0] - if (!authIdentity) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Auth identity not found" - ) - } +# Links between API Key Module and Other Modules - return authIdentity - } - ) +This document showcases the module links defined between the API Key Module and other Commerce Modules. - setAuthAppMetadataStep({ - authIdentityId: authIdentity.id, - actorType: "manager", - value: null, - }) +## Summary - return new WorkflowResponse(input.id) - } -) -``` +The API Key Module has the following links to other modules: -In the workflow, you: +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ApiKey|SalesChannel|Stored - many-to-many|Learn more| -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. +## Sales Channel Module +You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -# Auth Module Options +![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) -In this document, you'll learn about the options of the Auth Module. +This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. -## providers +Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). -The `providers` option is an array of auth module providers. +### Retrieve with Query -When the Medusa application starts, these providers are registered and can be used to handle authentication. +To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: -By default, the `emailpass` provider is registered to authenticate customers and admin users. +### query.graph -For example: +```ts +const { data: apiKeys } = await query.graph({ + entity: "api_key", + fields: [ + "sales_channels.*", + ], +}) -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +// apiKeys[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -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... - }, - }, - ], - }, - }, +const { data: apiKeys } = useQueryGraphStep({ + entity: "api_key", + fields: [ + "sales_channels.*", ], }) + +// apiKeys[0].sales_channels ``` -The `providers` option is an array of objects that accept the following properties: +### Manage with Link -- `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. +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 -## Auth CORS +```ts +import { Modules } from "@medusajs/framework/utils" -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`. +await link.create({ + [Modules.API_KEY]: { + publishable_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -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. +### createRemoteLinkStep -*** +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -## authMethodsPerActor Configuration +// ... -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. +createRemoteLinkStep({ + [Modules.API_KEY]: { + publishable_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -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). +# API Key Module -# Auth Module +In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. -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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard. -Medusa has auth related features available out-of-the-box through the Auth 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 Auth Module. +Medusa has API-key related features available out-of-the-box through the API Key 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 API Key Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Auth Features +## API Key Features -- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. -- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). -- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. -- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. +- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. +- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. +- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. +- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). *** -## How to Use the Auth Module +## How to Use the API Key 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. @@ -21205,67 +23442,42 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/authenticate-user.ts" highlights={highlights} +```ts title="src/workflows/create-api-key.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules, MedusaError } from "@medusajs/framework/utils" -import { MedusaRequest } from "@medusajs/framework/http" -import { AuthenticationInput } from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" -type Input = { - req: MedusaRequest -} +const createApiKeyStep = createStep( + "create-api-key", + async ({}, { container }) => { + const apiKeyModuleService = container.resolve(Modules.API_KEY) -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) + const apiKey = await apiKeyModuleService.createApiKeys({ + title: "Publishable API key", + type: "publishable", + created_by: "user_123", + }) - const { success, authIdentity, error } = await authModuleService - .authenticate( - "emailpass", - { - url: req.url, - headers: req.headers, - query: req.query, - body: req.body, - authScope: "admin", // or custom actor type - protocol: req.protocol, - } as AuthenticationInput - ) + return new StepResponse({ apiKey }, apiKey.id) + }, + async (apiKeyId, { container }) => { + const apiKeyModuleService = container.resolve(Modules.API_KEY) - if (!success) { - // incorrect authentication details - throw new MedusaError( - MedusaError.Types.UNAUTHORIZED, - error || "Incorrect authentication details" - ) - } + await apiKeyModuleService.deleteApiKeys([apiKeyId]) + } +) - return new StepResponse({ authIdentity }, authIdentity?.id) - }, - async (authIdentityId, { container }) => { - if (!authIdentityId) { - return - } - - const authModuleService = container.resolve(Modules.AUTH) - - await authModuleService.deleteAuthIdentities([authIdentityId]) - } -) - -export const authenticateUserWorkflow = createWorkflow( - "authenticate-user", - (input: Input) => { - const { authIdentity } = authenticateUserStep(input) +export const createApiKeyWorkflow = createWorkflow( + "create-api-key", + () => { + const { apiKey } = createApiKeyStep() return new WorkflowResponse({ - authIdentity, + apiKey, }) } ) @@ -21273,1399 +23485,1457 @@ export const authenticateUserWorkflow = createWorkflow( You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +### 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 { authenticateUserWorkflow } from "../../workflows/authenticate-user" +import { createApiKeyWorkflow } from "../../workflows/create-api-key" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) + const { result } = await createApiKeyWorkflow(req.scope) + .run() res.send(result) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Auth Module - -The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. +### 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 { createApiKeyWorkflow } from "../workflows/create-api-key" -## Providers +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createApiKeyWorkflow(container) + .run() -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. + console.log(result) +} -*** +export const config: SubscriberConfig = { + event: "user.created", +} +``` +### Scheduled Job -# How to Handle Password Reset Token Event +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createApiKeyWorkflow } from "../workflows/create-api-key" -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). +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createApiKeyWorkflow(container) + .run() -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. + console.log(result) +} -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -### Prerequisites +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -- [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. +# Authentication Flows with the Auth Main Service -Create the file `src/subscribers/handle-reset.ts` with the following content: +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. -```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" +## Authentication Methods -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 - ) +### Register - const urlPrefix = actor_type === "customer" ? - "https://storefront.com" : - "https://admin.com/app" +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. - 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}`, - }, - }) -} +For example: -export const config: SubscriberConfig = { - event: "auth.password_reset", -} +```ts +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) ``` -You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: +This method calls the `register` method of the provider specified in the first parameter and returns its data. -- `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`. +### Authenticate -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). +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: -In the subscriber, you: +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` -- 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. +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. *** -## 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. +## Auth Flow 1: Basic Authentication -For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: +The basic authentication flow requires first using the `register` method, then the `authenticate` method: -```bash -curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "admin-test@gmail.com" -}' -``` +```ts +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) -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 (error) { + // registration failed + // TODO return an error + return +} -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: +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) -```plain -info: Processing auth.password_reset which has 1 subscribers +if (success && !location) { + // user is authenticated +} ``` -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 +If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. -- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) +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`. -# Cart Concepts +![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) -In this document, you’ll get an overview of the main concepts of a cart. +### Auth Identity with Same Identifier -## Shipping and Billing Addresses +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. -A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). +There are two ways to handle this: -![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) +- 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. *** -## Line Items +## Auth Flow 2: Third-Party Service Authentication -A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. +The third-party service authentication method requires using the `authenticate` method first: -A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) -In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). +if (location) { + // return the location for the front-end to redirect to +} -*** +if (!success) { + // authentication failed +} -## Shipping Methods +// authentication successful +``` -A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. +If the `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. -In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. -### data Property +![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) -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. +### Overriding Callback URL -If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. +The 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 `data` property is an object used to store custom data relevant later for fulfillment. +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + callback_url: "example.com", + } +) +``` +### validateCallback -# Links between Cart Module and Other Modules +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. -This document showcases the module links defined between the Cart Module and other Commerce Modules. +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). -## Summary +The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: -The Cart Module has the following links to other modules: +```ts +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +if (success) { + // authentication succeeded +} +``` -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|Customer|Read-only - has one|Learn more| -|Order|Cart|Stored - one-to-one|Learn more| -|Cart|PaymentCollection|Stored - one-to-one|Learn more| -|LineItem|Product|Read-only - has one|Learn more| -|LineItem|ProductVariant|Read-only - has one|Learn more| -|Cart|Promotion|Stored - many-to-many|Learn more| -|Cart|Region|Read-only - has one|Learn more| -|Cart|SalesChannel|Read-only - has one|Learn more| +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. -## Customer Module +![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) -Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +*** -### Retrieve with Query +## Reset Password -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`: +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. -### query.graph +For example: ```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) -// carts[0].customer +if (success) { + // password reset successfully +} ``` -### useQueryGraphStep +The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) -// carts[0].customer -``` +# Auth Identity and Actor Types -*** +In this document, you’ll learn about concepts related to identity and actors in the Auth Module. -## Order Module +## What is an Auth Identity? -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. +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`. -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. +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 an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) +*** -### Retrieve with Query +## Actor Types -To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: +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). -### query.graph +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. -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) +For example, an auth identity of a customer has the following `app_metadata` property: -// carts[0].order +```json +{ + "app_metadata": { + "customer_id": "cus_123" + } +} ``` -### useQueryGraphStep +The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Protect Routes by Actor Type -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", +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"]), + ], + }, ], }) - -// carts[0].order ``` -### Manage with Link +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`. -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +*** -### link.create +## Custom Actor Types -```ts -import { Modules } from "@medusajs/framework/utils" +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. -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` +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). -### createRemoteLinkStep -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +# 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. -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` +Using the Emailpass auth module provider, you allow users to register and login with an email and password. *** -## Payment Module - -The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. - -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. +## Register the Emailpass Auth Module Provider -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +The Emailpass auth provider is registered by default with the Auth Module. -### Retrieve with Query +If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: -To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" -### query.graph +// ... -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // options... + }, + }, + ], + }, + }, ], }) - -// carts[0].payment_collection ``` -### useQueryGraphStep +### Module Options -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +|Configuration|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`hashConfig\`|An object of configurations to use when hashing the user's +password. Refer to |No|\`\`\`ts +const hashConfig = \{ + logN: 15, + r: 8, + p: 1 +} +\`\`\`| -// ... +*** -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +## Related Guides -// carts[0].payment_collection -``` +- [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) -### Manage with Link -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +# GitHub Auth Module Provider -### link.create +In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. -```ts -import { Modules } from "@medusajs/framework/utils" +The Github Auth Module Provider authenticates users with their GitHub account. -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). *** -## Product Module - -Medusa defines read-only links between: +## Register the Github Auth Module Provider -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. +### Prerequisites -### Retrieve with Query +- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) +- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) -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`: +Add the module to the array of providers passed to the Auth Module: -To retrieve the product, pass `product.*` in `fields`. +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" -### query.graph +// ... -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-github", + id: "github", + options: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GITHUB_CALLBACK_URL, + }, + }, + ], + }, + }, ], }) +``` -// lineItems.variant +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= ``` -### useQueryGraphStep +### Module Options -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| +|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| -// ... +*** -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +## Override Callback URL During Authentication -// lineItems.variant -``` +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. + +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). *** -## Promotion Module +## Examples -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. +- [How to implement third-party / 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). -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) +# Google Auth Module Provider -Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. +In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. -### Retrieve with Query +The Google Auth Module Provider authenticates users with their Google account. -To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: +Learn about the authentication flow for third-party providers in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md). -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. +*** -### query.graph +## Register the Google Auth Module Provider -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) +### Prerequisites -// carts[0].promotions -``` +- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) +- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) -### useQueryGraphStep +Add the module to the array of providers passed to the Auth Module: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", +module.exports = defineConfig({ + // ... + modules: [ + { + // ... + [Modules.AUTH]: { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-google", + id: "google", + options: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackUrl: process.env.GOOGLE_CALLBACK_URL, + }, + }, + ], + }, + }, + }, ], }) - -// carts[0].promotions ``` -### Manage with Link +### Environment Variables -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Make sure to add the necessary environment variables for the above options in `.env`: -### link.create +```plain +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL= +``` -```ts -import { Modules } from "@medusajs/framework/utils" +### Module Options -// ... +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the |Yes| +|\`clientSecret\`|A string indicating the |Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| -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" +## Override Callback URL During Authentication -// ... +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). *** -## Region Module +## Examples -Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. +- [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). -### 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`: +# Auth Module Provider -### query.graph +In this guide, you’ll learn about the Auth Module Provider and how it's used. -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) +## What is an Auth Module Provider? -// carts[0].region -``` +An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### 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) -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) +*** -// carts[0].region -``` +## How to Create an Auth Module Provider? -*** +An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`. -## Sales Channel Module +The module can have multiple auth provider services, where each is registered as a separate auth provider. -Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider. -### 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`: +## Configure Allowed Auth Providers of Actor Types -### query.graph +By default, users of all actor types can authenticate with all installed Auth Module Providers. -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +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: -// carts[0].sales_channel +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, +}) ``` -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -// ... +# How to Use Authentication Routes -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +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. -// carts[0].sales_channel -``` +These routes are added by Medusa's HTTP layer, not the Auth Module. +## Types of Authentication Flows -# Cart Module +### 1. Basic Authentication Flow -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. +This authentication flow doesn't require validation with third-party services. -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. +[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). -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +The steps are: -## Cart Features +![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) -- [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. +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. -## How to Use the Cart Module +To handle errors related to existing identities, refer to [this section](#handling-existing-identities). -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. +### 2. Third-Party Service Authenticate Flow -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. +This authentication flow authenticates the user with a third-party service, such as Google. -For example: +[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). -```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" +It requires the following steps: -const createCartStep = createStep( - "create-cart", - async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) +![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) - 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, - }, - ], - }) +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. - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) +*** - await cartModuleService.deleteCarts([cartId]) - } -) +## Register Route -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() +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. - return new WorkflowResponse({ - cart, - }) - } -) +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +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. -### API Route +For example, if you're registering a customer, you: -```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" +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). -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(req.scope) - .run() +### Path Parameters - res.send(result) -} -``` +Its path parameters are: -### Subscriber +- `{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`. -```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" +### Request Body Parameters -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - console.log(result) -} +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -export const config: SubscriberConfig = { - event: "user.created", +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." } ``` -### Scheduled Job +Use that token in the header of subsequent requests to send authenticated requests. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" +### Handling Existing Identities -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() +An auth identity with the same email may already exist in Medusa. This can happen if: - console.log(result) +- 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" } +``` -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, +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" } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +You can show that error message to the customer. *** +## Login Route -# Promotions Adjustments in Carts +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. -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` -## What are Adjustment Lines? +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. -An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. +### Path Parameters -The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. +Its path parameters are: -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) +- `{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`. -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +### Request Body Parameters -*** +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. -## Discountable Option +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. +#### Overriding Callback URL -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. +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. -## Promotion Actions +### Response Fields -When using the Cart and Promotion modules together, such as in the Medusa application, 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. +If the authentication is successful, you'll receive a `token` field in the response body object: -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). +```json +{ + "token": "..." +} +``` -For example: +Use that token in the header of subsequent requests to send authenticated requests. -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +If the authentication requires more action with a third-party service, you'll receive a `location` property: -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) +```json +{ + "location": "https://..." +} +``` -// retrieve line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -cart.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - adjustments: filteredAdjustments, - }) - } -}) +Redirect to that URL in the frontend to continue the authentication process with the third-party service. -// retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -cart.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, - }) - } -}) +[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). -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) -``` +*** -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. +## Validate Callback Route -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. +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. -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" +```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. -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) +### Path Parameters -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` +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`. -# Tax Lines in Cart Module +### Query Parameters -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. +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. -## What are Tax Lines? +### Response Fields -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. +If the authentication is successful, you'll receive a `token` field in the response body object: -![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) +```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). *** -## Tax Inclusivity +## Refresh Token Route -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. +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. -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. +It requires the user's JWT token that they received from the authentication or callback routes. -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. +```bash +curl -X POST http://localhost:9000/auth/token/refresh \ +-H 'Authorization: Bearer {token}' +``` -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. +### Response Fields -![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) +If the token was refreshed successfully, you'll receive a `token` field in the response body object: -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`. +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. *** -## Retrieve Tax Lines +## Reset Password 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. +To reset a user's password: -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) +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. -// 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", - }, - } -) -``` +[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) -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: +### Generate Reset Password Token Route -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) +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. -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) +```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. -# Links between Currency Module and Other Modules +#### Path Parameters -This document showcases the module links defined between the Currency Module and other Commerce Modules. +Its path parameters are: -## Summary +- `{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`. -The Currency Module has the following links to other modules: +#### Request Body Parameters -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +This route accepts in the request body an object having the following property: -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|StoreCurrency|Currency|Read-only - has one|Learn more| +- `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 -## Store Module +If the authentication is successful, the request returns a `201` response code. -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. +### Reset Password Route -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. +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. -### Retrieve with Query +```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" +}' +``` -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`: +This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. -### query.graph +#### Path Parameters -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +Its path parameters are: -// stores[0].supported_currencies[0].currency -``` +- `{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`. -### useQueryGraphStep +#### Pass Token in Authorization Header -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +### Request Body Parameters -// stores[0].supported_currencies[0].currency +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" +} ``` -# Currency Module +# How to Create an Actor Type -In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. +In this document, learn how to create an actor type and authenticate its associated data model. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. +## 0. Create Module with Data Model -Medusa has currency related features available out-of-the-box through the Currency 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 Currency Module. +Before creating an actor type, you must have a module with a data model representing the actor type. -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). -## Currency Features +The rest of this guide uses this `Manager` data model as an example: -- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. -- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. +```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 +``` *** -## How to Use the Currency Module +## 1. Create Workflow -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. +Start by creating a workflow that does two things: -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. +- 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: +For example, create the file `src/workflows/create-manager.ts`. with the following content: -```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} +```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} import { createWorkflow, - WorkflowResponse, createStep, StepResponse, - transform, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +import { + setAuthAppMetadataStep, +} from "@medusajs/medusa/core-flows" +import ManagerModuleService from "../modules/manager/service" -const retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) +type CreateManagerWorkflowInput = { + manager: { + first_name: string + last_name: string + email: string + } + authIdentityId: string +} - const currency = await currencyModuleService - .retrieveCurrency("usd") +const createManagerStep = createStep( + "create-manager-step", + async ({ + manager: managerData, + }: Pick, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") - return new StepResponse({ currency }) + const manager = await managerModuleService.createManager( + managerData + ) + + return new StepResponse(manager) } ) -type Input = { - price: number -} - -export const retrievePriceWithCurrency = createWorkflow( - "create-currency", - (input: Input) => { - const { currency } = retrieveCurrencyStep() - - const formattedPrice = transform({ - input, - currency, - }, (data) => { - return `${data.currency.symbol}${data.input.price}` +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, }) - return new WorkflowResponse({ - formattedPrice, + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, }) + + return new WorkflowResponse(manager) } ) -``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +export default createManagerWorkflow +``` -### API Route +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. -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" +The workflow has two steps: -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) +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. - res.send(result) -} -``` +*** -### Subscriber +## 2. Define the Create API Route -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" +Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) +So, create the file `src/api/manager/route.ts` with the following content: - console.log(result) -} +```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" -export const config: SubscriberConfig = { - event: "user.created", +type RequestBody = { + first_name: string + last_name: string + email: string } -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" -export default async function myCustomJob( - container: MedusaContainer +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse ) { - const { result } = await retrievePriceWithCurrency(container) + // 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({ - price: 10, + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, }) - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, + + res.status(200).json({ manager: result }) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# 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. +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: -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +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. -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`. +In the API route, you create the manager using the workflow from the previous section and return it in the response. *** -## 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. +## 3. Apply the `authenticate` Middleware -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. +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: -# Links between Customer Module and Other Modules - -This document showcases the module links defined between the Customer Module and other Commerce Modules. +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -## Summary +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], + }, + { + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], + }, + ], +}) +``` -The Customer Module has the following links to other modules: +This applies middlewares on two route patterns: -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +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. -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Customer|AccountHolder|Stored - many-to-many|Learn more| -|Cart|Customer|Read-only - has one|Learn more| -|Order|Customer|Read-only - has one|Learn more| +### Retrieve Manager API Route -*** +For example, create the file `src/api/manager/me/route.ts` with the following content: -## Payment Module +```ts title="src/api/manager/me/route.ts" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import ManagerModuleService from "../../../modules/manager/service" -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. +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +): Promise { + const query = req.scope.resolve("query") + const managerId = req.auth_context?.actor_id -This link is available starting from Medusa `v2.5.0`. + const { data: [manager] } = await query.graph({ + entity: "manager", + fields: ["*"], + filters: { + id: managerId, + }, + }, { + throwIfKeyNotFound: true, + }) -### Retrieve with Query + res.json({ manager }) +} +``` -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`: +This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. -### query.graph +*** -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) +## Test Custom Actor Type Authentication Flow -// customers[0].account_holder_link?.[0]?.account_holder -``` +To authenticate managers: -### useQueryGraphStep +1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```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. -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) +2. Send a `POST` request to `/manager` to create a manager: -// customers[0].account_holder_link?.[0]?.account_holder +```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" +}' ``` -### 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): +Replace `{token}` with the token returned in the previous step. -### link.create +3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: -```ts -import { Modules } from "@medusajs/framework/utils" +```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: -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) +```bash +curl 'http://localhost:9000/manager/me' \ +-H 'Authorization: Bearer {token}' ``` -### createRemoteLinkStep +Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Delete User of Actor Type -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` +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: -## Cart Module +```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" -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. +export type DeleteManagerWorkflow = { + id: string +} -### Retrieve with Query +const deleteManagerStep = createStep( + "delete-manager-step", + async ( + { id }: DeleteManagerWorkflow, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") -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`: + const manager = await managerModuleService.retrieve(id) -### query.graph + await managerModuleService.deleteManagers(id) -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) + return new StepResponse(undefined, { manager }) + }, + async ({ manager }, { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") -// carts.customer + await managerModuleService.createManagers(manager) + } + ) ``` -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +export const deleteManagerWorkflow = createWorkflow( + "delete-manager", + ( + input: WorkflowData + ): WorkflowResponse => { + deleteManagerStep(input) -// carts.customer + 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: -## Order Module +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. -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. +You can use this workflow when deleting a manager, such as in an API route. -### 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`: +# Auth Module Options -### query.graph +In this document, you'll learn about the options of the Auth Module. -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) +## providers -// orders.customer -``` +The `providers` option is an array of auth module providers. -### useQueryGraphStep +When the Medusa application starts, these providers are registered and can be used to handle authentication. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", +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... + }, + }, + ], + }, + }, ], }) - -// orders.customer ``` +The `providers` option is an array of objects that accept the following properties: -# Customer Module +- `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. -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. +## Auth CORS -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. +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). + + +# 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. + +Medusa has auth related features available out-of-the-box through the Auth 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 Auth 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 +## Auth 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). +- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. +- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). +- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. +- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. *** -## How to Use the Customer Module +## How to Use the Auth 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. @@ -22673,45 +24943,67 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-customer.ts" highlights={highlights} +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) +type Input = { + req: MedusaRequest +} - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) - return new StepResponse({ customer }, customer.id) + const { success, authIdentity, error } = await authModuleService + .authenticate( + "emailpass", + { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: "admin", // or custom actor type + protocol: req.protocol, + } as AuthenticationInput + ) + + if (!success) { + // incorrect authentication details + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Incorrect authentication details" + ) + } + + return new StepResponse({ authIdentity }, authIdentity?.id) }, - async (customerId, { container }) => { - if (!customerId) { + async (authIdentityId, { container }) => { + if (!authIdentityId) { return } - const customerModuleService = container.resolve(Modules.CUSTOMER) + + const authModuleService = container.resolve(Modules.AUTH) - await customerModuleService.deleteCustomers([customerId]) + await authModuleService.deleteAuthIdentities([authIdentityId]) } ) -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) return new WorkflowResponse({ - customer, + authIdentity, }) } ) @@ -22719,487 +25011,961 @@ export const createCustomerWorkflow = createWorkflow( 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" +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createCustomerWorkflow(req.scope) - .run() + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) 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" +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) - .run() +*** - console.log(result) -} +## Configure Auth Module -export const config: SubscriberConfig = { - event: "user.created", -} -``` +The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. -### Scheduled Job +*** -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" +## Providers -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(container) - .run() +Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - 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). +# Send Reset Password Email Notification -*** +In this guide, you'll learn how to handle the `auth.password_reset` event to send a reset password email (or other notification type) to users. +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. -# Fulfillment Concepts +## Reset Password Flow Overview -In this document, you’ll learn about some basic fulfillment concepts. +![Diagram showcasing the reset password flow detailed below](https://res.cloudinary.com/dza7lstvk/image/upload/v1754050032/Medusa%20Resources/reset-password_qheixj.jpg) -## Fulfillment Set +Users of any actor type (admin, customer, or custom actor type) can request to reset their password. The flow for resetting a password is as follows: -A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. +1. The user requests to reset their password either through the frontend (for example, [Medusa Admin](https://docs.medusajs.com/user-guide/reset-password/index.html.md)) or 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). +2. The Medusa application generates a password reset token and emits the `auth.password_reset` event. + - At this point, you can handle the event to send a notification to the user with instructions on how to reset their password. +3. The user receives the notification and clicks on the link to reset their password. + - The user can reset their password either through the frontend (for example, [Medusa Admin](https://docs.medusajs.com/user-guide/reset-password/index.html.md)) or the [Reset Password API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). -A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. +In this guide, you'll implement a subscriber that handles the `auth.password_reset` event to send an email notification to the user with instructions on how to reset their password. -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` +After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa Admin, storefront, or API routes. *** -## Service Zone +## Prerequisites: Notification Module Provider -A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. +To send an email or notification to the user, you must have a Notification Module Provider set up. -A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. +Medusa provides providers like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) and [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), and you can also [create your own custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). -![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) +Refer to the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) documentation for a list of available providers and how to set them up. -A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. +### Testing with the Local Notification Module Provider -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). +For testing purposes, you can use the [Local Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) by adding this to your `medusa-config.ts`: -*** +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-local", + id: "local", + options: { + channels: ["email"], + }, + }, + ], + }, + }, + ], +}) +``` -## Shipping Profile +The Local provider logs email details to your terminal instead of sending actual emails, which is useful for development and testing. -A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. +*** -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. +## Create the Reset Password Subscriber +To create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that handles the `auth.password_reset` event, create the file `src/subscribers/password-reset.ts` with the following content: -# Fulfillment Module Provider +```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" -In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. +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 config = container.resolve("configModule") -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + let urlPrefix = "" -## What is a Fulfillment Module Provider? + if (actor_type === "customer") { + urlPrefix = config.admin.storefrontUrl || "https://storefront.com" + } else { + const backendUrl = config.admin.backendUrl !== "/" ? config.admin.backendUrl : + "http://localhost:9000" + const adminPath = config.admin.path + urlPrefix = `${backendUrl}${adminPath}` + } -A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration. + await notificationModuleService.createNotifications({ + to: email, + channel: "email", + // TODO replace with template ID in notification provider + template: "password-reset", + data: { + // a URL to a frontend application + reset_url: `${urlPrefix}/reset-password?token=${token}&email=${email}`, + }, + }) +} -Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). +export const config: SubscriberConfig = { + event: "auth.password_reset", +} +``` -![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) +The subscriber receives the following data through the event payload: -*** +- `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`. -## Default Fulfillment Provider +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). -Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. +### Reset Password URL -This provider is installed by default in your application and you can use it to fulfill items manually. +Based on the user's actor type, you set the URL prefix to redirect the user to the appropriate frontend page to reset their password: -The identifier of the manual fulfillment provider is `fp_manual_manual`. +- If the user is a customer, you set the URL prefix to the storefront URL. +- If the user is an admin, you set the URL prefix to the backend URL, which is constructed from the `config.admin.backendUrl` and `config.admin.path` values. -*** +Note that the Medusa Admin has a reset password form at `/reset-password?token={token}&email={email}`. -## How to Create a Custom Fulfillment Provider? +### Notification Configurations -A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. +For the notification, you can configure the following fields: -The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. +- `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 of the email to send. This ID depends on the Notification Module provider you use. For example, if you use SendGrid, this would be the ID of the SendGrid template. + - Refer to the [Example Notification Templates](#example-notification-templates) section below for examples of notification templates to use. +- `data`: The data payload to pass to the template. You can pass additional fields, if necessary. -Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. +### Test It Out -{/* TODO add link to user guide */} +After you set up the Notification Module Provider, create a template in the provider, and create the subscriber, you can test the reset password flow. -After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. +Start the Medusa application with the following command: -*** +```bash npm2yarn +npm run dev +``` -## How are Fulfillment Providers Registered? +Then, open the Medusa Admin (locally at `http://localhost:9000/app`) and click on "Reset" in the login form. Then, enter the email of the user you want to reset the password for. -### Configure Fulfillment Module's Providers +Once you reset the password, you should see that the `auth.password_reset` event is emitted in the server's logs: -The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. +```bash +info: Processing auth.password_reset which has 1 subscribers +``` -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. +If you're using an email Notification Module Provider, check the user's email inbox for the reset password email with the link to reset their password. -### Registration on Application Start +If you're using the Local provider, check your terminal for the logged email details. -When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. +*** -For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. +## Next Steps: Implementing Frontend -### FulfillmentProvider Data Model +In your frontend, you must have a page that accepts `token` and `email` query parameters. -A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. +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). -This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. +The Medusa Admin already has a reset password page at `/reset-password?token={token}&email={email}`. So, you only need to implement this page in your storefront or custom admin dashboard. -![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) +### Examples -The `FulfillmentProvider` data model has the following properties: +- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. - - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. +*** -### How to Remove a Fulfillment Provider? +## Example Notification Templates -You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. +The following section provides example notification templates for some Notification Module Providers. -Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. +### SendGrid +Refer to the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) documentation for more details on how to set up SendGrid. -# Item Fulfillment +The following HTML template can be used with SendGrid to send a reset password email: -In this document, you’ll learn about the concepts of item fulfillment. +```html + + + + + + Reset Your Password + + + +
+
+

Reset Your Password

+
-## Fulfillment Data Model +
+

+ Hello{{#if email}} {{email}}{{/if}}, +

+

+ We received a request to reset your password. Click the button below to create a new password for your account. +

+
-A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + -*** +
+

+ Or copy and paste this URL into your browser: +

+ + {{reset_url}} + +
-## Fulfillment Processing by a Fulfillment Provider +
+

+ This password reset link will expire soon for security reasons. +

+

+ If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged. +

+
-A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +
+ + +``` -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. +Make sure to pass the `reset_url` variable to the template, which contains the URL to reset the password. -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) +You can also customize the template further to show other information. -*** +### Resend -## data Property +If you've integrated Resend as explained in the [Resend Integration Guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), you can add a new template for password reset emails at `src/modules/resend/emails/password-reset.tsx`: -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. +```tsx title="src/modules/resend/emails/password-reset.tsx" +import { + Text, + Container, + Heading, + Html, + Section, + Tailwind, + Head, + Preview, + Body, + Link, + Button, +} from "@react-email/components" -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. +type PasswordResetEmailProps = { + reset_url: string + email?: string +} -*** +function PasswordResetEmailComponent({ reset_url, email }: PasswordResetEmailProps) { + return ( + + + Reset your password + + + +
+ + Reset Your Password + +
-## Fulfillment Items +
+ + Hello{email ? ` ${email}` : ""}, + + + We received a request to reset your password. Click the button below to create a new password for your account. + +
-A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. +
+ +
-The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. +
+ + Or copy and paste this URL into your browser: + + + {reset_url} + +
-![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) +
+ + This password reset link will expire soon for security reasons. + + + If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged. + +
-*** +
+ + For security reasons, never share this reset link with anyone. If you're having trouble with the button above, copy and paste the URL into your web browser. + +
+
+ +
+ + ) +} -## Fulfillment Label +export const passwordResetEmail = (props: PasswordResetEmailProps) => ( + +) -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. +// Mock data for preview/development +const mockPasswordReset: PasswordResetEmailProps = { + reset_url: "https://your-app.com/reset-password?token=sample-reset-token-123", + email: "user@example.com", +} -*** +export default () => +``` -## Fulfillment Status +Feel free to customize the email template further to match your branding and style, or to add additional information. -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: +Then, in the Resend Module's service at `src/modules/resend/service.ts`, add the new template to the `templates` object and `Templates` type: -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. +```ts title="src/modules/resend/service.ts" +// other imports... +import { passwordResetEmail } from "./emails/password-reset" +enum Templates { + // ... + PASSWORD_RESET = "password-reset", +} -# Links between Fulfillment Module and Other Modules +const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { + // ... + [Templates.PASSWORD_RESET]: passwordResetEmail, +} +``` -This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. +Finally, find the `getTemplateSubject` function in the `ResendNotificationProviderService` and add a case for the `USER_INVITED` template: -## Summary +```ts title="src/modules/resend/service.ts" +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... -The Fulfillment Module has the following links to other modules: + private getTemplateSubject(template: Templates) { + // ... + switch (template) { + // ... + case Templates.PASSWORD_RESET: + return "Reset Your Password" + } + } +} +``` -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Order|Fulfillment|Stored - one-to-many|Learn more| -|Return|Fulfillment|Stored - one-to-many|Learn more| -|PriceSet|ShippingOption|Stored - many-to-one|Learn more| -|Product|ShippingProfile|Stored - many-to-one|Learn more| -|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| -|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| -*** +# Retrieve Cart Totals using Query -## Order Module +In this guide, you'll learn how to retrieve cart totals in your Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. +You may need to retrieve cart totals in your Medusa customizations, such as workflows or custom API routes, to perform custom actions with them. The ideal way to retrieve totals is with Query. -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. +Refer to the [Retrieve Cart Totals in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/cart/totals/index.html.md) guide for a storefront-specific approach. -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) +## How to Retrieve Cart Totals with Query -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +To retrieve cart totals, you mainly need to pass the `total` field within the `fields` option of the Query. This will return the cart's grand total, along with the totals of its line items and shipping methods. You can also pass additional total fields that you need for your use case. -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) +For example, to retrieve all totals of a cart: -### Retrieve with Query +Use `useQueryGraphStep` in workflows, and `query.graph` in custom API routes, scheduled jobs, and subscribers. -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: +### useQueryGraphStep -To retrieve the return, pass `return.*` in `fields`. +```ts highlights={[["12"]]} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "id", + "currency_code", + "total", + "subtotal", + "tax_total", + "discount_total", + "discount_subtotal", + "discount_tax_total", + "original_total", + "original_tax_total", + "item_total", + "item_subtotal", + "item_tax_total", + "original_item_total", + "original_item_subtotal", + "original_item_tax_total", + "shipping_total", + "shipping_subtotal", + "shipping_tax_total", + "original_shipping_tax_total", + "original_shipping_subtotal", + "original_shipping_total", + "credit_line_subtotal", + "credit_line_tax_total", + "credit_line_total", + "items.*", + "shipping_methods.*", + ], + filters: { + id: "cart_123", // Specify the cart ID + }, + }) + } +) +``` ### query.graph -```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", +```ts highlights={[["8"]]} +const query = container.resolve("query") // or req.scope.resolve in API routes + +const { data: [cart] } = await query.graph({ + entity: "cart", fields: [ - "order.*", + "id", + "currency_code", + "total", + "subtotal", + "tax_total", + "discount_total", + "discount_subtotal", + "discount_tax_total", + "original_total", + "original_tax_total", + "item_total", + "item_subtotal", + "item_tax_total", + "original_item_total", + "original_item_subtotal", + "original_item_tax_total", + "shipping_total", + "shipping_subtotal", + "shipping_tax_total", + "original_shipping_tax_total", + "original_shipping_subtotal", + "original_shipping_total", + "credit_line_subtotal", + "credit_line_tax_total", + "credit_line_total", + "items.*", + "shipping_methods.*", ], + filters: { + id: "cart_123", // Specify the cart ID + }, }) - -// fulfillments.order ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +The returned `cart` object will look like this: -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", - fields: [ - "order.*", +```json +{ + "id": "cart_123", + "currency_code": "usd", + "total": 10, + "subtotal": 10, + "tax_total": 0, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "original_total": 10, + "original_tax_total": 0, + "item_total": 10, + "item_subtotal": 10, + "item_tax_total": 0, + "original_item_total": 10, + "original_item_subtotal": 10, + "original_item_tax_total": 0, + "shipping_total": 10, + "shipping_subtotal": 10, + "shipping_tax_total": 0, + "original_shipping_tax_total": 0, + "original_shipping_subtotal": 10, + "original_shipping_total": 10, + "credit_line_subtotal": 0, + "credit_line_tax_total": 0, + "credit_line_total": 0, + "items": [ + { + "id": "cali_123", + // ... + "unit_price": 10, + "subtotal": 10, + "total": 0, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + } ], -}) - -// fulfillments.order + "shipping_methods": [ + { + "id": "casm_01K10AYZDKZGQXE8WXW3QP9T22", + // ... + "amount": 10, + "subtotal": 10, + "total": 10, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + } + ] +} ``` -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +### Cart Totals -### link.create +The cart will include the following total fields: -```ts -import { Modules } from "@medusajs/framework/utils" +- `total`: The grand total of the cart, including all line items, shipping methods, taxes, and discounts. +- `subtotal`: The cart's total excluding taxes, including promotions. +- `tax_total`: The total tax amount applied to the cart. +- `discount_total`: The total amount of discounts applied to the cart. +- `discount_subtotal`: The total amount of discounts applied to the cart's subtotal. +- `discount_tax_total`: The total amount of discounts applied to the cart's tax. +- `original_total`: The cart's total including taxes, excluding promotions. +- `original_tax_total`: The cart items' tax total including promotions. +- `item_total`: The cart items' total including taxes and promotions. +- `item_subtotal`: The cart items' total excluding taxes, including promotions. +- `item_tax_total`: The cart items' tax total including promotions. +- `original_item_total`: The cart items' total including taxes, excluding promotions. +- `original_item_subtotal`: The cart items' total excluding taxes, including promotions. +- `original_item_tax_total`: The cart items' tax total excluding promotions. +- `shipping_total`: The cart's shipping total including taxes and promotions. +- `shipping_subtotal`: The cart's shipping total excluding taxes, including promotions. +- `shipping_tax_total`: The total taxes applied to the cart's shipping amount. +- `original_shipping_tax_total`: The total taxes applied to the cart's shipping amount, excluding promotions. +- `original_shipping_subtotal`: The cart's shipping total excluding taxes, including promotions. +- `original_shipping_total`: The cart's shipping total including taxes, excluding promotions. +- `credit_line_subtotal`: The subtotal of the credit line applied to the cart. +- `credit_line_tax_total`: The total tax amount applied to the credit line. +- `credit_line_total`: The total amount of the credit line applied to the cart. -// ... +### Cart Line Item Totals -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` +The `items` array in the `cart` object contains total fields for each line item: -### createRemoteLinkStep +- `unit_price`: The price of a single unit of the line item. This field is not calculated and is stored in the database. +- `subtotal`: The total price of the line item before any discounts or taxes. +- `total`: The total price of the line item after applying discounts and taxes. +- `original_total`: The total price of the line item before any discounts. +- `discount_total`: The total amount of discounts applied to the line item. +- `discount_subtotal`: The total amount of discounts applied to the line item's subtotal. +- `discount_tax_total`: The total amount of discounts applied to the line item's tax. +- `tax_total`: The total tax amount applied to the line item. +- `original_tax_total`: The total tax amount applied to the line item before any discounts. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +### Cart Shipping Method Totals -// ... +The `shipping_methods` array in the `cart` object contains total fields for each shipping method: -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` +- `amount`: The amount charged for the shipping method. This field is not calculated and is stored in the database. +- `subtotal`: The total price of the shipping method before any discounts or taxes. +- `total`: The total price of the shipping method after applying discounts and taxes. +- `original_total`: The total price of the shipping method before any discounts. +- `discount_total`: The total amount of discounts applied to the shipping method. +- `discount_subtotal`: The total amount of discounts applied to the shipping method's subtotal. +- `discount_tax_total`: The total amount of discounts applied to the shipping method's tax. +- `tax_total`: The total tax amount applied to the shipping method. +- `original_tax_total`: The total tax amount applied to the shipping method before any discounts. *** -## Pricing Module - -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +## Caveats of Retrieving Cart Totals -![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) +### Using Asterisk (\*) in Cart's Query -### Retrieve with Query +Cart totals are calculated based on the cart's line items, shipping methods, taxes, and any discounts applied. They are not stored in the `Cart` data model. -To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +For that reason, you cannot retrieve cart totals by passing `*` to the Query's fields. For example, the following query will not return cart totals: -### query.graph +### useQueryGraphStep ```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: ["*"], + filters: { + id: "cart_123", + }, }) -// shippingOptions[0].price_set_link?.price_set_id +// carts don't include cart totals ``` -### useQueryGraphStep +### query.graph ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], +const { data: [cart] } = await query.graph({ + entity: "cart", + fields: ["*"], + filters: { + id: "cart_123", + }, }) -// shippingOptions[0].price_set_link?.price_set_id +// cart doesn't include cart totals ``` -### Manage with Link +This will return the cart data stored in the database, but not the calculated totals. -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +You also can't pass `*` along with `total` in the Query's fields option. Passing `*` will override the `total` field, and you will not retrieve cart totals. For example, the following query will not return cart totals: -### link.create +### useQueryGraphStep ```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: ["*", "total"], + filters: { + id: "cart_123", }, }) + +// carts don't include cart totals ``` -### createRemoteLinkStep +### query.graph ```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", +const { data: [cart] } = await query.graph({ + entity: "cart", + fields: ["*", "total"], + filters: { + id: "cart_123", }, }) + +// cart doesn't include cart totals ``` -*** +Instead, when you need to retrieve cart totals, explicitly pass the `total` field in the Query's fields option, as shown in [the previous section](#how-to-retrieve-cart-totals-with-query). You can also include other fields and relations you need, such as `email` and `items.*`. -## Product Module +### Applying Filters on Cart Totals -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. +You can't apply filters directly on the cart totals, as they are not stored in the `Cart` data model. You can still filter the cart based on its properties, such as `id`, `email`, or `currency_code`, but not on the calculated totals. -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). -### Retrieve with Query +# Cart Concepts -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +In this document, you’ll get an overview of the main concepts of a cart. -### query.graph +## Shipping and Billing Addresses -```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) +A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). -// shippingProfiles[0].products -``` +![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Line Items -// ... +A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) +A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. -// shippingProfiles[0].products -``` +In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). -### 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): +## Shipping Methods + +A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. + +In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. + +### data Property + +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. + +If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. + +The `data` property is an object used to store custom data relevant later for fulfillment. + + +# Links between Cart Module and Other Modules + +This document showcases the module links defined between the Cart Module and other Commerce Modules. + +## Summary + +The Cart 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| +|---|---|---|---| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Cart|Stored - one-to-one|Learn more| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|LineItem|Product|Read-only - has one|Learn more| +|LineItem|ProductVariant|Read-only - has one|Learn more| +|Cart|Promotion|Stored - many-to-many|Learn more| +|Cart|Region|Read-only - has one|Learn more| +|Cart|SalesChannel|Read-only - has one|Learn more| + +*** + +## Customer Module + +Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. + +### 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[0].customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts[0].customer +``` + +*** + +## Order Module + +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. + +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. + +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) + +### Retrieve with Query + +To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts[0].order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts[0].order +``` + +### Manage with Link + +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23209,11 +25975,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + [Modules.ORDER]: { + order_id: "order_123", }, }) ``` @@ -23227,46 +25993,123 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + [Modules.ORDER]: { + order_id: "order_123", }, }) ``` *** -## Stock Location Module +## Payment Module -The Stock Location Module provides features to manage stock locations in a store. +The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +### Retrieve with Query -![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) +To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts[0].payment_collection +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts[0].payment_collection +``` + +### Manage with Link + +To manage the payment collection 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.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines read-only links between: + +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. ### Retrieve with Query -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: +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 stock location of a fulfillment provider, pass `locations.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. ### query.graph ```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", +const { data: lineItems } = await query.graph({ + entity: "line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets[0].location +// lineItems.variant ``` ### useQueryGraphStep @@ -23276,19 +26119,67 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets[0].location +// lineItems.variant +``` + +*** + +## Promotion Module + +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. + +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. + +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. + +### Retrieve with Query + +To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts[0].promotions +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts[0].promotions ``` ### 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): +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23298,11 +26189,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -23316,85 +26207,114 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` +*** -# Fulfillment Module Options +## Region Module -In this document, you'll learn about the options of the Fulfillment Module. +Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. -## providers +### Retrieve with Query -The `providers` option is an array of fulfillment module providers. +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`: -When the Medusa application starts, these providers are registered and can be used to process fulfillments. +### query.graph -For example: +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +// carts[0].region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", ], }) + +// carts[0].region ``` -The `providers` option is an array of objects that accept the following properties: +*** -- `resolve`: A string indicating either 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. +## Sales Channel Module +Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. -# Fulfillment Module +### Retrieve with Query -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. +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`: -Refer to the Medusa Admin User Guide to learn how to use the dashboard to: +### query.graph -- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md). -- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md). +```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 +``` -Medusa has fulfillment related features available out-of-the-box through the Fulfillment 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 Fulfillment Module. + +# 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). -## Fulfillment Features +## Cart Features -- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. -- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. -- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. -- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. -- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies. +- [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 Fulfillment Module +## 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. @@ -23402,7 +26322,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-fulfillment.ts" highlights={highlights} +```ts title="src/workflows/create-cart.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -23411,50 +26331,45 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createFulfillmentStep = createStep( - "create-fulfillment", +const createCartStep = createStep( + "create-cart", async ({}, { container }) => { - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) + const cartModuleService = container.resolve(Modules.CART) - const fulfillment = await fulfillmentModuleService.createFulfillment({ - location_id: "loc_123", - provider_id: "webshipper", - delivery_address: { + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + shipping_address: { + address_1: "1512 Barataria Blvd", country_code: "us", - city: "Strongsville", - address_1: "18290 Royalton Rd", }, items: [ { title: "Shirt", - sku: "SHIRT", + unit_price: 1000, quantity: 1, - barcode: "123456", }, ], - labels: [], - order: {}, }) - return new StepResponse({ fulfillment }, fulfillment.id) + return new StepResponse({ cart }, cart.id) }, - async (fulfillmentId, { container }) => { - if (!fulfillmentId) { + async (cartId, { container }) => { + if (!cartId) { return } - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) + const cartModuleService = container.resolve(Modules.CART) - await fulfillmentModuleService.deleteFulfillment(fulfillmentId) + await cartModuleService.deleteCarts([cartId]) } ) -export const createFulfillmentWorkflow = createWorkflow( - "create-fulfillment", +export const createCartWorkflow = createWorkflow( + "create-cart", () => { - const { fulfillment } = createFulfillmentStep() + const { cart } = createCartStep() return new WorkflowResponse({ - fulfillment, + cart, }) } ) @@ -23469,13 +26384,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" +import { createCartWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createFulfillmentWorkflow(req.scope) + const { result } = await createCartWorkflow(req.scope) .run() res.send(result) @@ -23489,13 +26404,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" +import { createCartWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createFulfillmentWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -23510,12 +26425,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" +import { createCartWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createFulfillmentWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -23531,616 +26446,497 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure Fulfillment Module - -The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. - -*** +# Promotions Adjustments in Carts -# Shipping Option +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. -In this document, you’ll learn about shipping options and their rules. +## What are Adjustment Lines? -## What’s a Shipping Option? +An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. +The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. -When the customer places their order, they choose a shipping option to be used to fulfill their items. +![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. *** -## Service Zone Restrictions +## Discountable Option -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. +The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. +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. -![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) +*** -Service zones can be more restrictive, such as restricting to certain cities or province codes. +## Promotion Actions -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). +When using the Cart and Promotion modules together, such as in the Medusa application, 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. -![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) +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). -*** - -## Shipping Option Rules +For example: -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" -You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) + } +}) -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. -- `value`: One or more values. +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.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, + }) + } +}) -![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. -*** +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -## Shipping Profile and Types +// ... -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` -*** -## data Property +# Tax Lines in Cart Module -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. +## 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. -# Inventory Concepts +![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) -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +*** -## InventoryItem +## Tax Inclusivity -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. +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. -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. +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. -![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) +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -### Inventory Shipping Requirement +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. -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. +![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) -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). +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`. *** -## InventoryLevel +## Retrieve Tax Lines -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. +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. -It has three quantity-related properties: +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) -- `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. +// 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", + }, + } +) +``` -### Associated Location +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: -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. +```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) +) +``` -## 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. +# Links between Currency Module and Other Modules -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +This document showcases the module links defined between the Currency Module and other Commerce Modules. +## Summary -# Inventory Module in Medusa Flows +The Currency Module has the following links to other modules: -This document explains how the Inventory Module is used within the Medusa application's flows. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -## Product Variant Creation +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|StoreCurrency|Currency|Read-only - has one|Learn more| -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) +## Store Module -![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) +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. -## Add to Cart +### Retrieve with Query -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. +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`: -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +### query.graph -![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) +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -*** +// stores[0].supported_currencies[0].currency +``` -## Order Placed +### useQueryGraphStep -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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) +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -*** +// stores[0].supported_currencies[0].currency +``` -## 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: +# Currency Module -- 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. +In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. -![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) +Medusa has currency related features available out-of-the-box through the Currency 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 Currency Module. -*** +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Order Return +## Currency Features -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. +- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. +- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. -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) +## How to Use the Currency Module -### Dismissed Returned Items +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. -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +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: -# Inventory Kits +```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -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. +const retrieveCurrencyStep = createStep( + "retrieve-currency", + async ({}, { container }) => { + const currencyModuleService = container.resolve(Modules.CURRENCY) -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: + const currency = await currencyModuleService + .retrieveCurrency("usd") -- [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). + return new StepResponse({ currency }) + } +) -## What is an Inventory Kit? +type Input = { + price: number +} -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. +export const retrievePriceWithCurrency = createWorkflow( + "create-currency", + (input: Input) => { + const { currency } = retrieveCurrencyStep() -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. + const formattedPrice = transform({ + input, + currency, + }, (data) => { + return `${data.currency.symbol}${data.input.price}` + }) -Using inventory kits, you can implement use cases like: + return new WorkflowResponse({ + formattedPrice, + }) + } +) +``` -- [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. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -*** +### API Route -## Multi-Part Products +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) -To implement this in Medusa, you can: + res.send(result) +} +``` -- 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. +### Subscriber -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. +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" -![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) +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) -### Create Multi-Part Product + console.log(result) +} -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). +export const config: SubscriberConfig = { + event: "user.created", +} +``` -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: +### Scheduled Job -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" -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", - }, +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, }) - 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, - }, - ], - }, - ], - }, - }) + console.log(result) +} - // TODO create the product - } -) +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} ``` -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). +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/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" +# Customer Accounts -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) +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. - 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", - }, - ], - }, - }) - } -) -``` +## `has_account` Property -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. +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. -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). +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`. *** -## Bundled Products +## Email Uniqueness -While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products. +The above behavior means that two `Customer` records may exist with the same email address. However, the main difference is the `has_account` property's value. -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email address. -![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. +## Customer Deletion and Email Reuse -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. +When a merchant deletes a customer, the `Customer` record is soft-deleted, meaning it is not permanently removed from the database. -![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) +When using the Medusa Application with the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md), possible confusion may arise in the following scenarios: -### Create Bundled Product +1. An admin user is using the email address `john@example.com`, and a customer tries to register with the same email address. +2. An admin user has deleted a customer with the email address `jane@example.com`, and another customer tries to register with the same email address. -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. +In these and similar scenarios, the customer trying to register will receive an error message indicating that the email address is already in use: -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: +```json +{ + "type": "unauthorized", + "message": "Identity with email already exists" +} +``` -```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, - }, - }) +To resolve this, you can amend the registration flow to: - 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" }) - } -) -``` +1. Retrieve the login token of the existing identity with the same email address. +2. Use the login token when registering the new customer. This will not remove the existing identity but will allow the new customer to register with the same email address. -The bundled product has the same inventory items as those of the products part of the bundle. +You can learn more about how to implement this flow in the following guides: -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). +- [Conceptual guide on how to implement this flow with Medusa's authentication routes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#handling-existing-identities/index.html.md). +- [How-to guide on how to implement this in a storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). -# Links between Inventory Module and Other Modules +# Links between Customer 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 Customer Module and other Commerce Modules. ## Summary -The Inventory Module has the following links to other modules: +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| |---|---|---|---| -|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| -|InventoryLevel|StockLocation|Read-only - has many|Learn more| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Customer|Read-only - has one|Learn more| *** -## Product Module - -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. - -![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) +## Payment Module -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. +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. -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). +This link is available starting from Medusa `v2.5.0`. ### 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 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: inventoryItems } = await query.graph({ - entity: "inventory_item", +const { data: customers } = await query.graph({ + entity: "customer", fields: [ - "variants.*", + "account_holder_link.account_holder.*", ], }) -// inventoryItems[0].variants +// customers[0].account_holder_link?.[0]?.account_holder ``` ### useQueryGraphStep @@ -24150,19 +26946,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", +const { data: customers } = useQueryGraphStep({ + entity: "customer", fields: [ - "variants.*", + "account_holder_link.account_holder.*", ], }) -// inventoryItems[0].variants +// customers[0].account_holder_link?.[0]?.account_holder ``` ### 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 account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24172,11 +26968,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` @@ -24184,42 +26980,41 @@ await link.create({ ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` *** -## Stock Location Module +## Cart Module -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. +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 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`: +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: inventoryLevels } = await query.graph({ - entity: "inventory_level", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "stock_locations.*", + "customer.*", ], }) -// inventoryLevels[0].stock_locations +// carts.customer ``` ### useQueryGraphStep @@ -24229,38 +27024,75 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "stock_locations.*", + "customer.*", ], }) -// inventoryLevels[0].stock_locations +// carts.customer ``` +*** -# Inventory Module +## Order 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. +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. -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. +### Retrieve with Query -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. +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 +``` + + +# 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). -## Inventory Features +## Customer 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. +- [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 Inventory Module +## 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. @@ -24268,7 +27100,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +```ts title="src/workflows/create-customer.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -24277,36 +27109,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createInventoryItemStep = createStep( - "create-inventory-item", +const createCustomerStep = createStep( + "create-customer", async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const customerModuleService = container.resolve(Modules.CUSTOMER) - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", }) - return new StepResponse({ inventoryItem }, inventoryItem.id) + return new StepResponse({ customer }, customer.id) }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { + async (customerId, { container }) => { + if (!customerId) { return } - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const customerModuleService = container.resolve(Modules.CUSTOMER) - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + await customerModuleService.deleteCustomers([customerId]) } ) -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", +export const createCustomerWorkflow = createWorkflow( + "create-customer", () => { - const { inventoryItem } = createInventoryItemStep() + const { customer } = createCustomerStep() return new WorkflowResponse({ - inventoryItem, + customer, }) } ) @@ -24321,13 +27153,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../../workflows/create-customer" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createInventoryItemWorkflow(req.scope) + const { result } = await createCustomerWorkflow(req.scope) .run() res.send(result) @@ -24341,13 +27173,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -24362,12 +27194,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -24384,308 +27216,231 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Order Claim +# Fulfillment Concepts -In this document, you’ll learn about order claims. +In this document, you’ll learn about some basic fulfillment concepts. -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. +## Fulfillment Set -## What is a Claim? +A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. +A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` *** -## 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. +## Service Zone -*** +A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. -## Old and Replacement Items +A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. -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. +![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) -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). +A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. -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). +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). *** -## Claim Shipping Methods +## Shipping Profile -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). +A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. -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). +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. -*** -## Claim Refund +# Fulfillment Module Provider -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. +In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. -*** +## What is a Fulfillment Module Provider? -## How Claims Impact an Order’s Version +A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration. -When a claim is confirmed, the order’s version is incremented. +Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). +![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) -# Order Concepts +*** -In this document, you’ll learn about orders and related concepts +## Default Fulfillment Provider -## Order Items +Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. -The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. +This provider is installed by default in your application and you can use it to fulfill items manually. -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) +The identifier of the manual fulfillment provider is `fp_manual_manual`. -### Item’s Product Details +*** -The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. +## How to Create a Custom Fulfillment Provider? -*** +A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. -## Order’s Shipping Method +The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. -An order has one or more shipping methods used to handle item shipment. +Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. -Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). +{/* TODO add link to user guide */} -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) +After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. -### data Property +*** -When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. +## How are Fulfillment Providers Registered? -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. +### Configure Fulfillment Module's Providers -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. +The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. -*** +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. -## Order Totals +### Registration on Application Start -The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). +When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. -*** +For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. -## Order Payments +### FulfillmentProvider Data Model -Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. -An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. +This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. -Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). +![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) +The `FulfillmentProvider` data model has the following properties: -# Order Edit +- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. + - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. -In this document, you'll learn about order edits. +### How to Remove a Fulfillment Provider? -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. +You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. -## What is an Order Edit? +Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. -A merchant can edit an order to add new items or change the quantity of existing items in the order. -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). +# Item Fulfillment -The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. +In this document, you’ll learn about the concepts of item fulfillment. -In the case of an order edit, the `OrderChange`'s type is `edit`. +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). *** -## Add Items in an Order Edit +## Fulfillment Processing by a Fulfillment Provider -When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. -Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. -So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) *** -## Update Items in an Order Edit - -A merchant can update an existing item's quantity or price. - -This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. - -*** - -## Shipping Methods of New Items in the Edit - -Adding new items to the order requires adding shipping methods for those items. - -These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` - -*** - -## How Order Edits Impact an Order’s Version - -When an order edit is confirmed, the order’s version is incremented. - -*** - -## Payments and Refunds for Order Edit Changes - -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. - -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. +## data Property -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. *** -## Returned and New Items +## Fulfillment 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. +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. -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 fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. -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. +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) *** -## 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). +## Fulfillment Label -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). +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. *** -## 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). - -*** +## Fulfillment Status -## How Exchanges Impact an Order’s Version +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: -When an exchange is confirmed, the order’s version is incremented. +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. -# Links between Order Module and Other Modules +# Links between Fulfillment Module and Other Modules -This document showcases the module links defined between the Order Module and other Commerce Modules. +This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. ## Summary -The Order 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 Fulfillment Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|Order|Customer|Read-only - has one|Learn more| -|Order|Cart|Stored - one-to-one|Learn more| |Order|Fulfillment|Stored - one-to-many|Learn more| |Return|Fulfillment|Stored - one-to-many|Learn more| -|Order|PaymentCollection|Stored - one-to-many|Learn more| -|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| -|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| -|OrderLineItem|Product|Read-only - has many|Learn more| -|Order|Promotion|Stored - many-to-many|Learn more| -|Order|Region|Read-only - has one|Learn more| -|Order|SalesChannel|Read-only - has one|Learn more| +|PriceSet|ShippingOption|Stored - many-to-one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| +|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| *** -## Customer Module - -Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. - -### 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[0].customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders[0].customer -``` +## Order Module -*** +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. -## Cart Module +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. -The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) -Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) ### Retrieve with Query -To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: +To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +To retrieve the return, pass `return.*` in `fields`. ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", fields: [ - "cart.*", + "order.*", ], }) -// orders[0].cart +// fulfillments.order ``` ### useQueryGraphStep @@ -24695,19 +27450,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", fields: [ - "cart.*", + "order.*", ], }) -// orders[0].cart +// fulfillments.order ``` ### Manage with Link -To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24720,8 +27475,8 @@ await link.create({ [Modules.ORDER]: { order_id: "order_123", }, - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` @@ -24738,41 +27493,37 @@ createRemoteLinkStep({ [Modules.ORDER]: { order_id: "order_123", }, - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` *** -## Fulfillment Module - -A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. +## Pricing Module -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.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) ### Retrieve with Query -To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: - -To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. +To retrieve the price set of a shipping option 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: orders } = await query.graph({ - entity: "order", +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", fields: [ - "fulfillments.*", + "price_set_link.*", ], }) -// orders[0].fulfillments +// shippingOptions[0].price_set_link?.price_set_id ``` ### useQueryGraphStep @@ -24782,19 +27533,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", fields: [ - "fulfillments.*", + "price_set_link.*", ], }) -// orders[0].fulfillments +// shippingOptions[0].price_set_link?.price_set_id ``` ### Manage with Link -To manage the fulfillments of an order, 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 @@ -24804,11 +27555,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -24822,40 +27573,38 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` *** -## Payment Module - -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +## Product Module -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. -![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) +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 payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: +To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", fields: [ - "payment_collections.*", + "products.*", ], }) -// orders[0].payment_collections +// shippingProfiles[0].products ``` ### useQueryGraphStep @@ -24865,19 +27614,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", fields: [ - "payment_collections.*", + "products.*", ], }) -// orders[0].payment_collections +// shippingProfiles[0].products ``` ### Manage with Link -To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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 @@ -24887,11 +27636,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + product_id: "prod_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", }, }) ``` @@ -24905,83 +27654,46 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + product_id: "prod_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", }, }) ``` *** -## Product Module - -Medusa defines read-only links between: - -- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. -- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. - -### 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.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) +## Stock Location Module -// lineItems.variant -``` +The Stock Location Module provides features to manage stock locations in a store. -*** +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. -## Promotion Module +![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) -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +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 Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) +![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 promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: + +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", fields: [ - "promotion.*", + "location.*", ], }) -// orders[0].promotion +// fulfillmentSets[0].location ``` ### useQueryGraphStep @@ -24991,19 +27703,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", fields: [ - "promotion.*", + "location.*", ], }) -// orders[0].promotion +// fulfillmentSets[0].location ``` ### 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): +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 @@ -25013,11 +27725,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` @@ -25031,244 +27743,145 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` -*** - -## Region Module - -Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. -### Retrieve with Query +# Fulfillment Module Options -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`: +In this document, you'll learn about the options of the Fulfillment Module. -### query.graph +## providers -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) +The `providers` option is an array of fulfillment module providers. -// orders[0].region -``` +When the Medusa application starts, these providers are registered and can be used to process fulfillments. -### useQueryGraphStep +For example: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, ], }) - -// orders[0].region ``` -*** - -## Sales Channel Module +The `providers` option is an array of objects that accept the following properties: -Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. +- `resolve`: A string indicating either 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. -### 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`: +# Fulfillment Module -### query.graph +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. -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +Refer to the Medusa Admin User Guide to learn how to use the dashboard to: -// orders[0].sales_channel -``` +- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md). +- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md). -### useQueryGraphStep +Medusa has fulfillment related features available out-of-the-box through the Fulfillment 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 Fulfillment Module. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -// ... +## Fulfillment Features -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. +- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. +- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. +- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. +- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies. -// orders[0].sales_channel -``` +*** +## How to Use the Fulfillment Module -# Order Change +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. -In this document, you'll learn about the Order Change data model and possible actions in it. +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. -## OrderChange Data Model +For example: -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. +```ts title="src/workflows/create-fulfillment.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -Its `change_type` property indicates what the order change is created for: +const createFulfillmentStep = createStep( + "create-fulfillment", + async ({}, { container }) => { + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) -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). + const fulfillment = await fulfillmentModuleService.createFulfillment({ + location_id: "loc_123", + provider_id: "webshipper", + delivery_address: { + country_code: "us", + city: "Strongsville", + address_1: "18290 Royalton Rd", + }, + items: [ + { + title: "Shirt", + sku: "SHIRT", + quantity: 1, + barcode: "123456", + }, + ], + labels: [], + order: {}, + }) -Once the order change is confirmed, its changes are applied on the order. + return new StepResponse({ fulfillment }, fulfillment.id) + }, + async (fulfillmentId, { container }) => { + if (!fulfillmentId) { + return + } + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) -*** + await fulfillmentModuleService.deleteFulfillment(fulfillmentId) + } +) -## 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. - - -# 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() +export const createFulfillmentWorkflow = createWorkflow( + "create-fulfillment", + () => { + const { fulfillment } = createFulfillmentStep() return new WorkflowResponse({ - draftOrder, + fulfillment, }) } ) @@ -25283,13 +27896,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" +import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createDraftOrderWorkflow(req.scope) + const { result } = await createFulfillmentWorkflow(req.scope) .run() res.send(result) @@ -25303,13 +27916,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createFulfillmentWorkflow(container) .run() console.log(result) @@ -25324,12 +27937,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createFulfillmentWorkflow(container) .run() console.log(result) @@ -25345,560 +27958,616 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Fulfillment Module -# 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. +The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. *** -## 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 +# Shipping Option -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. +In this document, you’ll learn about shipping options and their rules. -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). +## What’s a Shipping Option? -```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. -// ... +When the customer places their order, they choose a shipping option to be used to fulfill their items. -// 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, - }) - } -}) +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). -//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", - } -) -``` +## Service Zone Restrictions -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" +![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) -// ... +Service zones can be more restrictive, such as restricting to certain cities or province codes. -await orderModuleService.setOrderLineItemAdjustments( - order.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). -await orderModuleService.setOrderShippingMethodAdjustments( - order.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` +![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) +*** -# Order Return +## Shipping Option Rules -In this document, you’ll learn about order returns. +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -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. +You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. -## What is a Return? +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: -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). +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. +- `value`: One or more values. -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. +![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. -Once the merchant receives the returned items, they mark the return as received. +![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) *** -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md). +## Shipping Profile and Types -The `ReturnItem` model has two properties storing the item's quantity: +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). -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. +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. *** -## 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](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -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. +## data Property -*** +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. -## Refund Payment +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. -The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return. +# Inventory Concepts -*** +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. -## Returns in Exchanges and Claims +## InventoryItem -When a merchant creates an exchange or a claim, it includes returning items from the customer. +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 `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. +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) -## How Returns Impact an Order’s Version +### Inventory Shipping Requirement -The order’s version is incremented when: +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. -1. A return is requested. -2. A return is marked as received. +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). +*** -# Tax Lines in Order Module +## InventoryLevel -In this document, you’ll learn about tax lines in an order. +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. -## What are Tax Lines? +It has three quantity-related properties: -A tax line indicates the tax rate of a line item or a shipping method. +- `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. -The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. +### Associated Location -![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) +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. *** -## 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 it 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 tax perspective. +## ReservationItem -![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) +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. -For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. -# Transactions +# Inventory Module in Medusa Flows -In this document, you’ll learn about an order’s transactions and its use. +This document explains how the Inventory Module is used within the Medusa application's flows. -## What is a Transaction? +## Product Variant Creation -A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +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. -The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. +![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) *** -## Checking Outstanding Amount - -The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. +## Add to Cart -```json -{ - "totals": { - "total": 30, - "subtotal": 30, - // ... - } -} -``` +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. -To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -|Condition|Result| -|---|---|---| -|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| -|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| -|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| +![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) *** -## Transaction Reference - -The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). +## Order Placed -The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. -- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. -- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. +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) -# Commerce Modules +*** -In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules. +## Order Fulfillment -A Commerce Module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: -A Commerce Module also defines data models, representing tables in the database. The Medusa Framework and tools allow you to extend these data models to add custom fields. +- 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. -## Commerce Modules List +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [API Key Module](https://docs.medusajs.com/commerce-modules/api-key/index.html.md) -- [Auth Module](https://docs.medusajs.com/commerce-modules/auth/index.html.md) -- [Cart Module](https://docs.medusajs.com/commerce-modules/cart/index.html.md) -- [Currency Module](https://docs.medusajs.com/commerce-modules/currency/index.html.md) -- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md) -- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md) -- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md) -- [Order Module](https://docs.medusajs.com/commerce-modules/order/index.html.md) -- [Payment Module](https://docs.medusajs.com/commerce-modules/payment/index.html.md) -- [Pricing Module](https://docs.medusajs.com/commerce-modules/pricing/index.html.md) -- [Product Module](https://docs.medusajs.com/commerce-modules/product/index.html.md) -- [Promotion Module](https://docs.medusajs.com/commerce-modules/promotion/index.html.md) -- [Region Module](https://docs.medusajs.com/commerce-modules/region/index.html.md) -- [Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md) -- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md) -- [Store Module](https://docs.medusajs.com/commerce-modules/store/index.html.md) -- [Tax Module](https://docs.medusajs.com/commerce-modules/tax/index.html.md) -- [User Module](https://docs.medusajs.com/commerce-modules/user/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) *** -## How to Use Modules +## Order Return -The Commerce Modules can be used in many use cases, including: +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. -- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. -- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. -- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. +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) -# Account Holders and Saved Payment Methods +### Dismissed Returned Items -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. -Account holders are available starting from Medusa `v2.5.0`. -## What's an Account Holder? +# Inventory Kits -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. +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. -It holds fields retrieved from the third-party provider, such as: +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: -- `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. +- [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). -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. +## What is an Inventory Kit? -### Relation between Account Holder and Customer +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 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. +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. -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. +Using inventory kits, you can implement use cases like: -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. +- [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. *** -## 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). - -*** +## Multi-Part Products -## Account Holder in Medusa Payment Flows +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. -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. +To implement this in Medusa, you can: -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. +- 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. -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). +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) -# Links between Payment Module and Other Modules +### Create Multi-Part Product -This document showcases the module links defined between the Payment Module and other Commerce Modules. +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). -## Summary +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: -The Payment Module has the following links to other modules: +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|PaymentCollection|Stored - one-to-one|Learn more| -|Customer|AccountHolder|Stored - many-to-many|Learn more| -|Order|PaymentCollection|Stored - one-to-many|Learn more| -|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| -|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| -|Region|PaymentProvider|Stored - many-to-many|Learn more| +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", + }, + }) -*** - -## Cart Module + 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, + }, + ], + }, + ], + }, + }) -The Cart Module provides cart-related features, but not payment processing. + // TODO create the product + } +) +``` -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. +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). -Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md). +Then, you create the inventory items that the product variant consists of. -### Retrieve with Query +Next, create the product and pass the inventory item's IDs to the product's variant: -To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -### query.graph +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... -```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) -// paymentCollections[0].cart + 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", + }, + ], + }, + }) + } +) ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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). -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) +*** -// paymentCollections[0].cart -``` +## Bundled Products -### Manage with Link +While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products. -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. -### link.create +![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) -```ts -import { Modules } from "@medusajs/framework/utils" +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. -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +![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) -### createRemoteLinkStep +### Create Bundled Product -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +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: -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +```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"], + }, + ], + }, + ], + }, + }) -## Customer Module + // TODO re-retrieve with inventory + } +) +``` -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. +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). -This link is available starting from Medusa `v2.5.0`. +Next, retrieve the products again but with variant information: -### Retrieve with Query +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) -### query.graph + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) -```ts -const { data: accountHolders } = await query.graph({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) -// accountHolders[0].customer + // create bundled product + } +) ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +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. -const { data: accountHolders } = useQueryGraphStep({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) +Finally, create the bundled product: -// accountHolders[0].customer +```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" }) + } +) ``` -### 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): +The bundled product has the same inventory items as those of the products part of the bundle. -### link.create +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). -```ts -import { Modules } from "@medusajs/framework/utils" -// ... +# Links between Inventory Module and Other Modules -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` +This document showcases the module links defined between the Inventory Module and other Commerce Modules. -### createRemoteLinkStep +## Summary -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +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. -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| +|InventoryLevel|StockLocation|Read-only - has many|Learn more| *** -## Order Module +## Product Module -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. +![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) -![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) +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 order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` 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: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", fields: [ - "order.*", + "variants.*", ], }) -// paymentCollections[0].order +// inventoryItems[0].variants ``` ### useQueryGraphStep @@ -25908,19 +28577,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", fields: [ - "order.*", + "variants.*", ], }) -// paymentCollections[0].order +// inventoryItems[0].variants ``` ### Manage with Link -To manage the payment collections of an order, 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 @@ -25930,11 +28599,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` @@ -25948,40 +28617,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` *** -## Region Module - -You can specify for each region which payment providers are available. The Medusa application defines a 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) +## Stock Location Module -This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. +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 regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: +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: paymentProviders } = await query.graph({ - entity: "payment_provider", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "regions.*", + "stock_locations.*", ], }) -// paymentProviders[0].regions +// inventoryLevels[0].stock_locations ``` ### useQueryGraphStep @@ -25991,132 +28656,38 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "regions.*", + "stock_locations.*", ], }) -// paymentProviders[0].regions +// inventoryLevels[0].stock_locations ``` -### 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" +# 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. -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` +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. -### createRemoteLinkStep +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. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -// ... +## Inventory Features -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - - -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. - -## All Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| - -*** - -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. - -For example: - -```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: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -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. - - -# 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. +- [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 Payment Module +## 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. @@ -26124,7 +28695,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -26133,35 +28704,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPaymentCollectionStep = createStep( - "create-payment-collection", +const createInventoryItemStep = createStep( + "create-inventory-item", async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - const paymentCollection = await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, }) - return new StepResponse({ paymentCollection }, paymentCollection.id) + return new StepResponse({ inventoryItem }, inventoryItem.id) }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { return } - const paymentModuleService = container.resolve(Modules.PAYMENT) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) } ) -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", () => { - const { paymentCollection } = createPaymentCollectionStep() + const { inventoryItem } = createInventoryItemStep() return new WorkflowResponse({ - paymentCollection, + inventoryItem, }) } ) @@ -26176,13 +28748,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPaymentCollectionWorkflow(req.scope) + const { result } = await createInventoryItemWorkflow(req.scope) .run() res.send(result) @@ -26196,13 +28768,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -26217,12 +28789,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -26238,714 +28810,605 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## 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. +# Order Claim -*** +In this document, you’ll learn about order claims. -## Providers +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. -Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. +## 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. -# Payment Steps in Checkout Flow +*** -In this guide, you'll learn about Medusa's accept payment flow that's used in checkout. +## Claim Type -## Overview of the Payment Flow in Checkout +The `Claim` data model has a `type` property whose value indicates the type of the claim: -The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout. +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. -This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers. +*** -The payment flow consists of the following steps: +## Old and Replacement Items -![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) +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. -- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart. - - This payment collection will hold all details related to the payment operations. -- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from. - - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region. -- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider. -- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves: - - Authorizing the payment session with the third-party payment provider. - - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion. +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). *** -## Implement Payment Checkout Step in Storefront +## Claim Shipping Methods -If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step. +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). -If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step. +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). *** -{/* TODO add section on customizng the payment flow */} - -## Build a Custom Payment Flow - -You can also build a custom payment flow using workflows or the Payment Module's main service. +## Claim Refund -Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more. +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. -# Payment Collection +*** -In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. +## How Claims Impact an Order’s Version -## What's a Payment Collection? +When a claim is confirmed, the order’s version is incremented. -A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). -Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +# Order Concepts -- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. -- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. -- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. +In this document, you’ll learn about orders and related concepts -*** +## Order Items -## Multiple Payments +The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. -The payment collection supports multiple payment sessions and payments. +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) -You can use this to accept payments in increments or split payments across payment providers. +### Item’s Product Details -![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) +The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. *** -## Usage with the Cart Module - -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. - -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. +## Order’s Shipping Method -It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). +An order has one or more shipping methods used to handle item shipment. -![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) -# Accept Payment in Checkout Flow +### data Property -In this guide, you'll learn how to implement it using workflows or the Payment Module. +When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. -## Why Implement the Payment Flow? +The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. -Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide. +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. -You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application. +*** -This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows. +## Order Totals -You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application. +The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). *** -## How to Implement the Accept Payment Flow? +## Order Payments -For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). +Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. +An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. -### 1. Create a Payment Collection +Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). -A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. -In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for. +# Order Edit -For example: +In this document, you'll learn about order edits. -### Using Workflow +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. -```ts -import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" +## What is an Order Edit? -// ... +A merchant can edit an order to add new items or change the quantity of existing items in the order. -await createPaymentCollectionForCartWorkflow(container) - .run({ - input: { - cart_id: "cart_123", - }, - }) -``` +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). -### Using Service +The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. -```ts -const paymentCollection = - await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) -``` +In the case of an order edit, the `OrderChange`'s type is `edit`. -### 2. Show Payment Providers +*** -Next, you'll show the customer the available payment providers to choose from. +## Add Items in an Order Edit -In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region. +When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). -### Using Query +Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. -```ts -const query = container.resolve("query") +So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. -const { data: regionPaymentProviders } = await query.graph({ - entryPoint: "region_payment_provider", - variables: { - filters: { - region_id: "reg_123", - }, - }, - fields: ["payment_providers.*"], -}) +*** -const paymentProviders = regionPaymentProviders.map( - (relation) => relation.payment_providers -) -``` +## Update Items in an Order Edit -### Using Service +A merchant can update an existing item's quantity or price. -```ts -const paymentProviders = await paymentModuleService.listPaymentProviders() -``` +This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. -### 3. Create Payment Sessions +*** -The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. +## Shipping Methods of New Items in the Edit -So, once the customer selects a payment provider, create a payment session for the selected payment provider. +Adding new items to the order requires adding shipping methods for those items. -This will also initialize the payment session in the third-party payment provider. +These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` -For example: +*** -### Using Workflow +## How Order Edits Impact an Order’s Version -```ts -import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" +When an order edit is confirmed, the order’s version is incremented. -// ... +*** -const { result: paymentSesion } = await createPaymentSessionsWorkflow(container) - .run({ - input: { - payment_collection_id: "paycol_123", - provider_id: "pp_stripe_stripe", - }, - }) -``` +## Payments and Refunds for Order Edit Changes -### Using Service +Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. -```ts -const paymentSession = - await paymentModuleService.createPaymentSession( - paymentCollection.id, - { - provider_id: "pp_stripe_stripe", - currency_code: "usd", - amount: 5000, - data: { - // any necessary data for the - // payment provider - }, - } - ) -``` +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). -### 4. Authorize Payment Session -Once the customer places the order, you need to authorize the payment session with the third-party payment provider. +# Order Exchange -For example: +In this document, you’ll learn about order exchanges. -### Using Step +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. -```ts -import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" +## What is an Exchange? -// ... +An exchange is the replacement of an item that the customer ordered with another. -authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) -``` +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. -### Using Service +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. -```ts -const payment = authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) -``` +*** -When the payment authorization is successful, a payment is created and returned. +## Returned and New Items -#### Handling Additional Action +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. -If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. +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 payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. +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. -In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. +*** -For example: +## Exchange Shipping Methods -```ts -try { - const payment = - await paymentModuleService.authorizePaymentSession( - paymentSession.id, - {} - ) -} catch (e) { - // retrieve the payment session again - const updatedPaymentSession = ( - await paymentModuleService.listPaymentSessions({ - id: [paymentSession.id], - }) - )[0] +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). - if (updatedPaymentSession.status === "requires_more") { - // TODO perform required action - // TODO authorize payment again. - } -} -``` +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). -### 5. Payment Flow Complete +*** -The payment flow is complete once the payment session is authorized and the payment is created. +## Exchange Payment -You can then: +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. -- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application. -- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). -- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). +|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.| -Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +*** -# Payment Module Provider +## How Exchanges Impact an Order’s Version -In this guide, you’ll learn about the Payment Module Provider and how it's used. +When an exchange is confirmed, the order’s version is incremented. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. -*** +# Links between Order Module and Other Modules -## What is a Payment Module Provider? +This document showcases the module links defined between the Order Module and other Commerce Modules. -The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md). +## Summary -To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. +The Order Module has the following links to other modules: -After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg) +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Order|Customer|Read-only - has one|Learn more| +|Order|Cart|Stored - one-to-one|Learn more| +|Order|Fulfillment|Stored - one-to-many|Learn more| +|Return|Fulfillment|Stored - one-to-many|Learn more| +|Order|PaymentCollection|Stored - one-to-many|Learn more| +|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| +|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| +|OrderLineItem|Product|Read-only - has many|Learn more| +|Order|Promotion|Stored - many-to-many|Learn more| +|Order|Region|Read-only - has one|Learn more| +|Order|SalesChannel|Read-only - has one|Learn more| -### List of Payment Module Providers +*** -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) +## Customer Module -### Default Payment Provider +Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. +### Retrieve with Query -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. +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`: -The identifier of the system payment provider is `pp_system`. +### query.graph -*** +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) -## How to Create a Custom Payment Provider? +// orders[0].customer +``` -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. +### useQueryGraphStep -The module can have multiple payment provider services, where each is registered as a separate payment provider. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. +// ... -After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md). +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders[0].customer +``` *** -## How are Payment Providers Registered? +## Cart Module -### Configure Payment Module's Providers +The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. -The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application. +Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide. +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) -### Registration on Application Start +### Retrieve with Query -When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module. +To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: -For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register. +### query.graph -### PaymentProvider Data Model - -A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application. - -![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg) - -This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application. - -The `PaymentProvider` data model has the following properties: - -- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Payment Module Provider's service. - - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the payment provider is enabled. - -### How to Remove a Payment Provider? - -If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record. - -Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option. - - -# 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. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "cart.*", + ], +}) -## Register the Stripe Module Provider +// orders[0].cart +``` -### Prerequisites +### useQueryGraphStep -- [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) +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "cart.*", ], }) -``` - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: -```bash -STRIPE_API_KEY= +// orders[0].cart ``` -### 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 +### Manage with Link -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. +To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -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. +### link.create -*** +```ts +import { Modules } from "@medusajs/framework/utils" -## Stripe Payment Provider IDs +// ... -When you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more. +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.CART]: { + cart_id: "cart_123", + }, +}) +``` -Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where: +### createRemoteLinkStep -- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider. -- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers: +// ... -|Provider Name|Provider ID| -|---|---|---| -|Basic Stripe Payment|\`pp\_stripe\_stripe\`| -|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`| -|BLIK Payments|\`pp\_stripe-blik\_stripe\`| -|giropay Payments|\`pp\_stripe-giropay\_stripe\`| -|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`| +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.CART]: { + cart_id: "cart_123", + }, +}) +``` *** -## 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: +## Fulfillment Module -- `{server_url}` is the URL to your deployed Medusa application in server mode. -- `{provider_id}` is the ID of the provider as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix. +A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) -|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\`| +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. -### Webhook Events +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) -When you set up the webhook in Stripe, choose the following events to listen to: +### Retrieve with Query -- `payment_intent.amount_capturable_updated` -- `payment_intent.succeeded` -- `payment_intent.payment_failed` -- `payment_intent.partially_funded` (Since [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5)) +To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: -*** +To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. -## Useful Guides +### query.graph -- [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). -- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md). +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "fulfillments.*", + ], +}) +// orders[0].fulfillments +``` -# Payment Session +### useQueryGraphStep -In this document, you’ll learn what a payment session is. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## 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. +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "fulfillments.*", + ], +}) -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. +// orders[0].fulfillments +``` -![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) +### Manage with Link -*** +To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -## data Property +### link.create -Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider. +```ts +import { Modules } from "@medusajs/framework/utils" -The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized. +// ... -Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider. +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` -If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. +### createRemoteLinkStep -### data Property in the Storefront +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront. +// ... -For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider. +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` *** -## 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. - - -# 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. - -*** +## Payment Module -## Capture Payments +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -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. +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. -The payment can also be captured incrementally, each time a capture record is created for that amount. +![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) -![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) +### Retrieve with Query -*** +To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: -## Refund Payments +### query.graph -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. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) -A payment can be refunded multiple times, and each time a refund record is created. +// orders[0].payment_collections +``` -![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) +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## data Property +// ... -Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider. +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) -The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment. +// orders[0].payment_collections +``` -Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider. +### Manage with Link -If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. +To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +### link.create -# Payment Webhook Events +```ts +import { Modules } from "@medusajs/framework/utils" -In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module. +// ... -## What's a Payment Webhook Event? +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status. +### createRemoteLinkStep -This is useful in many cases such as: +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -- When a payment is processed (authorized or captured) asynchronously. -- When a payment is managed on the third-party payment provider's side. -- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application. +// ... -So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions. +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` *** -## How to Handle Payment Webhook Events - -### Webhook Listener API Route - -The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where: - -- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. -- `[provider]` is the ID of the provider. For example, `stripe`. - -For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. - -You can use this webhook listener when configuring webhook events in your third-party payment provider. - -### getWebhookActionAndData Method - -The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider. - -Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events. - -![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) - -If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions: - -View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference. - -- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`. -- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`. -- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart. - - -# 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). +## Product Module -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). +Medusa defines read-only links between: -![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) +- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. +- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. -*** +### Retrieve with Query -## Price List +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`: -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. +To retrieve the product, pass `product.*` in `fields`. -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. +### query.graph -Its associated prices are represented by the `Price` data model. +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) +// lineItems.variant +``` -# Links between Pricing Module and Other Modules +### useQueryGraphStep -This document showcases the module links defined between the Pricing Module and other Commerce Modules. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Summary +// ... -The Pricing Module has the following links to other modules: +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|ShippingOption|PriceSet|Stored - one-to-one|Learn more| -|ProductVariant|PriceSet|Stored - one-to-one|Learn more| +// lineItems.variant +``` *** -## Fulfillment Module - -The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. +## Promotion Module -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +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 Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) +![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 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`: +To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: ### query.graph ```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "shipping_option.*", + "promotions.*", ], }) -// priceSets[0].shipping_option +// orders[0].promotions ``` ### useQueryGraphStep @@ -26955,19 +29418,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "shipping_option.*", + "promotions.*", ], }) -// priceSets[0].shipping_option +// orders[0].promotions ``` ### Manage with Link -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -26977,11 +29440,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -26995,44 +29458,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_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. +## Region Module -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. +Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. ### 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`: +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: priceSets } = await query.graph({ - entity: "price_set", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "variant.*", + "region.*", ], }) -// priceSets[0].variant +// orders[0].region ``` ### useQueryGraphStep @@ -27042,77 +29497,480 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "variant.*", + "region.*", ], }) -// priceSets[0].variant +// orders[0].region ``` -### 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): +## Sales Channel Module -### link.create +Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. -```ts -import { Modules } from "@medusajs/framework/utils" +### 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`: -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders[0].sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders[0].sales_channel +``` + + +# 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\`| + + +# Retrieve Order Totals Using Query + +In this guide, you'll learn how to retrieve order totals in your Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +You may need to retrieve order totals in your Medusa customizations, such as workflows or custom API routes, to perform custom actions with them. The ideal way to retrieve totals is using Query. + +Refer to the [Order Confirmation in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/order-confirmation#show-order-totals/index.html.md) guide for a storefront-specific approach. + +## How to Retrieve Order Totals with Query + +To retrieve order totals, you mainly need to pass the `total` field within the `fields` option of the Query. This will return the order's grand total, along with the totals of its line items and shipping methods. You can also pass additional total fields that you need for your use case. + +For example, to retrieve all totals of an order: + +Use `useQueryGraphStep` in workflows, and `query.graph` in custom API routes, scheduled jobs, and subscribers. + +### useQueryGraphStep + +```ts highlights={[["12"]]} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +export const myWorkflow = createWorkflow( + "my-workflow", + () => { + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "currency_code", + "total", + "subtotal", + "tax_total", + "original_total", + "original_subtotal", + "original_tax_total", + "discount_total", + "discount_tax_total", + "shipping_total", + "shipping_subtotal", + "shipping_tax_total", + "original_shipping_total", + "original_shipping_subtotal", + "original_shipping_tax_total", + "item_total", + "item_tax_total", + "item_subtotal", + "original_item_total", + "original_item_tax_total", + "original_item_subtotal", + "gift_card_total", + "gift_card_tax_total", + "items.*", + "shipping_methods.*", + "summary.*", + ], + filters: { + id: "order_123", // Specify the order ID + }, + }) + } +) +``` + +### query.graph + +```ts highlights={[["8"]]} +const query = container.resolve("query") // or req.scope.resolve in API routes + +const { data: [order] } = await query.graph({ + entity: "order", + fields: [ + "id", + "currency_code", + "total", + "subtotal", + "tax_total", + "original_total", + "original_subtotal", + "original_tax_total", + "discount_total", + "discount_tax_total", + "shipping_total", + "shipping_subtotal", + "shipping_tax_total", + "original_shipping_total", + "original_shipping_subtotal", + "original_shipping_tax_total", + "item_total", + "item_tax_total", + "item_subtotal", + "original_item_total", + "original_item_tax_total", + "original_item_subtotal", + "gift_card_total", + "gift_card_tax_total", + "items.*", + "shipping_methods.*", + "summary.*", + ], + filters: { + id: "order_123", // Specify the order ID }, - [Modules.PRICING]: { - price_set_id: "pset_123", +}) +``` + +The returned `order` object will look like this: + +```json +{ + "id": "order_01K1GEZ6Y1V9651AJNYG1WV3TC", + "currency_code": "eur", + "total": 20, + "subtotal": 20, + "tax_total": 0, + "original_total": 20, + "original_tax_total": 0, + "discount_total": 0, + "discount_tax_total": 0, + "shipping_total": 10, + "shipping_subtotal": 10, + "shipping_tax_total": 0, + "original_shipping_total": 10, + "original_shipping_subtotal": 10, + "original_shipping_tax_total": 0, + "item_total": 10, + "item_tax_total": 0, + "item_subtotal": 10, + "original_item_total": 10, + "original_item_tax_total": 0, + "original_item_subtotal": 10, + "items": [ + { + "subtotal": 10, + "total": 10, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + "refundable_total_per_unit": 10, + "refundable_total": 10, + "fulfilled_total": 0, + "shipped_total": 0, + "return_requested_total": 0, + "return_received_total": 0, + "return_dismissed_total": 0, + "write_off_total": 0, + // ... + } + ], + "shipping_methods": [ + { + "subtotal": 10, + "total": 10, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + // ... + } + ], + "summary": { + "paid_total": 0, + "refunded_total": 0, + "accounting_total": 20, + "credit_line_total": 0, + "transaction_total": 0, + "pending_difference": 20, + "current_order_total": 20, + "original_order_total": 20 + } +} +``` + +### Order Totals + +The order will include the following total fields: + +- `total`: The grand total of the order, including all line items, shipping methods, taxes, and discounts. +- `subtotal`: The order's total excluding taxes, including promotions. +- `tax_total`: The order's tax total including promotions. +- `original_total`: The order's total including taxes, excluding promotions. +- `original_subtotal`: The order's total excluding taxes, including promotions. +- `original_tax_total`: The order's tax total excluding promotions. +- `discount_total`: The order's discount or promotions total. +- `discount_tax_total`: The tax total of the order's discount or promotion. +- `shipping_total`: The order's shipping total including taxes and promotions. +- `shipping_subtotal`: The order's shipping total excluding taxes, including promotions. +- `shipping_tax_total`: The tax total of the order's shipping. +- `original_shipping_total`: The order's shipping total including taxes, excluding promotions. +- `original_shipping_subtotal`: The order's shipping total excluding taxes, including promotions. +- `original_shipping_tax_total`: The tax total of the order's shipping excluding promotions. +- `item_total`: The total of all line items in the order, including taxes and promotions. +- `item_tax_total`: The tax total of all line items in the order, including promotions. +- `item_subtotal`: The subtotal of all line items in the order, excluding taxes, including promotions. +- `original_item_total`: The total of all line items in the order, including taxes, excluding promotions. +- `original_item_tax_total`: The tax total of all line items in the order, excluding promotions. +- `original_item_subtotal`: The subtotal of all line items in the order, excluding taxes, including promotions. +- `gift_card_total`: The total amount of gift cards applied to the order. +- `gift_card_tax_total`: The tax total of the gift cards applied to the order. + +### Order Line Item Totals + +The `items` array in the `order` object contains total fields for each line item: + +- `unit_price`: The price of a single unit of the line item. This field is not calculated and is stored in the database. +- `subtotal`: The total price of the line item before any discounts or taxes. +- `total`: The total price of the line item after applying discounts and taxes. +- `original_total`: The total price of the line item before any discounts. +- `discount_total`: The total amount of discounts applied to the line item. +- `discount_subtotal`: The total amount of discounts applied to the line item's subtotal. +- `discount_tax_total`: The total amount of discounts applied to the line item's tax. +- `tax_total`: The total tax amount applied to the line item. +- `original_tax_total`: The total tax amount applied to the line item before any discounts. +- `refundable_total_per_unit`: The total amount that can be refunded per unit of the line item. +- `refundable_total`: The total amount that can be refunded for the line item. +- `fulfilled_total`: The total amount of the line item that has been fulfilled. +- `shipped_total`: The total amount of the line item that has been shipped. +- `return_requested_total`: The total amount of the line item that has been requested for return. +- `return_received_total`: The total amount of the line item that has been received from a return. +- `return_dismissed_total`: The total amount of the line item that has been dismissed by a return. + - These items are considered damaged and are not returned to the inventory. +- `write_off_total`: The total amount of the line item that has been written off. + - For example, if the order is edited and the line item is removed or its quantity has changed. + +### Order Shipping Method Totals + +The `shipping_methods` array in the `order` object contains total fields for each shipping method: + +- `amount`: The amount charged for the shipping method. This field is not calculated and is stored in the database. +- `subtotal`: The total price of the shipping method before any discounts or taxes. +- `total`: The total price of the shipping method after applying discounts and taxes. +- `original_total`: The total price of the shipping method before any discounts. +- `discount_total`: The total amount of discounts applied to the shipping method. +- `discount_subtotal`: The total amount of discounts applied to the shipping method's subtotal. +- `discount_tax_total`: The total amount of discounts applied to the shipping method's tax. +- `tax_total`: The total tax amount applied to the shipping method. +- `original_tax_total`: The total tax amount applied to the shipping method before any discounts. + +### Order Summary Totals + +The `summary` object in the `order` object provides an overview of the order's transactions. It includes the following fields: + +- `paid_total`: The total amount that has been paid for the order. +- `refunded_total`: The total amount that has been refunded for the order. +- `accounting_total`: The order's total without the credit-line total. +- `credit_line_total`: The total amount of credit lines applied to the order. +- `transaction_total`: The total amount of transactions associated with the order. +- `pending_difference`: The difference between the order's total and the paid total. +- `current_order_total`: The current total of the order. It's useful if the order has been edited or modified. +- `original_order_total`: The original total of the order before any modifications. + +*** + +## Caveats of Retrieving Order Totals + +### Using Asterisk (\*) in Order Query + +Order totals are calculated based on the order's line items, shipping methods, taxes, and any discounts applied. They are not stored in the `Order` data model. + +For that reason, you cannot retrieve order totals by passing `*` to the Query's fields. For example, the following query will not return order totals: + +### useQueryGraphStep + +```ts +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: ["*"], + filters: { + id: "order_123", }, }) + +// orders don't include order totals ``` -### createRemoteLinkStep +### query.graph ```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +const { data: [order] } = await query.graph({ + entity: "order", + fields: ["*"], + filters: { + id: "order_123", + }, +}) -// ... +// order doesn't include order totals +``` -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", +This will return the order data stored in the database, but not the calculated totals. + +You also can't pass `*` along with `total` in the Query's fields option. Passing `*` will override the `total` field, and you will not retrieve order totals. For example, the following query will not return order totals: + +### useQueryGraphStep + +```ts +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: ["*", "total"], + filters: { + id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", +}) + +// orders don't include order totals +``` + +### query.graph + +```ts +const { data: [order] } = await query.graph({ + entity: "order", + fields: ["*", "total"], + filters: { + id: "order_123", }, }) + +// order doesn't include order totals ``` +Instead, when you need to retrieve order totals, explicitly pass the `total` field in the Query's fields option, as shown in [the previous section](#how-to-retrieve-order-totals-with-query). You can also include other fields and relations you need, such as `email` and `items.*`. -# Pricing Module +### Applying Filters on Order Totals -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. +You can't apply filters directly on the order totals, as they are not stored in the `Order` data model. You can still filter the order based on its properties, such as `id`, `email`, or `currency_code`, but not on the calculated totals. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. -Medusa has pricing related features available out-of-the-box through the Pricing 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 Pricing Module. +# 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 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). -## Pricing Features +## Order Features -- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. -- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. -- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. -- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. -- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. +- [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 Pricing Module +## 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. @@ -27120,7 +29978,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-price-set.ts" highlights={highlights} +```ts title="src/workflows/create-draft-order.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -27129,46 +29987,48 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPriceSetStep = createStep( - "create-price-set", +const createDraftOrderStep = createStep( + "create-order", async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) + const orderModuleService = container.resolve(Modules.ORDER) - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ + const draftOrder = await orderModuleService.createOrders({ + currency_code: "usd", + items: [ { - amount: 500, - currency_code: "USD", + title: "Shirt", + quantity: 1, + unit_price: 3000, }, + ], + shipping_methods: [ { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, + name: "Express shipping", + amount: 3000, }, ], + status: "draft", }) - return new StepResponse({ priceSet }, priceSet.id) + return new StepResponse({ draftOrder }, draftOrder.id) }, - async (priceSetId, { container }) => { - if (!priceSetId) { + async (draftOrderId, { container }) => { + if (!draftOrderId) { return } - const pricingModuleService = container.resolve(Modules.PRICING) + const orderModuleService = container.resolve(Modules.ORDER) - await pricingModuleService.deletePriceSets([priceSetId]) + await orderModuleService.deleteOrders([draftOrderId]) } ) -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", () => { - const { priceSet } = createPriceSetStep() + const { draftOrder } = createDraftOrderStep() return new WorkflowResponse({ - priceSet, + draftOrder, }) } ) @@ -27183,13 +30043,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPriceSetWorkflow } from "../../workflows/create-price-set" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPriceSetWorkflow(req.scope) + const { result } = await createDraftOrderWorkflow(req.scope) .run() res.send(result) @@ -27203,13 +30063,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -27224,12 +30084,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -27246,2094 +30106,1606 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Prices Calculation +# Promotions Adjustments in Orders -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. +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. -## calculatePrices Method +## What are Adjustment Lines? -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. +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. -It returns a price object with the best matching price for each price set. +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. -### Calculation Context +![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 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. +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. -For example: +The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, +*** + +## 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"], { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + // TODO infer from cart or region + currency_code: "usd", } ) ``` -In this example, you retrieve the prices in a price set for the specified currency code and region ID. - -### Returned Price Object +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -For each price set, the `calculatePrices` method selects two prices: +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. -- 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. +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -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. +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) - - id: (\`string\`) The ID of the price. +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` - - price\_list\_id: (\`string\`) The ID of the associated price list. - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +# Order Return - - min\_quantity: (\`number\`) The price's min quantity condition. +In this document, you’ll learn about order returns. - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. +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. - - id: (\`string\`) The ID of the price. +## What is a Return? - - price\_list\_id: (\`string\`) The ID of the associated price list. +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). - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +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. - - min\_quantity: (\`number\`) The price's min quantity condition. +![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) - - max\_quantity: (\`number\`) The price's max quantity condition. +Once the merchant receives the returned items, they mark the return as received. *** -## Examples - -Consider the following price set: +## Returned Items -```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", - }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, - { - amount: 200, - currency_code: "EUR", - min_quantity: 100, - }, - ], -}) -``` +The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md). -### Default Price Selection +The `ReturnItem` model has two properties storing the item's quantity: -### Code +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. -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` +*** -### Result +## Return Shipping Methods -### Calculate Prices with Rules +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -### Code +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. -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) -``` +*** -### Result +## Refund Payment -### Tiered Pricing Selection +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. -### Code +The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return. -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - cart: { - items: [ - { - id: "item_1", - quantity: 200, - // assuming the price set belongs to this variant - variant_id: "variant_1", - // ... - } - ], - // ... - } - } - } -) -``` +*** -### Result +## Returns in Exchanges and Claims -### Price Selection with Price List +When a merchant creates an exchange or a claim, it includes returning items from the customer. -### Code +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. -```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" - } - } -) -``` +## How Returns Impact an Order’s Version -### Result +The order’s version is incremented when: +1. A return is requested. +2. A return is marked as received. -# Price Tiers and Rules -In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. +# Tax Lines in Order Module -## Tiered Pricing +In this document, you’ll learn about tax lines in an order. -Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: +## What are Tax Lines? -- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. -- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. +A tax line indicates the tax rate of a line item or a shipping method. -This is useful to set tiered pricing for resources like product variants and shipping options. +The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. -For example, you can set a variant's price to: +![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) -- `$10` by default. -- `$8` when the customer adds `10` or more of the variant to the cart. -- `$6` when the customer adds `20` or more of the variant to the cart. +*** -These price definitions would look like this: +## Tax Inclusivity -```json title="Example Prices" -[ - // default price - { - "amount": 10, - "currency_code": "usd", - }, - { - "amount": 8, - "currency_code": "usd", - "min_quantity": 10, - "max_quantity": 19, - }, - { - "amount": 6, - "currency_code": "usd", - "min_quantity": 20, - }, -], -``` +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. -### How to Create Tiered Prices? +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. -When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -For example: +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. +![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) -### Using Medusa Workflows +For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. -```ts highlights={tieredPricingHighlights} -const { result } = await createProductsWorkflow(container) - .run({ - input: { - products: [{ - variants: [{ - id: "variant_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], - // ... - }], - }], - // ... - }, - }) -``` -### Using the Pricing Module +# Transactions -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - // tiered prices - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], -}) -``` +In this document, you’ll learn about an order’s transactions and its use. -In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. +## What is a Transaction? -### How are Tiered Prices Applied? +A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply. +The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. -For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. +Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. -The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. +*** -If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. +## Checking Outstanding Amount -*** +The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. -## Price Rule +```json +{ + "totals": { + "total": 30, + "subtotal": 30, + // ... + } +} +``` -You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. +To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: -Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). +|Condition|Result| +|---|---|---| +|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| +|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| +|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| -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. +## Transaction Reference -![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) +The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). -A price can have multiple price rules. +The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: -For example, a price can be restricted by a region and a zip code. +- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. +- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. -![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 +# Commerce Modules -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). +In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules. -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. +A Commerce Module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. -![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) +A Commerce Module also defines data models, representing tables in the database. The Medusa Framework and tools allow you to extend these data models to add custom fields. -### How to Create Prices with Rules? +## Commerce Modules List -When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. +- [API Key Module](https://docs.medusajs.com/commerce-modules/api-key/index.html.md) +- [Auth Module](https://docs.medusajs.com/commerce-modules/auth/index.html.md) +- [Cart Module](https://docs.medusajs.com/commerce-modules/cart/index.html.md) +- [Currency Module](https://docs.medusajs.com/commerce-modules/currency/index.html.md) +- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md) +- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md) +- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md) +- [Order Module](https://docs.medusajs.com/commerce-modules/order/index.html.md) +- [Payment Module](https://docs.medusajs.com/commerce-modules/payment/index.html.md) +- [Pricing Module](https://docs.medusajs.com/commerce-modules/pricing/index.html.md) +- [Product Module](https://docs.medusajs.com/commerce-modules/product/index.html.md) +- [Promotion Module](https://docs.medusajs.com/commerce-modules/promotion/index.html.md) +- [Region Module](https://docs.medusajs.com/commerce-modules/region/index.html.md) +- [Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md) +- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md) +- [Store Module](https://docs.medusajs.com/commerce-modules/store/index.html.md) +- [Tax Module](https://docs.medusajs.com/commerce-modules/tax/index.html.md) +- [User Module](https://docs.medusajs.com/commerce-modules/user/index.html.md) -For example: +*** -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. +## How to Use Modules -### Using Medusa Workflows +The Commerce Modules can be used in many use cases, including: -```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, - }, - }, - }, - ], - }], - }) -``` +- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. +- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. +- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. -### Using the Pricing Module -```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, - }, - }, - }, - ], -}) -``` +# Account Holders and Saved Payment Methods -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. +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. -### How is the Price Rule Applied? +Account holders are available starting from Medusa `v2.5.0`. -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. +## What's an Account Holder? -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: +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. -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "item_total": { - "operator": "gte", - "value": 100, - } - } -} -``` +It holds fields retrieved from the third-party provider, such as: -The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. +- `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. -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: +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. -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "customer.group.id": { - "operator": "eq", - "value": "cusgrp_123" - } - } -} -``` +### Relation between Account Holder and Customer -In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. +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. -These same rules apply to product variant prices as well, or any other resource that has a price. +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. -# Tax-Inclusive Pricing +*** -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. +## Save Payment Methods -## What is Tax-Inclusive Pricing? +If a payment provider supports saving payment methods for a customer, they must implement the following methods: -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. +- `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. -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. +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). *** -## 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: +## Account Holder in Medusa Payment Flows -- `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`. +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. -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. +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. -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). -For example: -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` +# Links between Payment Module and Other Modules -In this example, tax-inclusivity is enabled for the `USD` currency code. +This document showcases the module links defined between the Payment Module and other Commerce Modules. -*** +## Summary -## Tax-Inclusive Pricing in Price Calculation +The Payment Module has the following links to other modules: -### Tax Context +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Order|PaymentCollection|Stored - one-to-many|Learn more| +|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| +|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| +|Region|PaymentProvider|Stored - many-to-many|Learn more| -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. +## Cart Module -### Returned Tax Properties +The Cart Module provides cart-related features, but not payment processing. -The `calculatePrices` method returns two properties related to tax-inclusivity: +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. -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). +Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/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. +### Retrieve with Query -A price is considered tax-inclusive if: +To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: -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. +### query.graph -### Tax Context Precedence +```ts +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: +// paymentCollections[0].cart +``` -- 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 +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Tax-Inclusive Pricing with Promotions +// ... -When you enable tax-inclusive prices for regions or currencies, this can impact how promotions are applied to the cart. So, it's recommended to enable tax-inclusiveness for promotions as well. +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) -Learn more in the [Tax-Inclusive Promotions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/promotion-taxes/index.html.md) guide. +// paymentCollections[0].cart +``` +### Manage with Link -# Calculate Product Variant Price with Taxes +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you'll learn how to calculate a product variant's price with taxes. +### link.create -## Step 0: Resolve Resources +```ts +import { Modules } from "@medusajs/framework/utils" -You'll need the following resources for the taxes calculation: +// ... -1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). -2. The Tax Module's main service to get the tax lines for each product. - -```ts -// other imports... -import { - Modules, - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -// In an API route, workflow step, etc... -const query = container.resolve(ContainerRegistrationKeys.QUERY) -const taxModuleService = container.resolve( - Modules.TAX -) +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) ``` -*** - -## Step 1: Retrieve Prices for a Context - -After resolving the resources, use Query to retrieve the products with the variants' prices for a context: - -Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). +### createRemoteLinkStep ```ts -import { QueryContext } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "region_123", - currency_code: "usd", - }), - }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` *** -## Step 2: Get Tax Lines for Products - -To retrieve the tax line of each product, first, add the following utility method: +## Customer Module -```ts -// other imports... -import { - HttpTypes, - TaxableItemDTO, -} from "@medusajs/framework/types" +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. -// ... -const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { - return product.variants - ?.map((variant) => { - if (!variant.calculated_price) { - return - } +This link is available starting from Medusa `v2.5.0`. - return { - id: variant.id, - product_id: product.id, - product_name: product.title, - product_categories: product.categories?.map((c) => c.name), - product_category_id: product.categories?.[0]?.id, - product_sku: variant.sku, - product_type: product.type, - product_type_id: product.type_id, - quantity: 1, - unit_price: variant.calculated_price.calculated_amount, - currency_code: variant.calculated_price.currency_code, - } - }) - .filter((v) => !!v) as unknown as TaxableItemDTO[] -} -``` +### Retrieve with Query -This formats the products as items to calculate tax lines for. +To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -Then, use it when retrieving the tax lines of the products retrieved earlier: +### query.graph ```ts -// other imports... -import { - ItemTaxLineDTO, -} from "@medusajs/framework/types" +const { data: accountHolders } = await query.graph({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) -// ... -const taxLines = (await taxModuleService.getTaxLines( - products.map(asTaxItem).flat(), - { - // example of context properties. You can pass other ones. - address: { - country_code, - }, - } -)) as unknown as ItemTaxLineDTO[] +// accountHolders[0].customer ``` -You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. +### useQueryGraphStep -For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -For the second parameter, you pass the current context. You can pass other details such as the customer's ID. +// ... -Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). +const { data: accountHolders } = useQueryGraphStep({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) -*** +// accountHolders[0].customer +``` -## Step 3: Calculate Price with Tax for Variant +### Manage with Link -To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -```ts highlights={taxLineHighlights} -const taxLinesMap = new Map() -taxLines.forEach((taxLine) => { - const variantId = taxLine.line_item_id - if (!taxLinesMap.has(variantId)) { - taxLinesMap.set(variantId, []) - } +### link.create - taxLinesMap.get(variantId)?.push(taxLine) +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, }) ``` -Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. - -Then, loop over the products and their variants to retrieve the prices with and without taxes: +### createRemoteLinkStep -```ts highlights={calculateTaxHighlights} -// other imports... -import { - calculateAmountsWithTax, -} from "@medusajs/framework/utils" +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -products.forEach((product) => { - product.variants?.forEach((variant) => { - if (!variant.calculated_price) { - return - } - - const taxLinesForVariant = taxLinesMap.get(variant.id) || [] - const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ - taxLines: taxLinesForVariant, - amount: variant.calculated_price!.calculated_amount!, - includesTax: - variant.calculated_price!.is_calculated_price_tax_inclusive!, - }) - // do something with prices... - }) +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, }) ``` -For each product variant, you: - -1. Retrieve its tax lines from the `taxLinesMap`. -2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. -3. The `calculateAmountsWithTax` function returns an object having two properties: - - `priceWithTax`: The variant's price with the taxes applied. - - `priceWithoutTax`: The variant's price without taxes applied. - +*** -# Get Product Variant Prices using Query +## Order Module -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). +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -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, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. -So, to retrieve data across the linked records of the two modules, you use Query. +![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) -## Retrieve All Product Variant Prices +### Retrieve with Query -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. +To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: -For example: +### query.graph -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", +```ts +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "*", - "variants.*", - "variants.prices.*", + "order.*", ], - filters: { - id: [ - "prod_123", - ], - }, }) + +// paymentCollections[0].order ``` -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). +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## 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. +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "order.*", + ], +}) -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). +// paymentCollections[0].order +``` -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: +### Manage with Link -- 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. +To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -For example: +### link.create -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" // ... -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` -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). +### createRemoteLinkStep -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). +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +// ... -# Get Product Variant Inventory Quantity +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +*** -Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide. +## Region Module -## Understanding Product Variant Inventory Availability +You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models. -Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant. +![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) -When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products. +This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. -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. +### Retrieve with Query -This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled. +To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: -*** +### query.graph -## Retrieve Product Variant Inventory +```ts +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", + fields: [ + "regions.*", + ], +}) -To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant. +// paymentProviders[0].regions +``` -For example: +### useQueryGraphStep -```ts highlights={variantAvailabilityHighlights} -import { getVariantAvailability } from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -// use req.scope instead of container in API routes -const query = container.resolve("query") - -const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: "sc_123", +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", + fields: [ + "regions.*", + ], }) -``` - -A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md). -So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in. +// paymentProviders[0].regions +``` -Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function. +### Manage with Link -### Parameters +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -The `getVariantAvailability` function accepts the following parameters: +### link.create -- query: (Query) Instance of Query to retrieve the necessary data. -- options: (\`object\`) The options to retrieve the variant availability. +```ts +import { Modules } from "@medusajs/framework/utils" - - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability. +// ... - - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in. +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -### Returns +### createRemoteLinkStep -The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -The value of each key is an object with the following properties: +// ... -- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`. -- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to. +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -For example, the object may look like this: -```json title="Example result" -{ - "variant_123": { - "availability": 10, - "sales_channel_id": "sc_123" - } -} -``` +# Payment Module Options -*** +In this document, you'll learn about the options of the Payment Module. -## Retrieve Sales Channel to Use +## All Module Options -To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either: +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| +|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| +|\`providers\`|An array of payment providers to install and register. Learn more |No|-| -- Use the sales channel of the request's scope. -- Use the sales channel that the variant's product is available in. +*** -### Method 1: Use Sales Channel Scope in Store Routes +## providers Option -Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key. +The `providers` option is an array of payment module providers. -So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object: +When the Medusa application starts, these providers are registered and can be used to process payments. -```ts highlights={salesChannelScopeHighlights} -import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" -import { getVariantAvailability } from "@medusajs/framework/utils" +For example: -export async function GET( - req: MedusaStoreRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") - const sales_channel_ids = req.publishable_key_context.sales_channel_ids +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" - const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: sales_channel_ids[0], - }) +// ... - res.json({ - availability, - }) -} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) ``` -In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. +The `providers` option is an array of objects that accept the following properties: -Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. +- `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. -Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. -### Method 2: Use Product's Sales Channel +# Payment Module -A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels. +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. -For example: +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. -```ts highlights={productSalesChannelHighlights} -import { getVariantAvailability } from "@medusajs/framework/utils" +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). -// use req.scope instead of container in API routes -const query = container.resolve("query") +## Payment Features -const { data: variants } = await query.graph({ - entity: "variant", - fields: ["id", "product.sales_channels.*"], - filters: { - id: "variant_123", - }, -}) +- [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. -const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: variants[0].product!.sales_channels![0]!.id, -}) -``` +*** -In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +## How to Use the Payment Module -You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product. +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. -Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. +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: -# Links between Product Module and Other Modules +```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" -This document showcases the module links defined between the Product Module and other Commerce Modules. +const createPaymentCollectionStep = createStep( + "create-payment-collection", + async ({}, { container }) => { + const paymentModuleService = container.resolve(Modules.PAYMENT) -## Summary + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) -The Product Module has the following links to other modules: + return new StepResponse({ paymentCollection }, paymentCollection.id) + }, + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { + return + } + const paymentModuleService = container.resolve(Modules.PAYMENT) -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + } +) -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|LineItem|Product|Read-only - has one|Learn more| -|Product|ShippingProfile|Stored - many-to-one|Learn more| -|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| -|OrderLineItem|Product|Read-only - has one|Learn more| -|ProductVariant|PriceSet|Stored - one-to-one|Learn more| -|Product|SalesChannel|Stored - many-to-many|Learn more| +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", + () => { + const { paymentCollection } = createPaymentCollectionStep() -*** + return new WorkflowResponse({ + paymentCollection, + }) + } +) +``` -## Cart Module +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Medusa defines read-only links between: +### API Route -- 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. +```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" -### Retrieve with Query +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPaymentCollectionWorkflow(req.scope) + .run() -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`: + res.send(result) +} +``` -To retrieve the product, pass `product.*` in `fields`. +### Subscriber -### query.graph +```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" -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPaymentCollectionWorkflow(container) + .run() -// lineItems[0].variant + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} ``` -### useQueryGraphStep +### Scheduled Job -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```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() -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) + console.log(result) +} -// lineItems[0].variant +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 +## Configure Payment 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. +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. -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). +*** -### Retrieve with Query +## Providers -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`: +Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. -### query.graph +*** -```ts -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "shipping_profile.*", - ], -}) -// products[0].shipping_profile -``` +# Payment Steps in Checkout Flow -### useQueryGraphStep +In this guide, you'll learn about Medusa's accept payment flow that's used in checkout. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Overview of the Payment Flow in Checkout -// ... +The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout. -const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "shipping_profile.*", - ], -}) +This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers. -// products[0].shipping_profile -``` +The payment flow consists of the following steps: -### Manage with Link +![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart. + - This payment collection will hold all details related to the payment operations. +- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from. + - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region. +- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider. +- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves: + - Authorizing the payment session with the third-party payment provider. + - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion. -### link.create +*** -```ts -import { Modules } from "@medusajs/framework/utils" +## Implement Payment Checkout Step in Storefront -// ... +If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step. -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` +If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step. -### createRemoteLinkStep +*** -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +{/* TODO add section on customizng the payment flow */} -// ... +## Build a Custom Payment Flow -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` +You can also build a custom payment flow using workflows or the Payment Module's main service. -*** +Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more. -## Inventory Module -The Inventory Module provides inventory-management features for any stock-kept item. +# Payment Collection -Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. +In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. -![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) +## What's a Payment Collection? -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 payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). -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). +Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: -### Retrieve with Query +- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. +- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. +- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. -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 +## Multiple Payments -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "inventory_items.*", - ], -}) +The payment collection supports multiple payment sessions and payments. -// variants[0].inventory_items -``` +You can use this to accept payments in increments or split payments across payment providers. -### useQueryGraphStep +![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Usage with the Cart Module -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "inventory_items.*", - ], -}) +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. -// variants[0].inventory_items -``` +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. -### Manage with Link +It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). -To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) -### link.create -```ts -import { Modules } from "@medusajs/framework/utils" +# Accept Payment in Checkout Flow -// ... +In this guide, you'll learn how to implement it using workflows or the Payment Module. -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` +## Why Implement the Payment Flow? -### createRemoteLinkStep +Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application. -// ... +This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows. -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` +You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application. *** -## Order Module +## How to Implement the Accept Payment Flow? -Medusa defines read-only links between: +For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). -- 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. +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. -### Retrieve with Query +### 1. Create a Payment Collection -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`: +A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. -To retrieve the product, pass `product.*` in `fields`. +In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for. -### query.graph +For example: + +### Using Workflow ```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) +import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" -// lineItems[0].variant +// ... + +await createPaymentCollectionForCartWorkflow(container) + .run({ + input: { + cart_id: "cart_123", + }, + }) ``` -### useQueryGraphStep +### Using Service ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) +``` -// ... +### 2. Show Payment Providers -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) +Next, you'll show the customer the available payment providers to choose from. -// lineItems[0].variant -``` +In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region. -*** +### Using Query -## Pricing Module +```ts +const query = container.resolve("query") -The Product Module doesn't provide pricing-related features. +const { data: regionPaymentProviders } = await query.graph({ + entryPoint: "region_payment_provider", + variables: { + filters: { + region_id: "reg_123", + }, + }, + fields: ["payment_providers.*"], +}) -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. +const paymentProviders = regionPaymentProviders.map( + (relation) => relation.payment_providers +) +``` -![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) +### Using Service -So, to add prices for a product variant, create a price set and add the prices to it. +```ts +const paymentProviders = await paymentModuleService.listPaymentProviders() +``` -### Retrieve with Query +### 3. Create Payment Sessions -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`: +The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. -### query.graph +So, once the customer selects a payment provider, create a payment session for the selected payment provider. -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) +This will also initialize the payment session in the third-party payment provider. -// variants[0].price_set -``` +For example: -### useQueryGraphStep +### Using Workflow ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "price_set.*", - ], -}) +const { result: paymentSesion } = await createPaymentSessionsWorkflow(container) + .run({ + input: { + payment_collection_id: "paycol_123", + provider_id: "pp_stripe_stripe", + }, + }) +``` -// variants[0].price_set +### Using Service + +```ts +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, + { + provider_id: "pp_stripe_stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider + }, + } + ) ``` -### Manage with Link +### 4. Authorize Payment Session -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Once the customer places the order, you need to authorize the payment session with the third-party payment provider. -### link.create +For example: + +### Using Step ```ts -import { Modules } from "@medusajs/framework/utils" +import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" // ... -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, +authorizePaymentSessionStep({ + id: "payses_123", + context: {}, }) ``` -### createRemoteLinkStep +### Using Service ```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", - }, +const payment = authorizePaymentSessionStep({ + id: "payses_123", + context: {}, }) ``` -*** - -## Sales Channel Module - -The Sales Channel Module provides functionalities to manage multiple selling channels in your store. +When the payment authorization is successful, a payment is created and returned. -Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. +#### Handling Additional Action -![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) +If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. -### Retrieve with Query +If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. -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`: +In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. -### query.graph +For example: ```ts -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "sales_channels.*", - ], -}) +try { + const payment = + await paymentModuleService.authorizePaymentSession( + paymentSession.id, + {} + ) +} catch (e) { + // retrieve the payment session again + const updatedPaymentSession = ( + await paymentModuleService.listPaymentSessions({ + id: [paymentSession.id], + }) + )[0] -// products[0].sales_channels + if (updatedPaymentSession.status === "requires_more") { + // TODO perform required action + // TODO authorize payment again. + } +} ``` -### useQueryGraphStep +### 5. Payment Flow Complete -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +The payment flow is complete once the payment session is authorized and the payment is created. -// ... +You can then: -const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "sales_channels.*", - ], -}) - -// products[0].sales_channels -``` - -### Manage with Link +- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application. +- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). +- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. -### link.create -```ts -import { Modules } from "@medusajs/framework/utils" +# Payment Module Provider -// ... +In this guide, you’ll learn about the Payment Module Provider and how it's used. -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. -### createRemoteLinkStep +*** -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +## What is a Payment Module Provider? -// ... +The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md). -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. +After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. -# Product Module +![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg) -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. +### List of Payment Module Providers -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard. +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) -Medusa has product related features available out-of-the-box through the Product 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 Product Module. +### Default Payment Provider -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. -## Product Features +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. -- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. -- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. -- [Bundled and Multi-Part Products](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. -- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for product variants with tiers and rules, allowing you to create complex pricing strategies. +The identifier of the system payment provider is `pp_system`. *** -## How to Use the Product Module +## How to Create a Custom Payment Provider? -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. +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. -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. +The module can have multiple payment provider services, where each is registered as a separate payment provider. -For example: +Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. -```ts title="src/workflows/create-product.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md). -const createProductStep = createStep( - "create-product", - async ({}, { container }) => { - const productService = container.resolve(Modules.PRODUCT) +*** - const product = await productService.createProducts({ - title: "Medusa Shirt", - options: [ - { - title: "Color", - values: ["Black", "White"], - }, - ], - variants: [ - { - title: "Black Shirt", - options: { - Color: "Black", - }, - }, - ], - }) +## How are Payment Providers Registered? - return new StepResponse({ product }, product.id) - }, - async (productId, { container }) => { - if (!productId) { - return - } - const productService = container.resolve(Modules.PRODUCT) +### Configure Payment Module's Providers - await productService.deleteProducts([productId]) - } -) +The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application. -export const createProductWorkflow = createWorkflow( - "create-product", - () => { - const { product } = createProductStep() +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide. - return new WorkflowResponse({ - product, - }) - } -) -``` +### Registration on Application Start -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module. -### API Route +For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register. -```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 { createProductWorkflow } from "../../workflows/create-product" +### PaymentProvider Data Model -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createProductWorkflow(req.scope) - .run() +A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application. - res.send(result) -} -``` +![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg) -### Subscriber +This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application. -```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 { createProductWorkflow } from "../workflows/create-product" +The `PaymentProvider` data model has the following properties: -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createProductWorkflow(container) - .run() +- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Payment Module Provider's service. + - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the payment provider is enabled. - console.log(result) -} +### How to Remove a Payment Provider? -export const config: SubscriberConfig = { - event: "user.created", -} -``` +If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record. -### Scheduled Job +Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createProductWorkflow } from "../workflows/create-product" -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createProductWorkflow(container) - .run() +# Stripe Module Provider - console.log(result) -} +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +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. -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +## 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) -# Configure Selling Products +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`: -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. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) +``` -The concepts in this guide are applicable starting from Medusa v2.5.1. +### Environment Variables -## Scenario +Make sure to add the necessary environment variables for the above options in `.env`: -Businesses can have different selling requirements: +```bash +STRIPE_API_KEY= +``` -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. +### Module Options -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. +|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|-| *** -## 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. +## Enable Stripe Providers in a Region -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. +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. -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. +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. -### 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. +## Stripe Payment Provider IDs -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 you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more. -When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: +Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where: -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. +- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider. +- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`. -*** +Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers: -## Use Case Examples +|Provider Name|Provider ID| +|---|---|---| +|Basic Stripe Payment|\`pp\_stripe\_stripe\`| +|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`| +|BLIK Payments|\`pp\_stripe-blik\_stripe\`| +|giropay Payments|\`pp\_stripe-giropay\_stripe\`| +|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`| -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.||| +## 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. -# Product Variant Inventory +### Webhook URL -# Product Variant Inventory +Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: -In this guide, you'll learn about the inventory management features related to product variants. +- `{server_url}` is the URL to your deployed Medusa application in server mode. +- `{provider_id}` is the ID of the provider as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix. -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. +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: -## Configure Inventory Management of Product Variants +|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\`| -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. +### Webhook Events -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 you set up the webhook in Stripe, choose the following events to listen to: -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. +- `payment_intent.amount_capturable_updated` +- `payment_intent.succeeded` +- `payment_intent.payment_failed` +- `payment_intent.partially_funded` (Since [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5)) *** -## How the Medusa Application Manages Inventory +## Useful Guides -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. +- [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). +- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md). -![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. +# Payment Session -![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) +In this document, you’ll learn what a payment session is. -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). +## What's a Payment Session? -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. +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. -![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) +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -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). +![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) -### 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. +## data Property -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. +Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider. -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. +The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized. -![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) +Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider. -*** +If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. -## Variant Back Orders +### data Property in the Storefront -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. +This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront. -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). +For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider. *** -## Additional Resources +## Payment Session Status -The following guides provide more details on inventory management in the Medusa application: +The `status` property of a payment session indicates its current status. Its value can be: -- [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. -- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant. -- [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). +- `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. -# Promotion Actions +# Payment -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). +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. -## computeActions Method +## What's a Payment? -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. +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. -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. +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. *** -## Action Types +## Capture Payments -### `addItemAdjustment` Action +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 `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. +The payment can also be captured incrementally, each time a capture record is created for that amount. -This action has the following format: +![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) -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string - is_tax_inclusive?: boolean -} -``` +*** -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. +## Refund Payments -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. +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. -### `removeItemAdjustment` Action +A payment can be refunded multiple times, and each time a refund record is created. -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. +![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) -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. +*** -This action has the following format: +## data Property -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` +Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider. -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. +The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment. -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. +Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider. -### `addShippingMethodAdjustment` Action +If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. -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: +# Payment Webhook Events -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} -``` +In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module. -This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. +## What's a Payment Webhook Event? -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. +A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status. -### `removeShippingMethodAdjustment` Action +This is useful in many cases such as: -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. +- When a payment is processed (authorized or captured) asynchronously. +- When a payment is managed on the third-party payment provider's side. +- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application. -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. +So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions. -This action has the following format: +*** -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} -``` +## How to Handle Payment Webhook Events -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. +### Webhook Listener API Route -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. +The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where: -### `campaignBudgetExceeded` Action +- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. +- `[provider]` is the ID of the provider. For example, `stripe`. -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. +For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. -This action has the following format: +You can use this webhook listener when configuring webhook events in your third-party payment provider. -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` +### getWebhookActionAndData Method -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. +The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider. +Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events. -# Application Method +![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) -In this document, you'll learn what an application method is. +If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions: -## What is an Application Method? +View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference. -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: +- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`. +- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`. +- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart. -|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 +# Pricing Concepts -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. +In this document, you’ll learn about the main concepts in the Pricing Module. -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. +## Price Set -![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) +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). -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. +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) *** -## Buy Promotion Rules +## Price List -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. +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. -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. -![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) +Its associated prices are represented by the `Price` data model. -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. +# Links between Pricing Module and Other Modules -# 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/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/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. +This document showcases the module links defined between the Pricing 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. +The Pricing Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|Cart|Promotion|Stored - many-to-many|Learn more| -|LineItemAdjustment|Promotion|Read-only - has one|Learn more| -|Order|Promotion|Stored - many-to-many|Learn more| +|ShippingOption|PriceSet|Stored - one-to-one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| *** -## Cart Module +## Fulfillment 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. +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 Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -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. +![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 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`. +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: promotions } = await query.graph({ - entity: "promotion", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "carts.*", + "shipping_option.*", ], }) -// promotions[0].carts +// priceSets[0].shipping_option ``` ### useQueryGraphStep @@ -29343,19 +31715,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "carts.*", + "shipping_option.*", ], }) -// promotions[0].carts +// priceSets[0].shipping_option ``` ### 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): +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 @@ -29365,11 +31737,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -29383,38 +31755,44 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` *** -## Order Module +## Product Module -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +The Product Module doesn't store or manage the prices of product variants. -![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) +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 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`: +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: promotions } = await query.graph({ - entity: "promotion", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "orders.*", + "variant.*", ], }) -// promotions[0].orders +// priceSets[0].variant ``` ### useQueryGraphStep @@ -29424,19 +31802,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "orders.*", + "variant.*", ], }) -// promotions[0].orders +// priceSets[0].variant ``` ### 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): +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 @@ -29446,11 +31824,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -29464,36 +31842,37 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` -# Promotion Module +# Pricing 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. +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. -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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists 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. +Medusa has pricing related features available out-of-the-box through the Pricing 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 Pricing 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 +## Pricing 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. +- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. +- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. +- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. +- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. +- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. *** -## How to Use the Promotion Module +## How to Use the Pricing 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. @@ -29501,7 +31880,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-promotion.ts" highlights={highlights} +```ts title="src/workflows/create-price-set.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -29510,41 +31889,46 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPromotionStep = createStep( - "create-promotion", +const createPriceSetStep = createStep( + "create-price-set", async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) + const pricingModuleService = container.resolve(Modules.PRICING) - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, - currency_code: "usd", - }, + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + { + amount: 500, + currency_code: "USD", + }, + { + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, + }, + ], }) - return new StepResponse({ promotion }, promotion.id) + return new StepResponse({ priceSet }, priceSet.id) }, - async (promotionId, { container }) => { - if (!promotionId) { + async (priceSetId, { container }) => { + if (!priceSetId) { return } - const promotionModuleService = container.resolve(Modules.PROMOTION) + const pricingModuleService = container.resolve(Modules.PRICING) - await promotionModuleService.deletePromotions(promotionId) + await pricingModuleService.deletePriceSets([priceSetId]) } ) -export const createPromotionWorkflow = createWorkflow( - "create-promotion", +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", () => { - const { promotion } = createPromotionStep() + const { priceSet } = createPriceSetStep() return new WorkflowResponse({ - promotion, + priceSet, }) } ) @@ -29559,13 +31943,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPromotionWorkflow(req.scope) + const { result } = await createPriceSetWorkflow(req.scope) .run() res.send(result) @@ -29579,13 +31963,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -29600,12 +31984,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -29622,1093 +32006,1748 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Tax-Inclusive Promotions - -In this guide, you’ll learn how taxes are applied to promotions in a cart. - -This feature is available from [Medusa v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5). - -## What are Tax-Inclusive Promotions? - -By default, promotions are tax-exclusive, meaning that the discount amount is applied as-is to the cart before taxes are calculated and applied to the cart total. +# Prices Calculation -A tax-inclusive promotion is a promotion for which taxes are calculated from the discount amount entered by the merchant. +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. -When a promotion is tax-inclusive, the discount amount is reduced by the calculated tax amount based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md). The reduced discount amount is then applied to the cart total. +## calculatePrices Method -Tax-inclusiveness doesn't apply to Buy X Get Y promotions. +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. -### When to Use Tax-Inclusive Promotions +It returns a price object with the best matching price for each price set. -Tax-inclusive promotions are most useful when using [tax-inclusive prices](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md) for items in the cart. +### Calculation Context -In this scenario, Medusa applies taxes consistently across the cart, ensuring that the total price reflects the taxes and promotions correctly. +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. -You can see this in action in the [examples below](#tax-inclusiveness-examples). +For example: -*** +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) +``` -## What Makes a Promotion Tax-Inclusive? +In this example, you retrieve the prices in a price set for the specified currency code and region ID. -The [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md) has an `is_tax_inclusive` property that determines whether the promotion is tax-inclusive. +### Returned Price Object -If `is_tax_inclusive` is disabled (which is the default), the promotion's discount amount will be applied as-is to the cart, before taxes are calculated. See an example in the [Tax-Exclusive Promotion Example](#tax-exclusive-promotion-example) section. +For each price set, the `calculatePrices` method selects two prices: -If `is_tax_inclusive` is enabled, the promotion's discount amount will first be reduced by the calculated tax amount (based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md)). The reduced discount amount is then applied to the cart total. See an example in the [Tax-Inclusive Promotion Example](#tax-inclusive-promotion-example) section. +- 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: -## How to Set a Promotion as Tax-Inclusive +- 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. -You can enable tax-inclusiveness for a promotion when [creating it in the Medusa Admin](https://docs.medusajs.com/user-guide/promotions/create/index.html.md). + - id: (\`string\`) The ID of the price. -You can set the `is_tax_inclusive` property when creating a promotion by using either the [Promotion workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/workflows/index.html.md) or the [Promotion Module's service](https://docs.medusajs.com/references/promotion/index.html.md). + - price\_list\_id: (\`string\`) The ID of the associated price list. -For most use cases, it's recommended to use [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) instead of directly using the module's service, as they implement the necessary rollback mechanisms in case of errors. + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -For example, if you're creating a promotion with the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) in an API route: + - min\_quantity: (\`number\`) The price's min quantity condition. -```ts highlights={[["17"]]} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPromotionsWorkflow } from "@medusajs/medusa/core-flows" + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createPromotionsWorkflow(req.scope) - .run({ - input: { - promotionsData: [{ - code: "10OFF", - // ... - is_tax_inclusive: true, - }], - } - }) + - id: (\`string\`) The ID of the price. - res.send(result) -} -``` + - price\_list\_id: (\`string\`) The ID of the associated price list. -In the above example, you set the `is_tax_inclusive` property to `true` when creating the promotion, making it tax-inclusive. + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -### Updating a Promotion's Tax-Inclusiveness + - min\_quantity: (\`number\`) The price's min quantity condition. -A promotion's tax-inclusiveness cannot be updated after it has been created. If you need to change a promotion's tax-inclusiveness, you must delete the existing promotion and create a new one with the desired `is_tax_inclusive` value. + - max\_quantity: (\`number\`) The price's max quantity condition. *** -## Tax-Inclusiveness Examples - -The following sections provide examples of how tax-inclusive promotions work in different scenarios, including both tax-exclusive and tax-inclusive promotions. - -These examples will help you understand how tax-inclusive promotions affect the cart total, allowing you to decide when to use them effectively. - -### Tax-Exclusive Promotion Example - -Consider the following scenario: - -- A tax-exclusive promotion gives a `$10` discount on the cart's total. -- The cart's tax region has a `25%` tax rate. -- The cart total before applying the promotion is `$100`. -- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). - -The result: - -1. Apply `$10` discount to cart total: `$100` - `$10` = `$90` -2. Calculate tax on discounted total: `$90` x `25%` = `$22.50` -3. Final total: `$90` + `$22.50` = `$112.50` - -### Tax-Inclusive Promotion Example - -Consider the following scenario: - -- A tax-inclusive promotion gives a `$10` discount on the cart's total. -- The cart's tax region has a `25%` tax rate. -- The cart total before applying the promotion is `$100`. -- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). +## Examples -The result: +Consider the following price set: -1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8` -2. Apply discount to cart total: `$100` - `$8` = `$92` -3. Calculate tax on discounted total: `$92` x `25%` = `$23` -4. Final total: `$92` + `$23` = `$115` +```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", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, + { + amount: 200, + currency_code: "EUR", + min_quantity: 100, + }, + ], +}) +``` -### Tax-Inclusive Promotions with Tax-Inclusive Prices +### Default Price Selection -Consider the following scenario: +### Code -- A tax-inclusive promotion gives a `$10` discount on the cart's total. -- The cart's tax region has a `25%` tax rate. -- The cart total before applying the promotion is `$100`. -- [The prices in the cart's tax region are tax-inclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` -The result: +### Result -1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8` -2. Calculate cart total without tax: `$100` ÷ `1.25` = `$80` -3. Apply discount to cart total without tax: `$80` - `$8` = `$72` -4. Add tax back to total: `$72` x `1.25` = `$90` +### Calculate Prices with Rules -The final total is `$90`, which correctly applies both the tax-inclusive promotion and tax-inclusive pricing. +### Code +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` -# Links between Region Module and Other Modules +### Result -This document showcases the module links defined between the Region Module and other Commerce Modules. +### Tiered Pricing Selection -## Summary +### Code -The Region Module has the following links to other modules: +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + cart: { + items: [ + { + id: "item_1", + quantity: 200, + // assuming the price set belongs to this variant + variant_id: "variant_1", + // ... + } + ], + // ... + } + } + } +) +``` -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +### Result -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|Region|Read-only - has one|Learn more| -|Order|Region|Read-only - has one|Learn more| -|Region|PaymentProvider|Stored - many-to-many|Learn more| +### Price Selection with Price List -*** +### Code -## Cart Module +```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, + }, + ], +}]); -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. +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` -### Retrieve with Query +### Result -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 +# Price Tiers and Rules -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) +In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. -// carts[0].region -``` +You can manage prices and tiers using the Medusa Admin: -### useQueryGraphStep +- [Edit shipping option prices with conditions](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#fixed-and-conditional-shipping-option-prices/index.html.md). +- [Add price lists to set prices with conditions for product variants](https://docs.medusajs.com/user-guide/price-lists/index.html.md). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Tiered Pricing -// ... +Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) +- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. +- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. -// carts[0].region +This is useful to set tiered pricing for resources like product variants and shipping options. + +For example, you can set a variant's price to: + +- `$10` by default. +- `$8` when the customer adds `10` or more of the variant to the cart. +- `$6` when the customer adds `20` or more of the variant to the cart. + +These price definitions would look like this: + +```json title="Example Prices" +[ + // default price + { + "amount": 10, + "currency_code": "usd", + }, + { + "amount": 8, + "currency_code": "usd", + "min_quantity": 10, + "max_quantity": 19, + }, + { + "amount": 6, + "currency_code": "usd", + "min_quantity": 20, + }, +], ``` -*** +### How to Create Tiered Prices? -## Order Module +When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. -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. +For example: -### Retrieve with Query +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. -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`: +### Using Medusa Workflows -### query.graph +```ts highlights={tieredPricingHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +// ... + +const { result } = await createProductsWorkflow(container) + .run({ + input: { + products: [{ + variants: [{ + id: "variant_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, + ], + // ... + }], + }], + // ... + }, + }) +``` + +### Using the Pricing Module ```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + // tiered prices + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, ], }) - -// orders[0].region ``` -### useQueryGraphStep +In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### How are Tiered Prices 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 the cart's items as a context when choosing the best price to apply. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) +For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. -// orders[0].region -``` +The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. -*** +If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. -## Payment Module +*** -You can specify for each region which payment providers are available for use. +## Price Rule -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. +You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. -![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) +Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). -### Retrieve with Query +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. -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`: +For exmaple, you create a price restricted to `10557` zip codes. -### query.graph +![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) -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) +A price can have multiple price rules. -// regions[0].payment_providers -``` +For example, a price can be restricted by a region and a zip code. -### useQueryGraphStep +![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) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### 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). -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. -// regions[0].payment_providers -``` +![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) -### Manage with Link +### How to Create Prices with Rules? -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. -### link.create +For example: -```ts -import { Modules } from "@medusajs/framework/utils" +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. -// ... +### Using Medusa Workflows -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) +```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: [ + { + attribute: "item_total", + operator: "gte", + value: 100, + }, + ], + }, + ], + }], + }) ``` -### createRemoteLinkStep +### Using the Pricing Module ```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", - }, +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, + }, + }, + }, + ], }) ``` +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. -# Region Module +### How is the Price Rule Applied? -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. +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. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. +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: -Medusa has region related features available out-of-the-box through the Region 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 Region Module. +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "item_total": { + "operator": "gte", + "value": 100, + } + } +} +``` -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +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: -## Region Features +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "customer.group.id": { + "operator": "eq", + "value": "cusgrp_123" + } + } +} +``` -- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. -- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. -- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. +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. -## How to Use Region 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. +## Examples -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. +This section provides some examples of implementing price tiers and rules for products and shipping options. -For example: +### Product Variant Price by Quantity -```ts title="src/workflows/create-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +### Using Medusa Workflows -const createRegionStep = createStep( - "create-region", - async ({}, { container }) => { - const regionModuleService = container.resolve(Modules.REGION) +```ts +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - const region = await regionModuleService.createRegions({ - name: "Europe", - currency_code: "eur", - }) +// ... - return new StepResponse({ region }, region.id) - }, - async (regionId, { container }) => { - if (!regionId) { - return - } - const regionModuleService = container.resolve(Modules.REGION) - - await regionModuleService.deleteRegions([regionId]) - } -) - -export const createRegionWorkflow = createWorkflow( - "create-region", - () => { - const { region } = createRegionStep() - - return new WorkflowResponse({ - region, - }) - } -) +const { result } = await createProductsWorkflow(container) + .run({ + input: { + products: [{ + title: "Premium T-Shirt", + variants: [{ + title: "Small / Black", + prices: [ + // default price + { + amount: 20, + currency_code: "usd", + }, + // buy 5 or more, get a small discount + { + amount: 18, + currency_code: "usd", + min_quantity: 5, + max_quantity: 9, + }, + // buy 10 or more, get a bigger discount + { + amount: 15, + currency_code: "usd", + min_quantity: 10, + }, + ], + }], + }], + }, + }) ``` -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 { createRegionWorkflow } from "../../workflows/create-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createRegionWorkflow(req.scope) - .run() +### Using the Pricing Module - res.send(result) -} +```ts +const priceSet = await pricingModule.addPrices({ + // this is the price set of the product variant + priceSetId: "pset_product_123", + prices: [ + // default price: $20.00 + { + amount: 20, + currency_code: "usd", + }, + // buy 5-9 shirts: $18.00 each + { + amount: 18, + currency_code: "usd", + min_quantity: 5, + max_quantity: 9, + }, + // buy 10+ shirts: $15.00 each + { + amount: 15, + currency_code: "usd", + min_quantity: 10, + }, + ], +}) ``` -### Subscriber +### Product Variant Price by Customer Group -```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 { createRegionWorkflow } from "../workflows/create-region" +### Using Medusa Workflows -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createRegionWorkflow(container) - .run() +```ts +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - console.log(result) -} +// ... -export const config: SubscriberConfig = { - event: "user.created", -} +const { result } = await createProductsWorkflow(container) + .run({ + input: { + products: [{ + title: "Premium T-Shirt", + variants: [{ + title: "Small / Black", + prices: [ + // default price + { + amount: 40, + currency_code: "usd", + }, + // discounted price for VIP customers + { + amount: 30, + currency_code: "usd", + rules: { + "customer.groups.id": "cusgrp_vip123", + }, + }, + // special price for wholesale customers + { + amount: 20, + currency_code: "usd", + rules: { + "customer.groups.id": "cusgrp_wholesale456", + }, + }, + ], + }], + }], + }, + }) ``` -### Scheduled Job +### Using the Pricing Module -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createRegionWorkflow } from "../workflows/create-region" +```ts +const priceSet = await pricingModule.addPrices({ + // this is the price set of the product variant + priceSetId: "pset_product_123", + prices: [ + // default price + { + amount: 40, + currency_code: "usd", + }, + // discounted price for VIP customers + { + amount: 30, + currency_code: "usd", + rules: { + "customer.groups.id": "cusgrp_vip123", + }, + }, + // special price for wholesale customers + { + amount: 20, + currency_code: "usd", + rules: { + "customer.groups.id": "cusgrp_wholesale456", + }, + }, + ], +}) +``` -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createRegionWorkflow(container) - .run() +### Free Shipping for Orders Above a Certain Amount - console.log(result) -} +### Using Medusa Workflows -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} +```ts +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 (2-5 business days)", + code: "standard", + }, + price_type: "flat", + prices: [ + // regular shipping price + { + currency_code: "usd", + amount: 10, + rules: [], + }, + // free shipping for carts over $100 + { + currency_code: "usd", + amount: 0, + rules: [ + { + attribute: "item_total", + operator: "gte", + value: 100, + }, + ], + }, + ], + }], + }) ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** +### Using the Pricing Module +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_shipping_123", + prices: [ + // regular shipping price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // free shipping for orders over $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: [ + { + operator: "gte", + value: 100, + }, + ], + }, + }, + ], +}) +``` -# Links between Sales Channel Module and Other Modules +### Shipping Prices Based on Customer Groups -This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. +### Using Medusa Workflows -## Summary +```ts +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Express Shipping", + service_zone_id: "serzo_456", + shipping_profile_id: "sp_456", + provider_id: "prov_456", + type: { + label: "Express", + description: "Express shipping (1-2 business days)", + code: "express", + }, + price_type: "flat", + prices: [ + // regular express shipping price + { + currency_code: "usd", + amount: 20, + rules: [], + }, + // discounted express shipping for VIP customers + { + currency_code: "usd", + amount: 15, + rules: [ + { + attribute: "customer.groups.id", + operator: "in", + value: ["cusgrp_vip123"], + }, + ], + }, + // special rate for B2B customers + { + currency_code: "usd", + amount: 13, + rules: [ + { + attribute: "customer.groups.id", + operator: "in", + value: ["cusgrp_b2b789"], + }, + ], + }, + ], + }], + }) +``` -The Sales Channel Module has the following links to other modules: +### Using the Pricing Module -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_shipping_456", + prices: [ + // regular express shipping + { + currency_code: "usd", + amount: 20, + rules: {}, + }, + // VIP customer express shipping + { + currency_code: "usd", + amount: 15, + rules: { + "customer.groups.id": [ + { + operator: "in", + value: ["cusgrp_vip123"], + }, + ], + }, + }, + // B2B customer express shipping + { + currency_code: "usd", + amount: 13, + rules: { + "customer.groups.id": [ + { + operator: "in", + value: ["cusgrp_b2b789"], + }, + ], + }, + }, + ], +}) +``` -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|ApiKey|SalesChannel|Stored - many-to-many|Learn more| -|Cart|SalesChannel|Read-only - has one|Learn more| -|Order|SalesChannel|Read-only - has one|Learn more| -|Product|SalesChannel|Stored - many-to-many|Learn more| -|SalesChannel|StockLocation|Stored - many-to-many|Learn more| +### Combined Tiered and Rule-Based Shipping Pricing -*** +### Using Medusa Workflows -## API Key Module +```ts +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Bulk Shipping", + service_zone_id: "serzo_789", + shipping_profile_id: "sp_789", + provider_id: "prov_789", + type: { + label: "Bulk", + description: "Shipping for bulk orders", + code: "bulk", + }, + price_type: "flat", + prices: [ + // base shipping price + { + currency_code: "usd", + amount: 20, + rules: [], + }, + // discounted shipping for 5-10 items + { + currency_code: "usd", + amount: 18, + min_quantity: 5, + max_quantity: 10, + rules: [], + }, + // heavily discounted shipping for 10+ items + { + currency_code: "usd", + amount: 15, + min_quantity: 11, + rules: [], + }, + // free shipping for VIP customers with carts over $200 + { + currency_code: "usd", + amount: 0, + rules: { + "customer.groups.id": [ + { + operator: "in", + value: ["cusgrp_vip123"], + }, + ], + item_total: [ + { + operator: "gte", + value: 200, + }, + ], + }, + }, + ], + }], + }) +``` -A publishable API key allows you to easily specify the sales channel scope in a client request. +### Using the Pricing Module -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_shipping_789", + prices: [ + // base shipping price: $20.00 + { + currency_code: "usd", + amount: 20, + rules: {}, + }, + // 5-10 items: $18.00 + { + currency_code: "usd", + amount: 18, + min_quantity: 5, + max_quantity: 10, + rules: {}, + }, + // 11+ items: $15.00 + { + currency_code: "usd", + amount: 15, + min_quantity: 11, + rules: {}, + }, + // VIP customers with orders over $200: FREE + { + currency_code: "usd", + amount: 0, + rules: [ + { + attribute: "customer.groups.id", + operator: "in", + value: ["cusgrp_vip123"], + }, + { + attribute: "item_total", + operator: "gte", + value: 200, + }, + ], + }, + ], +}) +``` -![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) +### Shipping Prices Based on Geographical Rules -### Retrieve with Query +### Using Medusa Workflows -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`: +```ts +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Regional Shipping", + service_zone_id: "serzo_geo123", + shipping_profile_id: "sp_standard", + provider_id: "prov_standard", + type: { + label: "Standard", + description: "Standard shipping with regional pricing", + code: "standard_regional", + }, + price_type: "flat", + prices: [ + // default shipping price + { + currency_code: "usd", + amount: 15, + rules: [], + }, + // special price for specific zip codes (metropolitan areas) + { + currency_code: "usd", + amount: 10, + rules: [ + { + attribute: "shipping_address.postal_code", + operator: "in", + value: ["10001", "10002", "10003", "90001", "90002", "90003"], + }, + ], + }, + // higher price for remote areas + { + currency_code: "usd", + amount: 25, + rules: [ + { + attribute: "shipping_address.postal_code", + operator: "in", + value: ["99501", "99502", "99503", "00601", "00602", "00603"], + }, + ], + }, + // different price for a specific region + { + currency_code: "usd", + amount: 12, + rules: [ + { + attribute: "region.id", + operator: "eq", + value: "reg_123", + }, + ], + }, + // different price for a specific country + { + currency_code: "usd", + amount: 20, + rules: [ + { + attribute: "shipping_address.country_code", + operator: "eq", + value: "ca", + }, + ], + }, + ], + }], + }) +``` -### query.graph +### Using the Pricing Module ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_shipping_geo", + prices: [ + // default shipping price: $15.00 + { + currency_code: "usd", + amount: 15, + rules: {}, + }, + // metropolitan areas: $10.00 + { + currency_code: "usd", + amount: 10, + rules: { + "shipping_address.postal_code": [ + { + operator: "in", + value: ["10001", "10002", "10003", "90001", "90002", "90003"], + }, + ], + }, + }, + // remote areas: $25.00 + { + currency_code: "usd", + amount: 25, + rules: { + "shipping_address.postal_code": [ + { + operator: "in", + value: ["99501", "99502", "99503", "00601", "00602", "00603"], + }, + ], + }, + }, + // Northeast region: $12.00 + { + currency_code: "usd", + amount: 12, + rules: { + "region.id": "reg_123", + }, + }, + // Canada: $20.00 + { + currency_code: "usd", + amount: 20, + rules: { + "shipping_address.country_code": "ca", + }, + }, ], }) - -// salesChannels[0].publishable_api_keys ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -// ... +# Tax-Inclusive Pricing -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. -// salesChannels[0].publishable_api_keys -``` +## What is Tax-Inclusive Pricing? -### Manage with Link +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. -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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. -### link.create +*** -```ts -import { Modules } from "@medusajs/framework/utils" +## 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: -await link.create({ - [Modules.API_KEY]: { - publishable_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +- `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`. -### createRemoteLinkStep +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. -// ... +For example: -createRemoteLinkStep({ - [Modules.API_KEY]: { - publishable_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} ``` +In this example, tax-inclusivity is enabled for the `USD` currency code. + *** -## Cart Module +## Tax-Inclusive Pricing in Price Calculation -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. +### Tax Context -### Retrieve with Query +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 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`: +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. -### query.graph +### Returned Tax Properties -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +The `calculatePrices` method returns two properties related to tax-inclusivity: -// carts[0].sales_channel -``` +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). -### useQueryGraphStep +- `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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +### Tax Context Precedence -// carts[0].sales_channel -``` +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 *** -## Order Module +## Tax-Inclusive Pricing with Promotions -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. +When you enable tax-inclusive prices for regions or currencies, this can impact how promotions are applied to the cart. So, it's recommended to enable tax-inclusiveness for promotions as well. -### Retrieve with Query +Learn more in the [Tax-Inclusive Promotions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/promotion-taxes/index.html.md) guide. -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 +# Calculate Product Variant Price with Taxes + +In this document, you'll learn how to calculate a product variant's price with taxes. + +## Step 0: Resolve Resources + +You'll need the following resources for the taxes calculation: + +1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). +2. The Tax Module's main service to get the tax lines for each product. ```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +// other imports... +import { + Modules, + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -// orders.sales_channel +// In an API route, workflow step, etc... +const query = container.resolve(ContainerRegistrationKeys.QUERY) +const taxModuleService = container.resolve( + Modules.TAX +) ``` -### useQueryGraphStep +*** + +## Step 1: Retrieve Prices for a Context + +After resolving the resources, use Query to retrieve the products with the variants' prices for a context: + +Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { QueryContext } from "@medusajs/framework/utils" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: products } = await query.graph({ + entity: "product", fields: [ - "sales_channel.*", + "*", + "variants.*", + "variants.calculated_price.*", ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "region_123", + currency_code: "usd", + }), + }, + }, }) - -// orders.sales_channel ``` *** -## Product Module +## Step 2: Get Tax Lines for Products -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. +To retrieve the tax line of each product, first, add the following utility method: -![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) +```ts +// other imports... +import { + HttpTypes, + TaxableItemDTO, +} from "@medusajs/framework/types" -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. +// ... +const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { + return product.variants + ?.map((variant) => { + if (!variant.calculated_price) { + return + } -### Retrieve with Query + return { + id: variant.id, + product_id: product.id, + product_name: product.title, + product_categories: product.categories?.map((c) => c.name), + product_category_id: product.categories?.[0]?.id, + product_sku: variant.sku, + product_type: product.type, + product_type_id: product.type_id, + quantity: 1, + unit_price: variant.calculated_price.calculated_amount, + currency_code: variant.calculated_price.currency_code, + } + }) + .filter((v) => !!v) as unknown as TaxableItemDTO[] +} +``` -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`: +This formats the products as items to calculate tax lines for. -### query.graph +Then, use it when retrieving the tax lines of the products retrieved earlier: ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) +// other imports... +import { + ItemTaxLineDTO, +} from "@medusajs/framework/types" -// salesChannels[0].products +// ... +const taxLines = (await taxModuleService.getTaxLines( + products.map(asTaxItem).flat(), + { + // example of context properties. You can pass other ones. + address: { + country_code, + }, + } +)) as unknown as ItemTaxLineDTO[] ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) +For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. -// salesChannels[0].products -``` +For the second parameter, you pass the current context. You can pass other details such as the customer's ID. -### Manage with Link +Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). -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 +## Step 3: Calculate Price with Tax for Variant -```ts -import { Modules } from "@medusajs/framework/utils" +To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: -// ... +```ts highlights={taxLineHighlights} +const taxLinesMap = new Map() +taxLines.forEach((taxLine) => { + const variantId = taxLine.line_item_id + if (!taxLinesMap.has(variantId)) { + taxLinesMap.set(variantId, []) + } -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, + taxLinesMap.get(variantId)?.push(taxLine) }) ``` -### createRemoteLinkStep +Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +Then, loop over the products and their variants to retrieve the prices with and without taxes: + +```ts highlights={calculateTaxHighlights} +// other imports... +import { + calculateAmountsWithTax, +} from "@medusajs/framework/utils" // ... +products.forEach((product) => { + product.variants?.forEach((variant) => { + if (!variant.calculated_price) { + return + } -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, + const taxLinesForVariant = taxLinesMap.get(variant.id) || [] + const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ + taxLines: taxLinesForVariant, + amount: variant.calculated_price!.calculated_amount!, + includesTax: + variant.calculated_price!.is_calculated_price_tax_inclusive!, + }) + + // do something with prices... + }) }) ``` -*** +For each product variant, you: -## Stock Location Module +1. Retrieve its tax lines from the `taxLinesMap`. +2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. +3. The `calculateAmountsWithTax` function returns an object having two properties: + - `priceWithTax`: The variant's price with the taxes applied. + - `priceWithoutTax`: The variant's price without taxes applied. -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. +# Get Product Variant Prices using Query -![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) +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). -### Retrieve with Query +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. -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`: +So, to retrieve data across the linked records of the two modules, you use Query. -### query.graph +## Retrieve All Product Variant Prices -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +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: [ - "stock_locations.*", + "*", + "variants.*", + "variants.prices.*", ], + filters: { + id: [ + "prod_123", + ], + }, }) - -// salesChannels[0].stock_locations ``` -### useQueryGraphStep +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). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Retrieve Calculated Price for a Context -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) +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. -// salesChannels[0].stock_locations -``` +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). -### Manage with Link +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +- 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. -### link.create +For example: -```ts -import { Modules } from "@medusajs/framework/utils" +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" // ... -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, }, }) ``` -### createRemoteLinkStep +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`. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +`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). -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` +# Get Product Variant Inventory Quantity -# Sales Channel Module +In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -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. +Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard. +## Understanding Product Variant Inventory Availability -Medusa has sales channel related features available out-of-the-box through the Sales Channel 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 Sales Channel Module. +Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant. -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products. -## What's a Sales Channel? +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. -A sales channel indicates an online or offline channel that you sell products on. +This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled. -Some use case examples for using a sales channel: +*** -- Implement a B2B Ecommerce Store. -- Specify different products for each channel you sell in. -- Support omnichannel in your ecommerce store. +## Retrieve Product Variant Inventory -*** +To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant. -## Sales Channel Features +For example: -- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. -- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. -- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. -- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. +```ts highlights={variantAvailabilityHighlights} +import { getVariantAvailability } from "@medusajs/framework/utils" -*** +// ... -## How to Use Sales Channel Module's Service +// use req.scope instead of container in API routes +const query = container.resolve("query") -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. +const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: "sc_123", +}) +``` -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. +A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md). -For example: +So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in. -```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function. -const createSalesChannelStep = createStep( - "create-sales-channel", - async ({}, { container }) => { - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) +### Parameters - const salesChannels = await salesChannelModuleService.createSalesChannels([ - { - name: "B2B", - }, - { - name: "Mobile App", - }, - ]) +The `getVariantAvailability` function accepts the following parameters: - return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) - }, - async (salesChannelIds, { container }) => { - if (!salesChannelIds) { - return - } - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) +- query: (Query) Instance of Query to retrieve the necessary data. +- options: (\`object\`) The options to retrieve the variant availability. - await salesChannelModuleService.deleteSalesChannels( - salesChannelIds - ) - } -) + - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability. -export const createSalesChannelWorkflow = createWorkflow( - "create-sales-channel", - () => { - const { salesChannels } = createSalesChannelStep() + - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in. - return new WorkflowResponse({ - salesChannels, - }) - } -) -``` +### Returns -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter. -### API Route +The value of each key is an object with the following properties: -```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 { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" +- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`. +- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createSalesChannelWorkflow(req.scope) - .run() +For example, the object may look like this: - res.send(result) +```json title="Example result" +{ + "variant_123": { + "availability": 10, + "sales_channel_id": "sc_123" + } } ``` -### 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 { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +## Retrieve Sales Channel to Use -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createSalesChannelWorkflow(container) - .run() +To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either: - console.log(result) -} +- Use the sales channel of the request's scope. +- Use the sales channel that the variant's product is available in. -export const config: SubscriberConfig = { - event: "user.created", -} -``` +### Method 1: Use Sales Channel Scope in Store Routes -### Scheduled Job +Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object: -export default async function myCustomJob( - container: MedusaContainer +```ts highlights={salesChannelScopeHighlights} +import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" +import { getVariantAvailability } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaStoreRequest, + res: MedusaResponse ) { - const { result } = await createSalesChannelWorkflow(container) - .run() + const query = req.scope.resolve("query") + const sales_channel_ids = req.publishable_key_context.sales_channel_ids - console.log(result) -} + const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: sales_channel_ids[0], + }) -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, + res.json({ + availability, + }) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. -*** +Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. +Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. -# Publishable API Keys with Sales Channels +### Method 2: Use Product's Sales Channel -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. +A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels. -## Publishable API Keys with Sales Channels +For example: -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. +```ts highlights={productSalesChannelHighlights} +import { getVariantAvailability } from "@medusajs/framework/utils" -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} +// use req.scope instead of container in API routes +const query = container.resolve("query") + +const { data: variants } = await query.graph({ + entity: "variant", + fields: ["id", "product.sales_channels.*"], + filters: { + id: "variant_123", + }, +}) + +const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: variants[0].product!.sales_channels![0]!.id, +}) ``` -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. +In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -*** +You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product. -## How to Create a Publishable API Key? +Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. -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 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| +|---|---|---|---| +|LineItem|Product|Read-only - has one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| +|OrderLineItem|Product|Read-only - has one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| +|Product|SalesChannel|Stored - many-to-many|Learn more| *** -## Access Sales Channels in Custom Store API Routes +## Cart Module -If you create an API route under the `/store` prefix, you can access the sales channels associated with the request's publishable API key using the `publishable_key_context` property of the request object. +Medusa defines read-only links between: -For example: +- 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 -import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" -import { getVariantAvailability } from "@medusajs/framework/utils" +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -export async function GET( - req: MedusaStoreRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") - const sales_channel_ids = req.publishable_key_context.sales_channel_ids +// lineItems[0].variant +``` - res.json({ - sales_channel_id: sales_channel_ids[0], - }) -} +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems[0].variant ``` -In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. +*** -You can then use these IDs based on your business logic. For example, you can retrieve the sales channels' details using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +## Fulfillment Module -Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. +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). -# Stock Location Concepts +### Retrieve with Query -In this document, you’ll learn about the main concepts in the Stock Location Module. +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`: -## Stock Location +### query.graph -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. +// products[0].shipping_profile +``` -*** +### useQueryGraphStep -## StockLocationAddress +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +// ... +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "shipping_profile.*", + ], +}) -# Links between Stock Location Module and Other Modules +// products[0].shipping_profile +``` -This document showcases the module links defined between the Stock Location Module and other Commerce Modules. +### Manage with Link -## Summary +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -The Stock Location Module has the following links to other modules: +### link.create -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +```ts +import { Modules } from "@medusajs/framework/utils" -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more| -|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more| -|InventoryLevel|StockLocation|Read-only - has many|Learn more| -|SalesChannel|StockLocation|Stored - many-to-many|Learn more| +// ... + +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", + }, +}) +``` *** -## Fulfillment Module +## Inventory Module -A fulfillment set can be conditioned to a specific stock location. +The Inventory Module provides inventory-management features for any stock-kept item. -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. +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 Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) +![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) -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +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 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) +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 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`. +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: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: variants } = await query.graph({ + entity: "variant", fields: [ - "fulfillment_sets.*", + "inventory_items.*", ], }) -// stockLocations[0].fulfillment_sets +// variants[0].inventory_items ``` ### useQueryGraphStep @@ -30718,19 +33757,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: variants } = useQueryGraphStep({ + entity: "variant", fields: [ - "fulfillment_sets.*", + "inventory_items.*", ], }) -// stockLocations[0].fulfillment_sets +// variants[0].inventory_items ``` ### 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): +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 @@ -30740,11 +33779,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` @@ -30758,36 +33797,41 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` *** -## Inventory Module +## Order 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 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 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`: +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: inventoryLevels } = await query.graph({ - entity: "inventory_level", +const { data: lineItems } = await query.graph({ + entity: "order_line_item", fields: [ - "stock_locations.*", + "variant.*", ], }) -// inventoryLevels[0].stock_locations +// lineItems[0].variant ``` ### useQueryGraphStep @@ -30797,41 +33841,126 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", fields: [ - "stock_locations.*", + "variant.*", ], }) -// inventoryLevels[0].stock_locations +// 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 -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +The Sales Channel Module provides functionalities to manage multiple selling channels in your store. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +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 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) +![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 stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: +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: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: products } = await query.graph({ + entity: "product", fields: [ "sales_channels.*", ], }) -// stockLocations[0].sales_channels +// products[0].sales_channels ``` ### useQueryGraphStep @@ -30841,19 +33970,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: products } = useQueryGraphStep({ + entity: "product", fields: [ "sales_channels.*", ], }) -// stockLocations[0].sales_channels +// products[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): +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 @@ -30863,12 +33992,12 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, [Modules.SALES_CHANNEL]: { sales_channel_id: "sc_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, }) ``` @@ -30881,34 +34010,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, [Modules.SALES_CHANNEL]: { sales_channel_id: "sc_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, }) ``` -# Stock Location Module +# Product 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 Product 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/products/index.html.md) to learn how to manage products 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 product related features available out-of-the-box through the Product 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 Product 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 +## Product 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. +- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. +- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](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. +- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for product variants with tiers and rules, allowing you to create complex pricing strategies. *** -## How to Use Stock Location Module's Service +## How to Use the Product 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. @@ -30916,7 +34047,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-product.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -30925,33 +34056,49 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createProductStep = createStep( + "create-product", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const productService = container.resolve(Modules.PRODUCT) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", + const product = await productService.createProducts({ + title: "Medusa Shirt", + options: [ + { + title: "Color", + values: ["Black", "White"], + }, + ], + variants: [ + { + title: "Black Shirt", + options: { + Color: "Black", + }, + }, + ], }) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ product }, product.id) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (productId, { container }) => { + if (!productId) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const productService = container.resolve(Modules.PRODUCT) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await productService.deleteProducts([productId]) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createProductWorkflow = createWorkflow( + "create-product", () => { - const { stockLocation } = createStockLocationStep() + const { product } = createProductStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ + product, + }) } ) ``` @@ -30965,13 +34112,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createProductWorkflow } from "../../workflows/create-product" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createProductWorkflow(req.scope) .run() res.send(result) @@ -30985,13 +34132,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createProductWorkflow } from "../workflows/create-product" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -31006,12 +34153,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 { createProductWorkflow } from "../workflows/create-product" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -31028,663 +34175,687 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Links between Store Module and Other Modules +# Configure Selling Products -This document showcases the module links defined between the Store Module and other Commerce Modules. +In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, their type, how you want to sell them, or your commerce ecosystem. -## Summary +The concepts in this guide are applicable starting from [Medusa v2.5.1](https://github.com/medusajs/medusa/releases/tag/v2.5.1). -The Store Module has the following links to other modules: +## Scenario -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +Businesses can have different selling requirements. They may sell: -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|StoreCurrency|Currency|Read-only - has many|Learn more| +1. Physical or digital items. +2. Items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. +3. 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. *** -## Currency Module +## Configuring Shipping Requirements -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. +### Product Shipping Requirement -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/StoreCurrency/index.html.md) data model in the Store Module (not in the Currency Module). +The Medusa application defines a link between the `Product` data model and the [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) data model 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. -### Retrieve with Query +![Diagram showcasing the link between a product and its shipping profile](https://res.cloudinary.com/dza7lstvk/image/upload/v1752827367/Medusa%20Resources/product-shipping-requirement_cpfpkz.jpg) -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`: +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. -### query.graph +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. -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +### Overriding Shipping Requirements for Variants -// stores[0].supported_currencies -``` +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). -### useQueryGraphStep +The inventory item has a `requires_shipping` property that can be used to override the variant's 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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +![Diagram showcasing the link between a product variant and its inventory item, and the inventory item's shipping requirement](https://res.cloudinary.com/dza7lstvk/image/upload/v1752828341/Medusa%20Resources/product-variant-shipping-requirement_ux5y44.jpg) -// ... +Refer to the [Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md) guide to learn more. -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping based on the following conditions (in the following order): -// stores[0].supported_currencies -``` +1. If the product variant has an inventory item, 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. +![Diagram showcasing the conditions that determine whether a product variant requires shipping](https://res.cloudinary.com/dza7lstvk/image/upload/v1752828969/Medusa%20Resources/shipping-requirement-check_fdh6lp.jpg) -# Store Module +### Shipping Requirements vs Shipping Options -In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. +The shipping options that you retrieve during checkout depend on the cart's shipping address. So, whether the items in the cart require shipping doesn't affect what shipping options are available at checkout. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. +This is a common misconception, where you expect to not receive any shipping options at checkout if the cart doesn't have any items that require shipping. However, the Medusa application always returns shipping options based on the cart's shipping address. -Medusa has store related features available out-of-the-box through the Store 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 Store Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Store Features +If you want to show the shipping options only if the cart has items that require shipping, you can either: -- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. -- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. +- Create a custom [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that checks whether the cart has items that require shipping. +- Perform this check in your storefront's frontend code, such as in the checkout page, and show or hide the shipping options accordingly. *** -## How to Use Store 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-store.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createStoreStep = createStep( - "create-store", - async ({}, { container }) => { - const storeModuleService = container.resolve(Modules.STORE) +## Use Case Examples - const store = await storeModuleService.createStores({ - name: "My Store", - supported_currencies: [{ - currency_code: "usd", - is_default: true, - }], - }) +By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: - return new StepResponse({ store }, store.id) - }, - async (storeId, { container }) => { - if(!storeId) { - return - } - const storeModuleService = container.resolve(Modules.STORE) - - await storeModuleService.deleteStores([storeId]) - } -) +|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.||| -export const createStoreWorkflow = createWorkflow( - "create-store", - () => { - const { store } = createStoreStep() - return new WorkflowResponse({ store }) - } -) -``` +# Product Variant Inventory -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +# Product Variant Inventory -### API Route +In this guide, you'll learn about the inventory management features related to product variants. -```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 { createStoreWorkflow } from "../../workflows/create-store" +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. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createStoreWorkflow(req.scope) - .run() +## Configure Inventory Management of Product Variants - res.send(result) -} -``` +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. -### Subscriber +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. -```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 { createStoreWorkflow } from "../workflows/create-store" +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. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createStoreWorkflow(container) - .run() +*** - console.log(result) -} +## How the Medusa Application Manages Inventory -export const config: SubscriberConfig = { - event: "user.created", -} -``` +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. -### Scheduled Job +![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) -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createStoreWorkflow } from "../workflows/create-store" +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. -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createStoreWorkflow(container) - .run() +![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) - console.log(result) -} +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). -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +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. -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +![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 -# Tax Module Options +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. -In this guide, you'll learn about the options of the Tax Module. +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. -## providers +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. -The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) or path to a file that defines a tax provider. +![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) -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" +## 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. -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` +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). -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. +## Additional Resources +The following guides provide more details on inventory management in the Medusa application: -# Tax Module +- [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. +- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant. +- [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). -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. -Refer to the [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. +# Promotion Actions -Medusa has tax related features available out-of-the-box through the Tax 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 Tax Module. +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). -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +## computeActions Method -## Tax Features +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. -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. -- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region. +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. *** -## How to Use Tax Module's Service +## Action Types -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. +### `addItemAdjustment` Action -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. +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. -For example: +This action has the following format: -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +```ts +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string + is_tax_inclusive?: boolean +} +``` -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) +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. - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) +### `removeItemAdjustment` Action - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. - return new WorkflowResponse({ taxRegion }) - } -) +This action has the following format: + +```ts +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. -### API Route +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. -```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 { createTaxRegionWorkflow } from "../../workflows/create-tax-region" +### `addShippingMethodAdjustment` Action -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createTaxRegionWorkflow(req.scope) - .run() +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. - res.send(result) +This action has the following format: + +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string } ``` -### Subscriber +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. -```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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) - .run() +### `removeShippingMethodAdjustment` Action - console.log(result) -} +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. -export const config: SubscriberConfig = { - event: "user.created", +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 } ``` -### Scheduled Job +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createTaxRegionWorkflow(container) - .run() +### `campaignBudgetExceeded` Action - console.log(result) -} +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, +This action has the following format: + +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Tax Module - -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. - -*** +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. -# Tax Calculation with the Tax Provider +# Application Method -In this guide, you’ll learn how tax lines are calculated using the tax provider. +In this document, you'll learn what an application method is. -## Tax Lines Calculation +## What is an Application Method? -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. +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: -For example: +|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?| -```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", - }, - } -) -``` +## Target Promotion Rules -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. +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 example above retrieves the tax lines based on the tax region for the United States. +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. -The method returns tax lines for the line item and shipping methods. For example: +![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) -```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" - } -] -``` +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. *** -## Using the Tax Provider in the Calculation +## Buy Promotion Rules -The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md). +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. -A tax module implements the logic to shape tax lines. Each tax region uses a tax provider. +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. -Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. +![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. -# Tax Module Provider -In this guide, you’ll learn about the Tax Module Provider and how it's used. +# Campaign -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 provider of a tax region using the dashboard. +In this document, you'll learn about campaigns. -## What is a Tax Module Provider? +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. -The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic. +## What is a Campaign? -The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md). +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. -![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg) +![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) *** -## Default Tax Provider +## Campaign Limits -The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide. +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. -This provider is installed by default in your application and you can use it with tax regions. +There are two types of budgets: -The identifier of the system tax provider is `tp_system`. +- `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) -## How to Create a Custom Tax Provider? -A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`. +# Promotion Concepts -The module can have multiple tax provider services, where each are registered as separate tax providers. +In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. -Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider. +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. -After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md). +## 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. -## How are Tax Providers Registered? +A promotion has two types: -### Configure Tax Module's Providers +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. -The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application. +|\`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.| -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide. +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. -### Registration on Application Start +*** -When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module. +## Promotion Rules -For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register. +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). -### TaxProvider Data Model +For example, you can create a promotion that only customers of the `VIP` customer group can use. -A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application. +![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) -This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application. +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. -![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg) +The expected value for the attribute is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. -The `TaxProvider` data model has the following properties: +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. -- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Tax Module Provider's service. - - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the tax provider is enabled. +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. -### How to Remove a Tax Provider? +### Flexible Rules -You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration. +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). -Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option. +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`. -# Tax Rates and Rules +![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 document, you’ll learn about tax rates and rules. +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. -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? +## How to Apply Rules on a Promotion? -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. +### Using Workflows -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. +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). -### Combinable Tax Rates +For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md): -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`. +```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", + ], + }, + ], + }], + }, + }) +``` -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. +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. -*** +### Using Promotion Module's Service -## Override Tax Rates with Rules +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. -You can create tax rates that override the default for specific conditions or rules. +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, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. +For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/references/promotion/createPromotions/index.html.md) method: -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. +```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", + ], + }, + ], + }, +]) +``` -![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) +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. -These two properties of the data model identify the rule’s target: +### How is the Promotion Rule Applied? -- `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. +A promotion is applied on a resource if its attributes match the promotion's rules. -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. +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`. -# Tax Region -In this document, you’ll learn about tax regions and how to use them with the Region Module. +# Links between Promotion Module and Other Modules -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. +This document showcases the module links defined between the Promotion Module and other Commerce Modules. -## What is a Tax Region? +## Summary -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. +The Promotion Module has the following links to other modules: -Tax regions can inherit settings and rules from a parent tax region. +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| +|---|---|---|---| +|Cart|Promotion|Stored - many-to-many|Learn more| +|LineItemAdjustment|Promotion|Read-only - has one|Learn more| +|Order|Promotion|Stored - many-to-many|Learn more| *** -## Tax Rules in a Tax Region +## Cart Module -Tax rules define the tax rates and behavior within a tax region. They specify: +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. -- The tax rate percentage. -- Which products the tax applies to. -- Other custom rules to determine tax applicability. +![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) -Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide. +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 -## Tax Provider +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`: -Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region. +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. -You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic. +### query.graph -Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) +// promotions[0].carts +``` -# User Module Options +### useQueryGraphStep -In this document, you'll learn about the options of the User Module. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Module Options +// ... -```ts title="medusa-config.ts" +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" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, +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 ``` -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| +### useQueryGraphStep -### Environment Variables +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -Make sure to add the necessary environment variables for the above options in `.env`: +// ... -```bash -JWT_SECRET=supersecret +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions[0].orders ``` +### Manage with Link -# User Module +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -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. +### link.create -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. +```ts +import { Modules } from "@medusajs/framework/utils" -Medusa has user related features available out-of-the-box through the User 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 User Module. +// ... + +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", + }, +}) +``` + + +# 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). -## User Features +## Promotion Features -- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. -- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. +- [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 User 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. @@ -31692,7 +34863,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-user.ts" highlights={highlights} +```ts title="src/workflows/create-promotion.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -31701,36 +34872,41 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createUserStep = createStep( - "create-user", +const createPromotionStep = createStep( + "create-promotion", async ({}, { container }) => { - const userModuleService = container.resolve(Modules.USER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - const user = await userModuleService.createUsers({ - email: "user@example.com", - first_name: "John", - last_name: "Smith", + 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({ user }, user.id) + return new StepResponse({ promotion }, promotion.id) }, - async (userId, { container }) => { - if (!userId) { + async (promotionId, { container }) => { + if (!promotionId) { return } - const userModuleService = container.resolve(Modules.USER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - await userModuleService.deleteUsers([userId]) + await promotionModuleService.deletePromotions(promotionId) } ) -export const createUserWorkflow = createWorkflow( - "create-user", +export const createPromotionWorkflow = createWorkflow( + "create-promotion", () => { - const { user } = createUserStep() + const { promotion } = createPromotionStep() return new WorkflowResponse({ - user, + promotion, }) } ) @@ -31745,13 +34921,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createUserWorkflow } from "../../workflows/create-user" +import { createPromotionWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createUserWorkflow(req.scope) + const { result } = await createPromotionWorkflow(req.scope) .run() res.send(result) @@ -31765,13 +34941,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createUserWorkflow } from "../workflows/create-user" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createUserWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -31786,12 +34962,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createUserWorkflow } from "../workflows/create-user" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createUserWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -31807,1731 +34983,1639 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure User Module - -The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. - -*** +# Tax-Inclusive Promotions -# User Creation Flows +In this guide, you’ll learn how taxes are applied to promotions in a cart. -In this document, learn the different ways to create a user using the User Module. +This feature is available from [Medusa v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5). -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. +## What are Tax-Inclusive Promotions? -## Straightforward User Creation +By default, promotions are tax-exclusive, meaning that the discount amount is applied as-is to the cart before taxes are calculated and applied to the cart total. -To create a user, use the [createUsers method of the User Module’s main service](https://docs.medusajs.com/references/user/createUsers/index.html.md): +A tax-inclusive promotion is a promotion for which taxes are calculated from the discount amount entered by the merchant. -```ts -const user = await userModuleService.createUsers({ - email: "user@example.com", -}) -``` +When a promotion is tax-inclusive, the discount amount is reduced by the calculated tax amount based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md). The reduced discount amount is then applied to the cart total. -You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). +Tax-inclusiveness doesn't apply to Buy X Get Y promotions. -*** +### When to Use Tax-Inclusive Promotions -## Invite Users +Tax-inclusive promotions are most useful when using [tax-inclusive prices](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md) for items in the cart. -To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: +In this scenario, Medusa applies taxes consistently across the cart, ensuring that the total price reflects the taxes and promotions correctly. -```ts -const invite = await userModuleService.createInvites({ - email: "user@example.com", -}) -``` +You can see this in action in the [examples below](#tax-inclusiveness-examples). -Later, you can accept the invite and create a new user for them: +*** -```ts -const invite = - await userModuleService.validateInviteToken("secret_123") +## What Makes a Promotion Tax-Inclusive? -await userModuleService.updateInvites({ - id: invite.id, - accepted: true, -}) +The [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md) has an `is_tax_inclusive` property that determines whether the promotion is tax-inclusive. -const user = await userModuleService.createUsers({ - email: invite.email, -}) -``` +If `is_tax_inclusive` is disabled (which is the default), the promotion's discount amount will be applied as-is to the cart, before taxes are calculated. See an example in the [Tax-Exclusive Promotion Example](#tax-exclusive-promotion-example) section. -### Invite Expiry +If `is_tax_inclusive` is enabled, the promotion's discount amount will first be reduced by the calculated tax amount (based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md)). The reduced discount amount is then applied to the cart total. See an example in the [Tax-Inclusive Promotion Example](#tax-inclusive-promotion-example) section. -An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): +*** -```ts -await userModuleService.refreshInviteTokens(["invite_123"]) -``` +## How to Set a Promotion as Tax-Inclusive -*** +You can enable tax-inclusiveness for a promotion when [creating it in the Medusa Admin](https://docs.medusajs.com/user-guide/promotions/create/index.html.md). -## Create Identity with the Auth Module +You can set the `is_tax_inclusive` property when creating a promotion by using either the [Promotion workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/workflows/index.html.md) or the [Promotion Module's service](https://docs.medusajs.com/references/promotion/index.html.md). -By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. +For most use cases, it's recommended to use [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) instead of directly using the module's service, as they implement the necessary rollback mechanisms in case of errors. -So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: +For example, if you're creating a promotion with the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) in an API route: -```ts -const { success, authIdentity } = - await authModuleService.authenticate("emailpass", { - // ... - }) +```ts highlights={[["17"]]} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPromotionsWorkflow } from "@medusajs/medusa/core-flows" -const [, count] = await userModuleService.listAndCountUsers({ - email: authIdentity.entity_id, -}) +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPromotionsWorkflow(req.scope) + .run({ + input: { + promotionsData: [{ + code: "10OFF", + // ... + is_tax_inclusive: true, + }], + }, + }) -if (!count) { - const user = await userModuleService.createUsers({ - email: authIdentity.entity_id, - }) + res.send(result) } ``` +In the above example, you set the `is_tax_inclusive` property to `true` when creating the promotion, making it tax-inclusive. -# Local Analytics Module Provider - -The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes. +### Updating a Promotion's Tax-Inclusiveness -The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). +A promotion's tax-inclusiveness cannot be updated after it has been created. If you need to change a promotion's tax-inclusiveness, you must delete the existing promotion and create a new one with the desired `is_tax_inclusive` value. *** -## Register the Local Analytics Module +## Tax-Inclusiveness Examples -Add the module into the `provider` object of the Analytics Module: +The following sections provide examples of how tax-inclusive promotions work in different scenarios, including both tax-exclusive and tax-inclusive promotions. -You can use only one Analytics Module Provider in your Medusa application. +These examples will help you understand how tax-inclusive promotions affect the cart total, allowing you to decide when to use them effectively. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/analytics", - options: { - providers: [ - { - resolve: "@medusajs/analytics-local", - id: "local", - }, - ], - }, - }, - ], -}) -``` +### Tax-Exclusive Promotion Example -*** +Consider the following scenario: -## Test out the Module +- A tax-exclusive promotion gives a `$10` discount on the cart's total. +- The cart's tax region has a `25%` tax rate. +- The cart total before applying the promotion is `$100`. +- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). -To test the module out, you'll track in the console when an order is placed. +The result: -You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. +1. Apply `$10` discount to cart total: `$100` - `$10` = `$90` +2. Calculate tax on discounted total: `$90` x `25%` = `$22.50` +3. Final total: `$90` + `$22.50` = `$112.50` -For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: +### Tax-Inclusive Promotion Example -```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { OrderDTO } from "@medusajs/framework/types" +Consider the following scenario: -type StepInput = { - order: OrderDTO -} +- A tax-inclusive promotion gives a `$10` discount on the cart's total. +- The cart's tax region has a `25%` tax rate. +- The cart total before applying the promotion is `$100`. +- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). -const trackOrderCreatedStep = createStep( - "track-order-created-step", - async ({ order }: StepInput, { container }) => { - const analyticsModuleService = container.resolve(Modules.ANALYTICS) +The result: - await analyticsModuleService.track({ - event: "order_created", - userId: order.customer_id, - properties: { - order_id: order.id, - total: order.total, - items: order.items.map((item) => ({ - variant_id: item.variant_id, - product_id: item.product_id, - quantity: item.quantity, - })), - customer_id: order.customer_id, - }, - }) - } -) +1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8` +2. Apply discount to cart total: `$100` - `$8` = `$92` +3. Calculate tax on discounted total: `$92` x `25%` = `$23` +4. Final total: `$92` + `$23` = `$115` -type WorkflowInput = { - order_id: string -} +### Tax-Inclusive Promotions with Tax-Inclusive Prices -export const trackOrderCreatedWorkflow = createWorkflow( - "track-order-created-workflow", - ({ order_id }: WorkflowInput) => { - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "*", - "customer.*", - "items.*", - ], - filters: { - id: order_id, - }, - }) - trackOrderCreatedStep({ - order: orders[0], - }) - } -) -``` +Consider the following scenario: -This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. +- A tax-inclusive promotion gives a `$10` discount on the cart's total. +- The cart's tax region has a `25%` tax rate. +- The cart total before applying the promotion is `$100`. +- [The prices in the cart's tax region are tax-inclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md). -In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event. +The result: -Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: +1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8` +2. Calculate cart total without tax: `$100` ÷ `1.25` = `$80` +3. Apply discount to cart total without tax: `$80` - `$8` = `$72` +4. Add tax back to total: `$72` x `1.25` = `$90` -```ts title="src/subscribers/order-placed.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" +The final total is `$90`, which correctly applies both the tax-inclusive promotion and tax-inclusive pricing. -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await trackOrderCreatedWorkflow(container).run({ - input: { - order_id: data.id, - }, - }) -} -export const config: SubscriberConfig = { - event: "order.placed", -} -``` +# Links between Region Module and Other Modules -This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. +This document showcases the module links defined between the Region Module and other Commerce Modules. -You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the console for the tracked event. +## Summary -*** +The Region Module has the following links to other modules: -## Additional Resources +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Cart|Region|Read-only - has one|Learn more| +|Order|Region|Read-only - has one|Learn more| +|Region|PaymentProvider|Stored - many-to-many|Learn more| +*** -# Analytics Module +## Cart Module -In this document, you'll learn about the Analytics Module and its providers. +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. -The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). +### Retrieve with Query -## What is the Analytics Module? +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`: -The Analytics Module exposes functionalities to track and analyze user interactions and system events with third-party services. For example, you can track cart updates or completed orders. +### query.graph -In your Medusa application, you can use the Analytics Module to send data to third-party analytics services like PostHog or Segment, enabling you to gain insights into user behavior and system performance. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) -![Diagram showcasing the flow of tracking an event like order.placed](https://res.cloudinary.com/dza7lstvk/image/upload/v1747832107/Medusa%20Resources/analytics-module-overview_egz7xg.jpg) +// carts[0].region +``` -*** +### useQueryGraphStep -## How to Use the Analytics Module? +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### Configure Analytics Module Provider +// ... -To use the Analytics Module, you need to configure it along with an Analytics Module Provider. +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) -An Analytics Module Provider implements the underlying logic of sending analytics data. It integrates with a third-party analytics service to send the data collected through the Analytics Module. +// carts[0].region +``` -Medusa provides two Analytics Module Providers: Local and PostHog module providers. +*** -You can also [create a custom Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) that integrates with a third-party service, like Segment. +## Order Module -- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md) -- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md) +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. -[Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md): undefined +### Retrieve with Query -To configure the Analytics Module and its provider, add it to the list of modules in your `medusa-config.ts` file. For example: +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`: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/analytics", - options: { - providers: [ - { - resolve: "@medusajs/medusa/analytics-local", - id: "local", - }, - ], - }, - }, +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", ], }) + +// orders[0].region ``` -Refer to the documentation of each provider for specific configuration options. +### useQueryGraphStep -### Track Events +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -To track an event, you can use the Analytics Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +// ... -In a step of your workflow, you can resolve the Analytics Module's service and use its methods to track events or identify users. +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) -For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: +// orders[0].region +``` -```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { OrderDTO } from "@medusajs/framework/types" +*** -type StepInput = { - order: OrderDTO -} +## Payment Module -const trackOrderCreatedStep = createStep( - "track-order-created-step", - async ({ order }: StepInput, { container }) => { - const analyticsModuleService = container.resolve(Modules.ANALYTICS) +You can specify for each region which payment providers are available for use. - await analyticsModuleService.track({ - event: "order_created", - userId: order.customer_id, - properties: { - order_id: order.id, - total: order.total, - items: order.items.map((item) => ({ - variant_id: item.variant_id, - product_id: item.product_id, - quantity: item.quantity, - })), - customer_id: order.customer_id, - }, - }) - } -) +Medusa defines a module link between the `PaymentProvider` and the `Region` data models. -type WorkflowInput = { - order_id: string -} +![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) -export const trackOrderCreatedWorkflow = createWorkflow( - "track-order-created-workflow", - ({ order_id }: WorkflowInput) => { - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "*", - "customer.*", - "items.*", - ], - filters: { - id: order_id, - }, - }) - trackOrderCreatedStep({ - order: orders[0], - }) - } -) -``` +### Retrieve with Query -This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. +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`: -In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured in `medusa-config.ts` to track the event. +### query.graph -### Execute Analytics Workflow +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) -After that, you can execute this workflow in a subscriber that runs when a product is created. +// regions[0].payment_providers +``` -create a subscriber at `src/subscribers/order-placed.ts` with the following content: +### useQueryGraphStep -```ts title="src/subscribers/order-placed.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await trackOrderCreatedWorkflow(container).run({ - input: { - order_id: data.id, - }, - }) -} +// ... -export const config: SubscriberConfig = { - event: "order.placed", -} -``` +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) -This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. +// regions[0].payment_providers +``` -You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the provider you integrated with (for example, PostHog) for the tracked event. +### 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): -# PostHog Analytics Module Provider +### link.create -The PostHog Analytics Module Provider allows you to integrate [PostHog](https://posthog.com/) with Medusa. +```ts +import { Modules } from "@medusajs/framework/utils" -PostHog is an open-source product analytics platform that helps you track user interactions and analyze user behavior in your commerce application. +// ... -By integrating PostHog with Medusa, you can track events such as cart additions, order completions, and user sign-ups, enabling you to gain insights into user behavior and optimize your application accordingly. +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). +### createRemoteLinkStep -*** +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -## Register the PostHog Analytics Module +// ... -### Prerequisites +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -- [PostHog account](https://app.posthog.com/signup) -- [PostHog API Key](https://posthog.com/docs/getting-started/api-key) -Add the module into the `provider` object of the Analytics Module: +# Region Module -You can use only one provider in your Medusa application. +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. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/analytics", - options: { - providers: [ - { - resolve: "@medusajs/analytics-posthog", - id: "posthog", - options: { - posthogEventsKey: process.env.POSTHOG_EVENTS_API_KEY, - posthogHost: process.env.POSTHOG_HOST, - }, - }, - ], - }, - }, - ], -}) -``` +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. -### Environment Variables +Medusa has region related features available out-of-the-box through the Region 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 Region Module. -Make sure to add the following environment variables: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```bash -POSTHOG_EVENTS_API_KEY= -POSTHOG_HOST= -``` +*** -### PostHog Analytics Module Options +## Region Features -|Option|Description|Default| -|---|---|---| -|\`eventsKey\`|The PostHog API key for tracking events. This is required to authenticate your requests to the PostHog API.|-| -|\`posthogHost\`|The PostHog API host URL.|\`https://eu.i.posthog.com\`| +- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. +- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. +- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. *** -## Test out the Module +## How to Use Region Module's Service -To test the module out, you'll track in PostHog when an order is placed. +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'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. +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, create a workflow at `src/workflows/track-order-placed.ts` with the following content: +For example: -```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { createStep } from "@medusajs/framework/workflows-sdk" +```ts title="src/workflows/create-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -import { OrderDTO } from "@medusajs/framework/types" - -type StepInput = { - order: OrderDTO -} -const trackOrderCreatedStep = createStep( - "track-order-created-step", - async ({ order }: StepInput, { container }) => { - const analyticsModuleService = container.resolve(Modules.ANALYTICS) +const createRegionStep = createStep( + "create-region", + async ({}, { container }) => { + const regionModuleService = container.resolve(Modules.REGION) - await analyticsModuleService.track({ - event: "order_created", - userId: order.customer_id, - properties: { - order_id: order.id, - total: order.total, - items: order.items.map((item) => ({ - variant_id: item.variant_id, - product_id: item.product_id, - quantity: item.quantity, - })), - customer_id: order.customer_id, - }, + const region = await regionModuleService.createRegions({ + name: "Europe", + currency_code: "eur", }) + + return new StepResponse({ region }, region.id) + }, + async (regionId, { container }) => { + if (!regionId) { + return + } + const regionModuleService = container.resolve(Modules.REGION) + + await regionModuleService.deleteRegions([regionId]) } ) -type WorkflowInput = { - order_id: string -} +export const createRegionWorkflow = createWorkflow( + "create-region", + () => { + const { region } = createRegionStep() -export const trackOrderCreatedWorkflow = createWorkflow( - "track-order-created-workflow", - ({ order_id }: WorkflowInput) => { - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "*", - "customer.*", - "items.*", - ], - filters: { - id: order_id, - }, - }) - trackOrderCreatedStep({ - order: orders[0], + return new WorkflowResponse({ + region, }) } ) ``` -This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. - -In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the PostHog Analytics Module Provider, in this case) to track the event. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: +### API Route -```ts title="src/subscribers/order-placed.ts" +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { - SubscriberArgs, - SubscriberConfig, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createRegionWorkflow } from "../../workflows/create-region" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createRegionWorkflow(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 { trackOrderCreatedWorkflow } from "../workflows/track-order-created" +import { createRegionWorkflow } from "../workflows/create-region" -export default async function orderPlacedHandler({ +export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - await trackOrderCreatedWorkflow(container).run({ - input: { - order_id: data.id, - }, - }) + const { result } = await createRegionWorkflow(container) + .run() + + console.log(result) } export const config: SubscriberConfig = { - event: "order.placed", + event: "user.created", } ``` -This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. +### Scheduled Job -You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking your PostHog dashboard for the tracked event. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createRegionWorkflow } from "../workflows/create-region" -*** +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createRegionWorkflow(container) + .run() -## Additional Resources + console.log(result) +} -- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) +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). -# How to Create a Cache Module +*** -In this guide, you’ll learn how to create a Cache Module. -## 1. Create Module Directory +# Links between Sales Channel Module and Other Modules -Start by creating a new directory for your module. For example, `src/modules/my-cache`. +This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. -*** +## Summary -## 2. Create the Cache Service +The Sales Channel Module has the following links to other modules: -Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`: +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ApiKey|SalesChannel|Stored - many-to-many|Learn more| +|Cart|SalesChannel|Read-only - has one|Learn more| +|Order|SalesChannel|Read-only - has one|Learn more| +|Product|SalesChannel|Stored - many-to-many|Learn more| +|SalesChannel|StockLocation|Stored - many-to-many|Learn more| -```ts title="src/modules/my-cache/service.ts" -import { ICacheService } from "@medusajs/framework/types" +*** -class MyCacheService implements ICacheService { - get(key: string): Promise { - throw new Error("Method not implemented.") - } - set(key: string, data: unknown, ttl?: number): Promise { - throw new Error("Method not implemented.") - } - invalidate(key: string): Promise { - throw new Error("Method not implemented.") - } -} +## API Key Module -export default MyCacheService -``` +A publishable API key allows you to easily specify the sales channel scope in a client request. -The service implements the required methods based on the desired caching mechanism. +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -### Implement get Method +![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) -The `get` method retrieves the value of a cached item based on its key. +### Retrieve with Query -The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist. +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`: -For example, to implement this method using Memcached: +### query.graph -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - // ... - async get(cacheKey: string): Promise { - return new Promise((res, rej) => { - this.memcached.get(cacheKey, (err, data) => { - if (err) { - res(null) - } else { - if (data) { - res(JSON.parse(data)) - } else { - res(null) - } - } - }) - }) - } -} +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels[0].publishable_api_keys ``` -### Implement set Method +### useQueryGraphStep -The `set` method is used to set an item in the cache. It accepts three parameters: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item. -2. The second parameter is the data to be added to the cache. The data can be of any type. -3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache. +// ... -For example, to implement this method using Memcached: +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - protected TTL = 60 - // ... - async set( - key: string, - data: Record, - ttl: number = this.TTL // or any value - ): Promise { - return new Promise((res, rej) => - this.memcached.set( - key, JSON.stringify(data), ttl, (err) => { - if (err) { - rej(err) - } else { - res() - } - }) - ) - } -} +// salesChannels[0].publishable_api_keys ``` -### Implement invalidate Method +### Manage with Link -The `invalidate` method removes an item from the cache using its key. +To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand. +### link.create -The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache. +```ts +import { Modules } from "@medusajs/framework/utils" -For example, to implement this method using Memcached: +// ... -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - // ... - async invalidate(key: string): Promise { - return new Promise((res, rej) => { - this.memcached.del(key, (err) => { - if (err) { - rej(err) - } else { - res() - } - }) - }) - } -} +await link.create({ + [Modules.API_KEY]: { + publishable_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` -*** - -## 3. Create Module Definition File +### createRemoteLinkStep -Create the file `src/modules/my-cache/index.ts` with the following content: +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -```ts title="src/modules/my-cache/index.ts" -import MyCacheService from "./service" -import { Module } from "@medusajs/framework/utils" +// ... -export default Module("my-cache", { - service: MyCacheService, +createRemoteLinkStep({ + [Modules.API_KEY]: { + publishable_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, }) ``` -This exports the module's definition, indicating that the `MyCacheService` is the main service of the module. - *** -## 4. Use Module +## Cart Module -To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key. +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. -For example: +### Retrieve with Query -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +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 -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/my-cache", - options: { - // any options - ttl: 30, - }, - }, +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", ], }) + +// carts[0].sales_channel ``` +### useQueryGraphStep -# In-Memory Cache Module +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application. +// ... -This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) -For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md). +// carts[0].sales_channel +``` *** -## Register the In-Memory Cache Module +## Order Module -The In-Memory Cache Module is registered by default in your application. +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. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +### Retrieve with Query -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" -// ... +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`: -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-inmemory", - options: { - // optional options - }, - }, +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", ], }) + +// orders.sales_channel ``` -### In-Memory Cache Module Options +### useQueryGraphStep -|Option|Description|Default| -|---|---|---|---|---| -|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`| +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +// ... -# Cache Module +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) -In this document, you'll learn what a Cache Module is and how to use it in your Medusa application. +// orders.sales_channel +``` -## What is a Cache Module? +*** -A Cache Module is used to cache the results of computations such as price selection or various tax calculations. +## Product Module -The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture. +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. -### Default Cache Module +![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) -By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. -*** +### Retrieve with Query -## How to Use the Cache Module? +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`: -You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +### query.graph -In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache. +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) -For example: +// salesChannels[0].products +``` + +### useQueryGraphStep ```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, -} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const cacheModuleService = container.resolve( - Modules.CACHE - ) +// ... - await cacheModuleService.set("key", "value") - } -) +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) -export const workflow = createWorkflow( - "workflow-1", - () => { - step1() - } -) +// salesChannels[0].products ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +### Manage with Link -Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`. +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 -## List of Cache Modules +```ts +import { Modules } from "@medusajs/framework/utils" -Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). +// ... -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` +### createRemoteLinkStep -# Redis Cache Module +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module. +// ... -Our Cloud offering automatically provisions a Redis instance and configures the Redis Cache Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` *** -## Register the Redis Cache Module +## Stock Location Module -### Prerequisites +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. -- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +![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) -```ts title="medusa-config.ts" highlights={highlights} -import { Modules } from "@medusajs/framework/utils" +### 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`: -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.CACHE_REDIS_URL, - }, - }, +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "stock_locations.*", ], }) + +// salesChannels[0].stock_locations ``` -### Environment Variables +### useQueryGraphStep -Make sure to add the following environment variables: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -```bash -CACHE_REDIS_URL= -``` +// ... -### Redis Cache Module Options +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], +}) -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| -|\`redisOptions\`|An object of Redis options. Refer to the |No|-| -|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`| -|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`| +// salesChannels[0].stock_locations +``` -*** +### Manage with Link -## Test the Module +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -To test the module, start the Medusa application: +### link.create -```bash npm2yarn -npm run dev -``` +```ts +import { Modules } from "@medusajs/framework/utils" -You'll see the following message in the terminal's logs: +// ... -```bash noCopy noReport -Connection to Redis in module 'cache-redis' established +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) ``` +### createRemoteLinkStep -# How to Create an Event Module +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -In this guide, you’ll learn how to create an Event Module. +// ... -## 1. Create Module Directory +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` -Start by creating a new directory for your module. For example, `src/modules/my-event`. -*** +# Sales Channel Module -## 2. Create the Event Service +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. -Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard. -The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework: +Medusa has sales channel related features available out-of-the-box through the Sales Channel 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 Sales Channel Module. -```ts title="src/modules/my-event/service.ts" -import { AbstractEventBusModuleService } from "@medusajs/framework/utils" -import { Message } from "@medusajs/types" +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -class MyEventService extends AbstractEventBusModuleService { - async emit(data: Message | Message[], options: Record): Promise { - throw new Error("Method not implemented.") - } - async releaseGroupedEvents(eventGroupId: string): Promise { - throw new Error("Method not implemented.") - } - async clearGroupedEvents(eventGroupId: string): Promise { - throw new Error("Method not implemented.") - } -} +## What's a Sales Channel? -export default MyEventService -``` +A sales channel indicates an online or offline channel that you sell products on. -The service implements the required methods based on the desired publish/subscribe logic. +Some use case examples for using a sales channel: -### eventToSubscribersMap\_ Property +- Implement a B2B Ecommerce Store. +- Specify different products for each channel you sell in. +- Support omnichannel in your ecommerce store. -The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions. - -In your custom implementation, you can use this property to manage the subscribed handler functions: - -```ts -const eventSubscribers = - this.eventToSubscribersMap_.get(eventName) || [] -``` - -### emit Method - -The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks. - -An example implementation: - -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - async emit(data: Message | Message[], options: Record): Promise { - const events = Array.isArray(data) ? data : [data] - - for (const event of events) { - console.log(`Received the event ${event.name} with data ${event.data}`) - - // TODO push the event somewhere - } - } - // ... -} -``` +*** -The `emit` method receives the following parameters: +## Sales Channel Features -- data: (\`object or array of objects\`) The emitted event(s). +- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. +- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. +- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. +- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. - - name: (\`string\`) The name of the emitted event. +*** - - data: (\`object\`) The data payload of the event. +## How to Use Sales Channel Module's Service - - metadata: (\`object\`) Additional details of the emitted event. +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. - - eventGroupId: (string) A group ID that the event belongs to. +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. - - options: (\`object\`) Additional options relevant for the event service. +For example: -### releaseGroupedEvents Method +```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events. +const createSalesChannelStep = createStep( + "create-sales-channel", + async ({}, { container }) => { + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) -If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group. + const salesChannels = await salesChannelModuleService.createSalesChannels([ + { + name: "B2B", + }, + { + name: "Mobile App", + }, + ]) -For example: + return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) + }, + async (salesChannelIds, { container }) => { + if (!salesChannelIds) { + return + } + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - protected groupedEventsMap_: Map + await salesChannelModuleService.deleteSalesChannels( + salesChannelIds + ) + } +) - constructor() { - // @ts-ignore - super(...arguments) +export const createSalesChannelWorkflow = createWorkflow( + "create-sales-channel", + () => { + const { salesChannels } = createSalesChannelStep() - this.groupedEventsMap_ = new Map() + return new WorkflowResponse({ + salesChannels, + }) } +) +``` - async releaseGroupedEvents(eventGroupId: string): Promise { - const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || [] +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - for (const event of groupedEvents) { - const { options, ...eventBody } = event +### API Route - // TODO emit event - } +```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 { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" - await this.clearGroupedEvents(eventGroupId) - } +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createSalesChannelWorkflow(req.scope) + .run() - // ... + res.send(result) } ``` -The `releaseGroupedEvents` receives the group ID as a parameter. +### Subscriber -In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next. +```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 { createSalesChannelWorkflow } from "../workflows/create-sales-channel" -To add events to the grouped events map, you can do it in the `emit` method: +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createSalesChannelWorkflow(container) + .run() -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - // ... - async emit(data: Message | Message[], options: Record): Promise { - const events = Array.isArray(data) ? data : [data] + console.log(result) +} - for (const event of events) { - console.log(`Received the event ${event.name} with data ${event.data}`) +export const config: SubscriberConfig = { + event: "user.created", +} +``` - if (event.metadata.eventGroupId) { - const groupedEvents = this.groupedEventsMap_.get( - event.metadata.eventGroupId - ) || [] +### Scheduled Job - groupedEvents.push(event) +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" - this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents) - continue - } +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createSalesChannelWorkflow(container) + .run() - // TODO push the event somewhere - } - } + console.log(result) } -``` -### clearGroupedEvents Method +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -If your Event Module supports grouped events, this method is used to remove the events of a group. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -For example: +*** -```ts title="src/modules/my-event/service.ts" -class MyEventService extends AbstractEventBusModuleService { - // from previous section - protected groupedEventsMap_: Map - async clearGroupedEvents(eventGroupId: string): Promise { - this.groupedEventsMap_.delete(eventGroupId) - } +# 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. -The method accepts the group's name as a parameter. +## Publishable API Keys with Sales Channels -In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well. +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: -## 3. Create Module Definition File +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} +``` -Create the file `src/modules/my-event/index.ts` with the following content: +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. -```ts title="src/modules/my-event/index.ts" -import MyEventService from "./service" -import { Module } from "@medusajs/framework/utils" +*** -export default Module("my-event", { - service: MyEventService, -}) -``` +## How to Create a Publishable API Key? -This exports the module's definition, indicating that the `MyEventService` is the main service of the module. +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). *** -## 4. Use Module +## Access Sales Channels in Custom Store API Routes -To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key. +If you create an API route under the `/store` prefix, you can access the sales channels associated with the request's publishable API key using the `publishable_key_context` property of the request object. For example: -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +```ts +import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" +import { getVariantAvailability } from "@medusajs/framework/utils" -// ... +export async function GET( + req: MedusaStoreRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") + const sales_channel_ids = req.publishable_key_context.sales_channel_ids -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/my-event", - options: { - // any options - }, - }, - ], -}) + res.json({ + sales_channel_id: sales_channel_ids[0], + }) +} ``` +In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. -# Local Event Module +You can then use these IDs based on your business logic. For example, you can retrieve the sales channels' details using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment. +Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. -This module is useful for development and testing, but it’s not recommended to be used in production. -For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md). +# Stock Location Concepts -*** +In this document, you’ll learn about the main concepts in the Stock Location Module. -## Register the Local Event Module +## Stock Location -The Local Event Module is registered by default in your application. +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +*** -// ... +## StockLocationAddress -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/event-bus-local", - }, - ], -}) -``` +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. -*** -## Test the Module +# Links between Stock Location Module and Other Modules -To test the module, start the Medusa application: +This document showcases the module links defined between the Stock Location Module and other Commerce Modules. -```bash npm2yarn -npm run dev -``` +## Summary -You'll see the following message in the terminal's logs: +The Stock Location Module has the following links to other modules: -```bash noCopy noReport -Local Event Bus installed. This is not recommended for production. -``` +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| +|---|---|---|---| +|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more| +|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more| +|InventoryLevel|StockLocation|Read-only - has many|Learn more| +|SalesChannel|StockLocation|Stored - many-to-many|Learn more| -# Event Module +*** -In this document, you'll learn what an Event Module is and how to use it in your Medusa application. +## Fulfillment Module -## What is an Event Module? +A fulfillment set can be conditioned to a specific stock location. -An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. -This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own. +![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) -Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. -### Default Event Module +![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) -By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). +### 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`: -## How to Use the Event Module? +To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. -You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +### query.graph -Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events. +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) -For example: +// stockLocations[0].fulfillment_sets +``` + +### useQueryGraphStep ```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, -} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const eventModuleService = container.resolve( - Modules.EVENT - ) +// ... - await eventModuleService.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -) +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) -export const workflow = createWorkflow( - "workflow-1", - () => { - step1() - } -) +// stockLocations[0].fulfillment_sets ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`. - -*** +### Manage with Link -## List of Event Modules +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). +### link.create -- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) +```ts +import { Modules } from "@medusajs/framework/utils" +// ... -# Redis Event Module +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` -The Redis Event Module uses Redis to implement Medusa's pub/sub events system. +### createRemoteLinkStep -It's powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -In production, it's recommended to use this module. +// ... -Our Cloud offering automatically provisions a Redis instance and configures the Redis Event Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` *** -## Register the Redis Event Module - -### Prerequisites +## Inventory Module -- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) +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. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +### Retrieve with Query -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +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 -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.EVENTS_REDIS_URL, - }, - }, +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", ], }) -``` - -### Environment Variables - -Make sure to add the following environment variables: -```bash -EVENTS_REDIS_URL= +// inventoryLevels[0].stock_locations ``` -### Redis Event Module Options +### useQueryGraphStep -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| -|\`redisOptions\`|An object of Redis options. Refer to the |No|-| -|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`| -|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-| -|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-| -|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-| +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Test the Module +// ... -To test the module, start the Medusa application: +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) -```bash npm2yarn -npm run dev +// inventoryLevels[0].stock_locations ``` -You'll see the following message in the terminal's logs: +*** -```bash noCopy noReport -Connection to Redis in module 'event-redis' established -``` +## 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. -# Local File Module Provider +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. -The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory. +![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) -- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead. -- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory. +### 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`: -## Register the Local File Module +### query.graph -The Local File Module Provider is registered by default in your application. +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) -Add the module into the `providers` array of the File Module: +// stockLocations[0].sales_channels +``` -The File Module accepts one provider only. +### useQueryGraphStep -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -module.exports = { - // ... - modules: [ - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-local", - id: "local", - options: { - // provider options... - }, - }, - ], - }, - }, +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", ], -} +}) + +// stockLocations[0].sales_channels ``` -### Local File Module Options +### Manage with Link -|Option|Description|Default| -|---|---|---|---|---| -|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`| -|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`| +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 -# File Module +```ts +import { Modules } from "@medusajs/framework/utils" -In this document, you'll learn about the File Module and its providers. +// ... -## What is the File Module? +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", + }, +}) +``` -The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well. + +# 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 the File Module? +## How to Use Stock Location Module's Service -You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +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. -In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files. +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 -import { Modules } from "@medusajs/framework/utils" +```ts title="src/workflows/create-stock-location.ts" highlights={highlights} import { + createWorkflow, + WorkflowResponse, createStep, - createWorkflow, StepResponse, - WorkflowResponse, } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -const step1 = createStep( - "step-1", +const createStockLocationStep = createStep( + "create-stock-location", async ({}, { container }) => { - const fileModuleService = container.resolve( - Modules.FILE - ) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - const { url } = await fileModuleService.retrieveFile("image.png") + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", + }) - return new StepResponse(url) - } + 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 workflow = createWorkflow( - "workflow-1", +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", () => { - const url = step1() + const { stockLocation } = createStockLocationStep() - return new WorkflowResponse(url) + return new WorkflowResponse({ stockLocation }) } ) ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow. +### 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" -### What is a File Module Provider? +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStockLocationWorkflow(req.scope) + .run() -A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations. + res.send(result) +} +``` -Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error. +### Subscriber -By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application. +```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" -This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md). +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStockLocationWorkflow(container) + .run() -- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) -- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) + console.log(result) +} +export const config: SubscriberConfig = { + event: "user.created", +} +``` -# S3 File Module Provider +### Scheduled Job -The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" -Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Refer to the [S3](https://docs.medusajs.com/cloud/s3/index.html.md) Cloud documentation for more details. +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStockLocationWorkflow(container) + .run() -## Prerequisites + console.log(result) +} -### AWS S3 +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin). -- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html). -- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). -- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled: - 1. On your bucket's dashboard, click on the Permissions tab. - 2. Click on the Edit button of the Block public access (bucket settings) section. - 3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button. - 4. Confirm saving the changes by entering `confirm` in the pop-up that shows. - 5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button. - 6. In the form that opens: - - Choose the "ACLs enabled" card. - - Click on the "Save changes" button. - 7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button. - 8. In the form that opens, enable the Read permission for "Everyone (public access)". - 9. Check the "I understand the effects of these changes on my objects and buckets." checkbox. - 10. Click on the "Save changes" button. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -### MinIO +*** -- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new). -- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/). -- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). -### DigitalOcean Spaces +# Links between Store Module and Other Modules -1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up). -2. Set up your R2 bucket: - - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information. - - Click "Create bucket" - - Enter a unique bucket name - - Select "Automatic" for location - - Choose "Standard" for storage class - - Confirm by clicking "Create bucket" -3. Configure public access: - - Make sure you have a [domain configured in your Cloudflare account](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/). - - On your bucket's dashboard, click on the Settings tab. - - In the General Section look for Custom Domains (recommended for production use) - - Click on the Add button to add your domain name. - - Enter the domain name you want to connect to and select Continue. - - Review the new record that will be added to the DNS table and select Connect Domain. -4. Retrieve credentials: - - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens): - - Click "Create User API token" - - Edit the "R2 Token" name - - Under Permissions, select Object Read & Write permission types - - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section. - - Once done, click the "Create User API Token" button. - - Copy the jurisdiction-specific endpoint for S3 clients to S3\_ENDPOINT into your environment variables. - - Copy the Access Key ID and Secret Access Key to the corresponding fields into your environment variables. - - Copy your custom domain to `S3_FILE_URL` with leading https:// into your environment variables. +This document showcases the module links defined between the Store Module and other Commerce Modules. -### Supabase S3 Storage +## Summary -### Cloudflare R2 +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| +|---|---|---|---| +|StoreCurrency|Currency|Read-only - has many|Learn more| *** -## Register the S3 File Module +## Currency Module -Add the module into the `providers` array of the File 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. -The File Module accepts one provider only. +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/StoreCurrency/index.html.md) data model in the Store Module (not in the Currency Module). -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +### 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`: -module.exports = { - // ... - modules: [ - // ... - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-s3", - id: "s3", - options: { - file_url: process.env.S3_FILE_URL, - access_key_id: process.env.S3_ACCESS_KEY_ID, - secret_access_key: process.env.S3_SECRET_ACCESS_KEY, - region: process.env.S3_REGION, - bucket: process.env.S3_BUCKET, - endpoint: process.env.S3_ENDPOINT, - // other options... - }, - }, - ], - }, - }, +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", ], -} +}) + +// stores[0].supported_currencies ``` -### Additional Configuration for MinIO and Supabase +### useQueryGraphStep -If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -For example: +// ... -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/file", - options: { - providers: [ - { - resolve: "@medusajs/medusa/file-s3", - id: "s3", - options: { - // ... - additional_client_config: { - forcePathStyle: true, - }, - }, - }, - ], - }, - }, +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", ], }) -``` - -### S3 File Module Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`file\_url\`|The base URL to upload files to.|-| -|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-| -|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-| -|\`region\`|The bucket's region code.|-| -|\`bucket\`|The bucket's name.|-| -|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-| -|\`prefix\`|A string to prefix each uploaded file's name.|-| -|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`| -|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`| -|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-| -*** +// stores[0].supported_currencies +``` -## Troubleshooting +# Store Module -# Locking Module +In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. -In this document, you'll learn about the Locking Module and its providers. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. -## What is the Locking Module? +Medusa has store related features available out-of-the-box through the Store 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 Store Module. -The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes that are trying to access the same resource at the same time, and ensures data consistency. +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -Medusa uses the Locking Module to control concurrency, avoid race conditions, and protect parts of code that should not be executed by more than one process at a time. This is especially essential in distributed or multi-threaded environments. +## Store Features -For example, Medusa uses the Locking Module in inventory management to ensure that only one transaction can update the stock levels at a time. By using the Locking Module in this scenario, Medusa prevents overselling an inventory item and keeps its quantity amounts accurate, even during high traffic periods or when receiving concurrent requests. +- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. +- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. *** -## How to Use the Locking Module? +## How to Use Store Module's Service -You can use the Locking Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +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. -In a step of your workflow, you can resolve the Locking Module's service and use its methods to execute an asynchronous job, acquire a lock, or release locks. +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 -import { Modules } from "@medusajs/framework/utils" +```ts title="src/workflows/create-store.ts" highlights={highlights} import { + createWorkflow, + WorkflowResponse, createStep, - createWorkflow, + StepResponse, } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -const step1 = createStep( - "step-1", +const createStoreStep = createStep( + "create-store", async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING - ) - const productModuleService = container.resolve( - Modules.PRODUCT - ) + const storeModuleService = container.resolve(Modules.STORE) - await lockingModuleService.execute("prod_123", async () => { - await productModuleService.deleteProduct("prod_123") + const store = await storeModuleService.createStores({ + name: "My Store", + supported_currencies: [{ + currency_code: "usd", + is_default: true, + }], }) - } + + return new StepResponse({ store }, store.id) + }, + async (storeId, { container }) => { + if(!storeId) { + return + } + const storeModuleService = container.resolve(Modules.STORE) + + await storeModuleService.deleteStores([storeId]) + } ) -export const workflow = createWorkflow( - "workflow-1", +export const createStoreWorkflow = createWorkflow( + "create-store", () => { - step1() + const { store } = createStoreStep() + + return new WorkflowResponse({ store }) } ) ``` -In the example above, you create a workflow that has a step. In the step, you resolve the services of the Locking and Product modules from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Then, you use the `execute` method of the Locking Module to acquire a lock for the product with the ID `prod_123` and execute an asynchronous function, which deletes the product. +### 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 { createStoreWorkflow } from "../../workflows/create-store" -## When to Use the Locking Module? +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStoreWorkflow(req.scope) + .run() -You should use the Locking Module when you need to ensure that only one process can access a shared resource at a time. As mentioned in the inventory example previously, you don't want customers to order quantities of inventory that are not available, or to update the stock levels of an item concurrently. + res.send(result) +} +``` -In those scenarios, you can use the Locking Module to acquire a lock for a resource and execute a critical section of code that should not be accessed by multiple processes simultaneously. +### 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 { createStoreWorkflow } from "../workflows/create-store" -## What is a Locking Module Provider? +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStoreWorkflow(container) + .run() -A Locking Module Provider implements the underlying logic of the Locking Module. It manages the locking mechanisms and ensures that only one process can access a shared resource at a time. + console.log(result) +} -Medusa provides [multiple Locking Module Providers](#list-of-locking-module-providers) that are suitable for development and production. You can also create a [custom Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md) to implement custom locking mechanisms or integrate with third-party services. +export const config: SubscriberConfig = { + event: "user.created", +} +``` -### Default Locking Module Provider +### Scheduled Job -By default, Medusa uses the In-Memory Locking Module Provider. This provider uses a plain JavaScript map to store the locks. While this is useful for development, it is not recommended for production environments as it is only intended for use in a single-instance environment. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStoreWorkflow } from "../workflows/create-store" -To add more providers, you can register them in the `medusa-config.ts` file. For example: +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStoreWorkflow(container) + .run() -```ts -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - // add providers here... - ], - }, - }, - ], -}) -``` + console.log(result) +} -When you register other providers in `medusa-config.ts`, Medusa will set the default provider based on the following scenarios: +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -|Scenario|Default Provider| -|---|---|---| -|One provider is registered.|The registered provider.| -|Multiple providers are registered and none of them has an |In-Memory Locking Module Provider.| -|Multiple providers and one of them has an |The provider with the | +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## List of Locking Module Providers -Medusa provides the following Locking Module Providers. You can use one of them, or [Create a Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md). +# Tax Module Options -- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md) -- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md) +In this guide, you'll learn about the options of the Tax Module. +## providers -# PostgreSQL Locking Module Provider - -The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database. - -The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks. - -While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible. +The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) 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. -## Register the PostgreSQL Locking Module Provider +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: +// ... -```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/locking", + resolve: "@medusajs/tax", options: { providers: [ { - resolve: "@medusajs/medusa/locking-postgres", - id: "locking-postgres", - // set this if you want this provider to be used by default - // and you have other Locking Module Providers registered. - is_default: true, + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, }, ], }, @@ -33540,507 +36624,413 @@ module.exports = defineConfig({ }) ``` -### Run Migrations +The objects in the array accept the following properties: -The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider: +- `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. -```bash -npx medusa db:migrate -``` -This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database. +# 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. -## Use Provider with Locking Module +Refer to the [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. -The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: +Medusa has tax related features available out-of-the-box through the Tax 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 Tax Module. -```ts title="medusa-config.ts" highlights={defaultHighlights} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-postgres", - id: "locking-postgres", - is_default: true, - }, - ], - }, - }, - ], -}) -``` +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods. +## Tax Features -For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): +- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. +- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. +- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. +- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region. -```ts +*** + +## How to Use Tax 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-tax-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" -const step1 = createStep( - "step-1", +const createTaxRegionStep = createStep( + "create-tax-region", async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING - ) + const taxModuleService = container.resolve(Modules.TAX) - await lockingModuleService.acquire("prod_123", { - provider: "lp_locking-postgres", + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", }) - } -) -``` + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) -# Redis Locking Module Provider + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) -The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments. +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() -This provider is recommended for production environments where Medusa is running in a multi-instance setup. + return new WorkflowResponse({ taxRegion }) + } +) +``` -Our Cloud offering automatically provisions a Redis instance and configures the Redis Locking Module Provider for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -*** +### API Route -## Register the Redis Locking Module Provider +```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 { createTaxRegionWorkflow } from "../../workflows/create-tax-region" -### Prerequisites +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(req.scope) + .run() -- [A redis server set up locally or a database in your deployed application.](https://redis.io/download) + res.send(result) +} +``` -To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: +### Subscriber -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-redis", - id: "locking-redis", - // set this if you want this provider to be used by default - // and you have other Locking Module Providers registered. - is_default: true, - options: { - redisUrl: process.env.LOCKING_REDIS_URL, - }, - }, - ], - }, - }, - ], -}) -``` +```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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" -### Environment Variables +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(container) + .run() -Make sure to add the following environment variable: + console.log(result) +} -```bash -LOCKING_REDIS_URL= +export const config: SubscriberConfig = { + event: "user.created", +} ``` -Where `` is the URL of your Redis server, either locally or in the deployed environment. +### Scheduled Job -The default Redis URL in a local environment is `redis://localhost:6379`. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" -### Redis Locking Module Provider Options +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(container) + .run() -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| -|\`redisOptions\`|An object of Redis options. Refer to the |No|-| -|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`| -|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`| -|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`| -|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`| + console.log(result) +} -*** +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -## Test out the Module +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -To test out the Redis Locking Module Provider, start the Medusa application: +*** -```bash npm2yarn -npm run dev -``` +## Configure Tax Module -You'll see the following message logged in the terminal: +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. -```bash -info: Connection to Redis in "locking-redis" provider established -``` +*** -This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server. -If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations. +# Tax Calculation with the Tax Provider -*** +In this guide, you’ll learn how tax lines are calculated using the tax provider. -## Use Provider with Locking Module +## Tax Lines Calculation -The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: +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. -```ts title="medusa-config.ts" highlights={defaultHighlights} -module.exports = defineConfig({ - // ... - modules: [ +For example: + +```ts +const taxLines = await taxModuleService.getTaxLines( + [ { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-redis", - id: "locking-redis", - is_default: true, - options: { - // ... - }, - }, - ], - }, + 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", + }, + } +) ``` -If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods. - -For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): +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. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" +The example above retrieves the tax lines based on the tax region for the United States. -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING - ) +The method returns tax lines for the line item and shipping methods. For example: - await lockingModuleService.acquire("prod_123", { - provider: "lp_locking-redis", - }) - } -) +```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" + } +] ``` +*** -# Local Notification Module Provider +## Using the Tax Provider in the Calculation -The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development. +The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md). -*** +A tax module implements the logic to shape tax lines. Each tax region uses a tax provider. -## Register the Local Notification Module +Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. -The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel. -Add the module into the `providers` array of the Notification Module: +# Tax Module Provider -Only one provider can be defined for a channel. +In this guide, you’ll learn about the Tax Module Provider and how it's used. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +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 provider of a tax region using the dashboard. -// ... +## What is a Tax Module Provider? -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - // ... - { - resolve: "@medusajs/medusa/notification-local", - id: "local", - options: { - channels: ["email"], - }, - }, - ], - }, - }, - ], -}) -``` +The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic. -### Local Notification Module Options +The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md). -|Option|Description| -|---|---|---| -|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification, -it's important to specify its channels to make sure it's used when a notification for that channel is created.| +![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg) +*** -# Notification Module +## Default Tax Provider -In this document, you'll learn about the Notification Module and its providers. +The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide. -## What is the Notification Module? +This provider is installed by default in your application and you can use it with tax regions. -The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well. +The identifier of the system tax provider is `tp_system`. *** -## How to Use the Notification Module? +## How to Create a Custom Tax Provider? -You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`. -In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications. +The module can have multiple tax provider services, where each are registered as separate tax providers. -For example: +Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider. -```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, -} from "@medusajs/framework/workflows-sdk" +After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md). -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) +*** - await notificationModuleService.createNotifications({ - to: "customer@gmail.com", - channel: "email", - template: "product-created", - data, - }) - } -) +## How are Tax Providers Registered? -export const workflow = createWorkflow( - "workflow-1", - () => { - step1() - } -) -``` +### Configure Tax Module's Providers -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application. -Then, you use the `createNotifications` method of the Notification Module to send an email notification. +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide. -Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). +### Registration on Application Start -*** +When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module. -## What is a Notification Module Provider? +For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register. -A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification. +### TaxProvider Data Model -By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal. +A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application. -Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). +This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application. -- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) -- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) -- [Mailchimp](https://docs.medusajs.com/integrations/guides/mailchimp/index.html.md) -- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md) -- [Slack](https://docs.medusajs.com/integrations/guides/slack/index.html.md) -- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md) +![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg) -*** +The `TaxProvider` data model has the following properties: -## Notification Module Provider Channels +- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Tax Module Provider's service. + - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the tax provider is enabled. -When you send a notification, you specify the channel to send it through, such as `email` or `sms`. +### How to Remove a Tax Provider? -You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel. +You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration. -For example: +Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option. -```ts title="medusa-config.ts" highlights={[["19"]]} -import { Modules } from "@medusajs/framework/utils" -// ... +# Tax Rates and Rules -module.exports = { - // ... - modules: [ - // ... - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - // ... - { - resolve: "@medusajs/medusa/notification-local", - id: "notification", - options: { - channels: ["email"], - }, - }, - ], - }, - }, - ], -} -``` +In this document, you’ll learn about tax rates and rules. -The `channels` option is an array of strings indicating the channels this provider is used for. +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? -# Send Notification with the Notification Module +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. -In this guide, you'll learn about the different ways to send notifications using the Notification Module. +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. -## Using the Create Method +### Combinable Tax Rates -In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method: +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`. -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" -import { INotificationModuleService } from "@medusajs/framework/types" +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. -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const notificationModuleService: INotificationModuleService = - container.resolve(Modules.NOTIFICATION) +*** - await notificationModuleService.createNotifications({ - to: "user@gmail.com", - channel: "email", - template: "product-created", - data, - }) -} +## Override Tax Rates with Rules -export const config: SubscriberConfig = { - event: "product.created", -} -``` +You can create tax rates that override the default for specific conditions or rules. -The `create` method accepts an object or an array of objects having the following properties: +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. -- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number. -- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification. -- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here. -- data: (\`Record\\`) The data to pass along to the template, if necessary. +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. -For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md). +![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. *** -## Using the sendNotificationsStep +## Tax Rules in a Tax Region -If you want to send a notification as part of a workflow, You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow. +Tax rules define the tax rates and behavior within a tax region. They specify: -For example: +- The tax rate percentage. +- Which products the tax applies to. +- Other custom rules to determine tax applicability. -```ts title="src/workflows/send-email.ts" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { - sendNotificationsStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide. -type WorkflowInput = { - id: string -} +*** -export const sendEmailWorkflow = createWorkflow( - "send-email-workflow", - ({ id }: WorkflowInput) => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "*", - "variants.*", - ], - filters: { - id, - }, - }) +## Tax Provider - sendNotificationsStep({ - to: "user@gmail.com", - channel: "email", - template: "product-created", - data: { - product_title: product[0].title, - product_image: product[0].images[0]?.url, - }, - }) - } -) -``` +Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region. -For a full list of input properties accepted, refer to the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) reference. +You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic. -You can then execute this workflow in a subscriber, API route, or scheduled job. +Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. -For example, you can execute it when a product is created: -```ts title="src/subscribers/product-created.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { sendEmailWorkflow } from "../workflows/send-email" +# Send Invite User Email Notification -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await sendEmailWorkflow(container).run({ - input: { - id: data.id, - }, - }) -} +In this guide, you'll learn how to handle the `invite.created` and `invite.resent` events to send an invite email (or other notification type) to the user. -export const config: SubscriberConfig = { - event: "product.created", -} -``` +Refer to the [Manage Invites](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) user guide to learn how to manage invites using the Medusa Admin. +## User Invite Flow Overview -# SendGrid Notification Module Provider +![Diagram showcasing the user invite flow detailed below](https://res.cloudinary.com/dza7lstvk/image/upload/v1754047213/Medusa%20Resources/invite-user_uqspsv.jpg) -The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers. +Admin users can add new users to their store by sending them an invite. The flow for inviting users is as follows: -## Register the SendGrid Notification Module +1. An admin user invites a user either through the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) or using the [Create Invite API route](https://docs.medusajs.com/api/admin#invites_postinvites). +2. The invite is created and the `invite.created` event is emitted. + - At this point, you can handle the event to send an email or notification to the user. +3. The invited user receives the invite and can accept it, which creates a new user. + - The invited user can accept the invite either through the Medusa Admin or using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept). -### Prerequisites +The admin user can also resend the invite if the invited user doesn't receive the invite or doesn't accept it before expiry, which emits the `invite.resent` event. -- [SendGrid account](https://signup.sendgrid.com) -- [Setup SendGrid single sender](https://docs.sendgrid.com/ui/sending-email/sender-verification) -- [SendGrid API Key](https://docs.sendgrid.com/ui/account-and-settings/api-keys) +In this guide, you'll implement a subscriber that handles the `invite.created` and `invite.resent` events to send an email to the user. -Add the module into the `providers` array of the Notification Module: +After adding the subscriber, you will have a complete user invite flow that you can utilize either through the Medusa Admin or using the Admin APIs. -Only one provider can be defined for a channel. +*** -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +## Prerequisites: Notification Module Provider -// ... +To send an email or notification to the user, you must have a Notification Module Provider set up. +Medusa provides providers like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) and [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), and you can also [create your own custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). + +Refer to the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) documentation for a list of available providers and how to set them up. + +### Testing with the Local Notification Module Provider + +For testing purposes, you can use the [Local Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) by adding this to your `medusa-config.ts`: + +```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ @@ -34050,12 +37040,10 @@ module.exports = defineConfig({ providers: [ // ... { - resolve: "@medusajs/medusa/notification-sendgrid", - id: "sendgrid", + resolve: "@medusajs/medusa/notification-local", + id: "local", options: { channels: ["email"], - api_key: process.env.SENDGRID_API_KEY, - from: process.env.SENDGRID_FROM, }, }, ], @@ -34065,5997 +37053,13675 @@ module.exports = defineConfig({ }) ``` -### Environment Variables - -Make sure to add the following environment variables: - -```bash -SENDGRID_API_KEY= -SENDGRID_FROM= -``` - -### SendGrid Notification Module Options - -|Option|Description| -|---|---|---| -||The channels this notification module is used to send notifications for. -Only one provider can be defined for a channel.| -| -| -| -| - -## SendGrid Templates - -When you send a notification, you must specify the ID of the template to use in SendGrid. - -Refer to [this SendGrid documentation guide](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates) on how to create templates for your different email types. +The Local provider logs email details to your terminal instead of sending actual emails, which is useful for development and testing. *** -## Test out the Module - -To test the module out, you'll listen to the `product.created` event and send an email when a product is created. +## Create the Invite User Subscriber -Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) at `src/subscribers/product-created.ts` with the following content: +To create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that handles the `invite.created` and `invite.resent` events, create the file `src/subscribers/user-invited.ts` with the following content: -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" +```ts title="src/subscribers/user-invited.ts" highlights={highlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -export default async function productCreateHandler({ +export default async function inviteCreatedHandler({ event: { data }, container, -}: SubscriberArgs<{ id: string }>) { - const notificationModuleService = container.resolve(Modules.NOTIFICATION) +}: SubscriberArgs<{ + id: string +}>) { const query = container.resolve("query") + const notificationModuleService = container.resolve( + "notification" + ) + const config = container.resolve("configModule") - const { data: [product] } = await query.graph({ - entity: "product", - fields: ["*"], + const { data: [invite] } = await query.graph({ + entity: "invite", + fields: [ + "email", + "token", + ], filters: { id: data.id, }, }) + const backend_url = config.admin.backendUrl !== "/" ? config.admin.backendUrl : + "http://localhost:9000" + const adminPath = config.admin.path + await notificationModuleService.createNotifications({ - to: "test@gmail.com", + to: invite.email, + // TODO replace with template ID in notification provider + template: "user-invited", channel: "email", - template: "product-created", data: { - product_title: product.title, - product_image: product.images[0]?.url, + invite_url: `${backend_url}${adminPath}/invite?token=${invite.token}`, }, }) } export const config: SubscriberConfig = { - event: "product.created", + event: [ + "invite.created", + "invite.resent", + ], } ``` -In this subscriber, you: - -- Resolve the Notification Module's main service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -- Retrieve the product's details using Query to pass them to the template in SendGrid. -- Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the specified email. By specifying the `email` channel, the SendGrid Notification Module Provider is used to send the notification. -- The `template` property of the `createNotifications` method's parameter specifies the ID of the template defined in SendGrid. -- The `data` property allows you to pass data to the template in SendGrid. For example, the product's title and image. +The subscriber receives the ID of the invite in the event payload. You resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to fetch the invite details, including the email and token. -Then, start the Medusa application: +### Invite URL -```bash npm2yarn -npm run dev -``` +You then use the Notification Module's service to send an email to the user with the invite link. The invite link is constructed using: -And create a product either using the [API route](https://docs.medusajs.com/api/admin#products_postproducts) or the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md). This runs the subscriber and sends an email using SendGrid. +1. The backend URL of the Medusa application, since the Medusa Admin is hosted on the same URL. +2. The admin path, which is the path to the Medusa Admin (for example, `/app`). -### Other Events to Handle +Note that the Medusa Admin has an invite form at `/invite?token=`, which accepts the invite token as a query parameter. -Medusa emits other events that you can handle to send notifications using the SendGrid Notification Module Provider, such as `order.placed` when an order is placed. +### Notification Configurations -Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a complete list of events emitted by Medusa. +For the notification, you can configure the following fields: -### Sending Emails with SendGrid in Workflows +- `template`: The template ID of the email to send. This ID depends on the Notification Module provider you use. For example, if you use SendGrid, this would be the ID of the SendGrid template. + - Refer to the [Example Notification Templates](#example-notification-templates) section below for examples of notification templates to use. +- `channel`: The channel to send the notification through. In this case, it's set to `email`. +- `data`: The data to pass to the notification template. You can add additional fields as needed, such as the invited user's email. -You can also send an email using SendGrid in any [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). This allows you to send emails within your custom flows. +### Test It Out -You can use the [sendNotifcationStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid. +After you set up the Notification Module Provider, create a template in the provider, and create the subscriber, you can test the full invite flow. -For example: +Start the Medusa application with the following command: -```ts title="src/workflows/send-email.ts" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { - sendNotificationsStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +```bash npm2yarn +npm run dev +``` -type WorkflowInput = { - id: string -} +Then, open the Medusa Admin (locally at `http://localhost:9000/app`) and go to Settings → Users. Create an invite as explained in the [Manage Invites](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) user guide. -export const sendEmailWorkflow = createWorkflow( - "send-email-workflow", - ({ id }: WorkflowInput) => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: [ - "*", - "variants.*", - ], - filters: { - id, - }, - }) +Once you create the invite, you should see that the `invite.created` event is emitted in the server's logs: - sendNotificationsStep({ - to: "test@gmail.com", - channel: "email", - template: "product-created", - data: { - product_title: product[0].title, - product_image: product[0].images[0]?.url, - }, - }) - } -) +```bash +info: Processing invite.created which has 1 subscribers ``` -This workflow works similarly to the subscriber. It retrieves the product's details using Query and sends an email using SendGrid (by specifying the `email` channel) to the `test@gmail.com` email. - -You can also execute this workflow in a subscriber. For example, you can execute it when a product is created: +If you're using an email Notification Module Provider, check the recipient's email inbox for the invite email with the link to accept the invite. -```ts title="src/subscribers/product-created.ts" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" -import { sendEmailWorkflow } from "../workflows/send-email" +If you're using the Local provider, check your terminal for the logged email details. -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await sendEmailWorkflow(container).run({ - input: { - id: data.id, - }, - }) -} +*** -export const config: SubscriberConfig = { - event: "product.created", -} -``` +## Example Notification Templates -This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid. +The following section provides example notification templates for some Notification Module Providers. +### SendGrid -# Infrastructure Modules +Refer to the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) documentation for more details on how to set up SendGrid. -Medusa's architectural functionalities, such as emitting and subscribing to events or caching data, are all implemented in Infrastructure Modules. An Infrastructure Module is a package that can be installed and used in any Medusa application. These modules allow you to choose and integrate custom services for architectural purposes. +The following HTML template can be used with SendGrid to send an invite email: -For example, you can use our [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md) to handle event functionalities, or create a custom module that implements these functionalities with Memcached. Learn more in [the Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). +```html + + + + + + You're Invited! + + + +
+
+

You're Invited!

+
-This section of the documentation showcases Medusa's Infrastructure Modules, how they work, and how to use them in your Medusa application. +
+

+ Hello{{#if email}} {{email}}{{/if}}, +

+

+ You've been invited to join our platform. Click the button below to accept your invitation and set up your account. +

+
-## Analytics Module + -The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). +
+

+ Or copy and paste this URL into your browser: +

+ + {{invite_url}} + +
-The Analytics Module exposes functionalities to track and analyze user interactions and system events. For example, tracking cart updates or completed orders. Learn more in the [Analytics Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md). + +
+ + +``` -{/* The Analytics Module has module providers that implement the underlying logic of integrating third-party services for tracking analytics. The following Analytics Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Analytics Module Provider guide](/references/analytics/provider). */} +Make sure to pass the `invite_url` variable to the template, which contains the URL to accept the invite. -- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md) -- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md) +You can also customize the template further to show other information, such as the user's email. -## Cache Module +### Resend -A Cache Module is used to cache the results of computations such as price selection or various tax calculations. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md). +If you've integrated Resend as explained in the [Resend Integration Guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), you can add a new template for user invites at `src/modules/resend/emails/user-invited.tsx`: -The following Cache modules are provided by Medusa. You can also create your own cache module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). +```tsx title="src/modules/resend/emails/user-invited.tsx" +import { + Text, + Container, + Heading, + Html, + Section, + Tailwind, + Head, + Preview, + Body, + Link, + Button, +} from "@react-email/components" -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) +type UserInvitedEmailProps = { + invite_url: string + email?: string +} -*** +function UserInvitedEmailComponent({ invite_url, email }: UserInvitedEmailProps) { + return ( + + + You've been invited to join our platform + + + +
+ + You're Invited! + +
-## Event Module +
+ + Hello{email ? ` ${email}` : ""}, + + + You've been invited to join our platform. Click the button below to accept your invitation and set up your account. + +
-An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). +
+ +
-The following Event modules are provided by Medusa. You can also create your own event module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). +
+ + Or copy and paste this URL into your browser: + + + {invite_url} + +
-- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) +
+ + If you weren't expecting this invitation, you can ignore this email. + +
+
+ +
+ + ) +} -*** +export const userInvitedEmail = (props: UserInvitedEmailProps) => ( + +) -## File Module +// Mock data for preview/development +const mockInvite: UserInvitedEmailProps = { + invite_url: "https://your-app.com/app/invite/sample-token-123", + email: "user@example.com", +} -The File Module handles file upload and storage of assets, such as product images. Refer to the [File Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) to learn more about it. +export default () => +``` -The File Module has module providers that implement the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The following File Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create File Module Provider guide](https://docs.medusajs.com/references/file-provider-module/index.html.md). +Feel free to customize the email template further to match your branding and style, or to add additional information such as the user's email. -- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) -- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) +Then, in the Resend Module's service at `src/modules/resend/service.ts`, add the new template to the `templates` object and `Templates` type: -*** +```ts title="src/modules/resend/service.ts" +// other imports... +import { userInvitedEmail } from "./emails/user-invited" -## Locking Module +enum Templates { + // ... + USER_INVITED = "user-invited", +} -The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes and ensures data consistency. Refer to the [Locking Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to learn more about it. +const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { + // ... + [Templates.USER_INVITED]: userInvitedEmail, +} +``` -The Locking Module uses module providers that implement the underlying logic of the locking mechanism. The following Locking Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Locking Module Provider guide](https://docs.medusajs.com/references/locking-module-provider/index.html.md). +Finally, find the `getTemplateSubject` function in the `ResendNotificationProviderService` and add a case for the `USER_INVITED` template: -- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md) -- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md) +```ts title="src/modules/resend/service.ts" +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... -*** + private getTemplateSubject(template: Templates) { + // ... + switch (template) { + // ... + case Templates.USER_INVITED: + return "You've been invited to join our platform" + } + } +} +``` -## Notification Module -The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notifcation Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it. +# User Module Options -The Notification Module has module providers that implement the underlying logic of sending notifications, typically through integrating a third-party service. The following modules are provided by Medusa. You can also create a custom provider as explained in the [Create Notification Module Provider guide](https://docs.medusajs.com/references/notification-provider-module/index.html.md). +In this document, you'll learn about the options of the User Module. -- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) -- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) +## Module Options -### Notification Module Provider Guides +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -- [Send Notification](https://docs.medusajs.com/infrastructure-modules/notification/send-notification/index.html.md) -- [Create Notification Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) -- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md) +// ... -*** +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/user", + options: { + jwt_secret: process.env.JWT_SECRET, + }, + }, + ], +}) +``` -## Workflow Engine Module +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| -A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Worklow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md). +### Environment Variables -The following Workflow Engine modules are provided by Medusa. +Make sure to add the necessary environment variables for the above options in `.env`: -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) +```bash +JWT_SECRET=supersecret +``` -# How to Use the Workflow Engine Module +# User Module -In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them. +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. -*** +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. -## Resolve Workflow Engine Module's Service +Medusa has user related features available out-of-the-box through the User 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 User Module. -In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```ts -import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" +## User Features -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const workflowEngineModuleService = container.resolve( - Modules.WORKFLOW_ENGINE - ) - - // TODO use workflowEngineModuleService - } -) -``` +- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. +- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. -This will resolve the service of the configured Workflow Engine Module, which is the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. +*** -You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods. +## How to Use User 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. -## setStepSuccess +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. -This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step. +For example: -### Example +```ts title="src/workflows/create-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -```ts -// other imports... -import { - TransactionHandlerType, -} from "@medusajs/framework/utils" +const createUserStep = createStep( + "create-user", + async ({}, { container }) => { + const userModuleService = container.resolve(Modules.USER) -await workflowEngineModuleService.setStepSuccess({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Done!"), - options: { - container, + const user = await userModuleService.createUsers({ + email: "user@example.com", + first_name: "John", + last_name: "Smith", + }) + + return new StepResponse({ user }, user.id) }, -}) -``` + async (userId, { container }) => { + if (!userId) { + return + } + const userModuleService = container.resolve(Modules.USER) -### Parameters + await userModuleService.deleteUsers([userId]) + } +) -- idempotencyKey: (\`object\`) The details of the step to set as successful. +export const createUserWorkflow = createWorkflow( + "create-user", + () => { + const { user } = createUserStep() - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + return new WorkflowResponse({ + user, + }) + } +) +``` - - transactionId: (\`string\`) The ID of the workflow execution's transaction. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. +### API Route - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. -- options: (\`object\`) Options to pass to the step. +```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 { createUserWorkflow } from "../../workflows/create-user" - - container: (\`Container\`) An instance of the Medusa container. +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createUserWorkflow(req.scope) + .run() -*** + res.send(result) +} +``` -## setStepFailure +### Subscriber -This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed. +```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 { createUserWorkflow } from "../workflows/create-user" -### Example +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createUserWorkflow(container) + .run() -```ts -// other imports... -import { - TransactionHandlerType, -} from "@medusajs/framework/utils" + console.log(result) +} -await workflowEngineModuleService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, -}) +export const config: SubscriberConfig = { + event: "user.created", +} ``` -### Parameters +### Scheduled Job -- idempotencyKey: (\`object\`) The details of the step to set as failed. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createUserWorkflow } from "../workflows/create-user" - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createUserWorkflow(container) + .run() - - transactionId: (\`string\`) The ID of the workflow execution's transaction. + console.log(result) +} - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. -- options: (\`object\`) Options to pass to the step. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - - container: (\`Container\`) An instance of the Medusa container. +*** + +## Configure User Module + +The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. *** -## subscribe -This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing. +# User Creation Flows -Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more. +In this document, learn the different ways to create a user using the User Module. -### Example +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. -```ts -const { transaction } = await helloWorldWorkflow(container).run() +## Straightforward User Creation -const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", -} +To create a user, use the [createUsers method of the User Module’s main service](https://docs.medusajs.com/references/user/createUsers/index.html.md): -await workflowEngineModuleService.subscribe({ - ...subscriptionOptions, - subscriber: async (data) => { - if (data.eventType === "onFinish") { - console.log("Finished execution", data.result) - // unsubscribe - await workflowEngineModuleService.unsubscribe({ - ...subscriptionOptions, - subscriberOrId: subscriptionOptions.subscriberId, - }) - } else if (data.eventType === "onStepFailure") { - console.log("Workflow failed", data.step) - } - }, +```ts +const user = await userModuleService.createUsers({ + email: "user@example.com", }) ``` -### Parameters +You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). -- subscriptionOptions: (\`object\`) The options for the subscription. +*** - - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +## Invite Users - - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. +To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: - - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events. +```ts +const invite = await userModuleService.createInvites({ + email: "user@example.com", +}) +``` - - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event. +Later, you can accept the invite and create a new user for them: -*** +```ts +const invite = + await userModuleService.validateInviteToken("secret_123") -## unsubscribe +await userModuleService.updateInvites({ + id: invite.id, + accepted: true, +}) -This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result. +const user = await userModuleService.createUsers({ + email: invite.email, +}) +``` -### Example +### Invite Expiry + +An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): ```ts -await workflowEngineModuleService.unsubscribe({ - workflowId: "hello-world", - transactionId: "transaction-id", - subscriberOrId: "hello-world-subscriber", -}) +await userModuleService.refreshInviteTokens(["invite_123"]) ``` -### Parameters +*** -- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. -- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events. +## Create Identity with the Auth Module +By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. -# In-Memory Workflow Engine Module +So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: -The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions. +```ts +const { success, authIdentity } = + await authModuleService.authenticate("emailpass", { + // ... + }) -This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. +const [, count] = await userModuleService.listAndCountUsers({ + email: authIdentity.entity_id, +}) -For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md). +if (!count) { + const user = await userModuleService.createUsers({ + email: authIdentity.entity_id, + }) +} +``` -*** -## Register the In-Memory Workflow Engine Module +# Local Analytics Module Provider -The In-Memory Workflow Engine Module is registered by default in your application. +The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +*** -// ... +## Register the Local Analytics Module + +Add the module into the `provider` object of the Analytics Module: + +You can use only one Analytics Module Provider in your Medusa application. +```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/workflow-engine-inmemory", + resolve: "@medusajs/medusa/analytics", + options: { + providers: [ + { + resolve: "@medusajs/analytics-local", + id: "local", + }, + ], + }, }, ], }) ``` +*** -# Workflow Engine Module - -In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. +## Test out the Module -## What is a Workflow Engine Module? +To test the module out, you'll track in the console when an order is placed. -A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service. +You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. -### Default Workflow Engine Module +For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: -Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead. +```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { OrderDTO } from "@medusajs/framework/types" -*** +type StepInput = { + order: OrderDTO +} -## How to Use the Workflow Engine Module? +const trackOrderCreatedStep = createStep( + "track-order-created-step", + async ({ order }: StepInput, { container }) => { + const analyticsModuleService = container.resolve(Modules.ANALYTICS) -You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + await analyticsModuleService.track({ + event: "order_created", + userId: order.customer_id, + properties: { + order_id: order.id, + total: order.total, + items: order.items.map((item) => ({ + variant_id: item.variant_id, + product_id: item.product_id, + quantity: item.quantity, + })), + customer_id: order.customer_id, + }, + }) + } +) -In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps. +type WorkflowInput = { + order_id: string +} -For example: +export const trackOrderCreatedWorkflow = createWorkflow( + "track-order-created-workflow", + ({ order_id }: WorkflowInput) => { + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "*", + "customer.*", + "items.*", + ], + filters: { + id: order_id, + }, + }) + trackOrderCreatedStep({ + order: orders[0], + }) + } +) +``` -```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) +In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event. - const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ - transaction_id: transaction_id, - }) +Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: - return new StepResponse(workflowExecution) - } -) +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" -export const workflow = createWorkflow( - "workflow-1", - () => { - const workflowExecution = step1() +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await trackOrderCreatedWorkflow(container).run({ + input: { + order_id: data.id, + }, + }) +} - return new WorkflowResponse(workflowExecution) - } -) +export const config: SubscriberConfig = { + event: "order.placed", +} ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. -Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow. +You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the console for the tracked event. *** -## List of Workflow Engine Modules +## Additional Resources -Medusa provides the following Workflow Engine Modules. +- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) +# Analytics Module -# Redis Workflow Engine Module +In this document, you'll learn about the Analytics Module and its providers. -The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module. +The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). -Our Cloud offering automatically provisions a Redis instance and configures the Redis Workflow Engine Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. +## What is the Analytics Module? + +The Analytics Module exposes functionalities to track and analyze user interactions and system events with third-party services. For example, you can track cart updates or completed orders. + +In your Medusa application, you can use the Analytics Module to send data to third-party analytics services like PostHog or Segment, enabling you to gain insights into user behavior and system performance. + +![Analytics Module workflow diagram showing the event tracking flow: when a customer places an order in the storefront, the Medusa application uses the Analytics Module to capture the event data and forwards it to configured third-party analytics services for business intelligence and user behavior analysis](https://res.cloudinary.com/dza7lstvk/image/upload/v1747832107/Medusa%20Resources/analytics-module-overview_egz7xg.jpg) *** -## Register the Redis Workflow Engine Module +## How to Use the Analytics Module? -### Prerequisites +### Configure Analytics Module Provider -- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) +To use the Analytics Module, you need to configure it along with an Analytics Module Provider. -Add the module into the `modules` property of the exported object in `medusa-config.ts`: +An Analytics Module Provider implements the underlying logic of sending analytics data. It integrates with a third-party analytics service to send the data collected through the Analytics Module. -```ts title="medusa-config.ts" highlights={highlights} -import { Modules } from "@medusajs/framework/utils" +Medusa provides two Analytics Module Providers: Local and PostHog module providers. -// ... +You can also [create a custom Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) that integrates with a third-party service, like Segment. + +- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md) +- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md) + +[Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md): undefined + +To configure the Analytics Module and its provider, add it to the list of modules in your `medusa-config.ts` file. For example: +```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/workflow-engine-redis", + resolve: "@medusajs/medusa/analytics", options: { - redis: { - url: process.env.WE_REDIS_URL, - }, + providers: [ + { + resolve: "@medusajs/medusa/analytics-local", + id: "local", + }, + ], }, }, ], }) ``` -### Environment Variables +Refer to the documentation of each provider for specific configuration options. -Make sure to add the following environment variables: +### Track Events -```bash -WE_REDIS_URL= -``` +To track an event, you can use the Analytics Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. -### Redis Workflow Engine Module Options +In a step of your workflow, you can resolve the Analytics Module's service and use its methods to track events or identify users. -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |-| -|\`options\`|An object of Redis options. Refer to the |No|-| -|\`queueName\`|The name of the queue used to keep track of retries and timeouts.|No|\`medusa-workflows\`| -|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |-| +For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: -## Test the Module +```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { OrderDTO } from "@medusajs/framework/types" -To test the module, start the Medusa application: +type StepInput = { + order: OrderDTO +} -```bash npm2yarn -npm run dev -``` +const trackOrderCreatedStep = createStep( + "track-order-created-step", + async ({ order }: StepInput, { container }) => { + const analyticsModuleService = container.resolve(Modules.ANALYTICS) -You'll see the following message in the terminal's logs: + await analyticsModuleService.track({ + event: "order_created", + userId: order.customer_id, + properties: { + order_id: order.id, + total: order.total, + items: order.items.map((item) => ({ + variant_id: item.variant_id, + product_id: item.product_id, + quantity: item.quantity, + })), + customer_id: order.customer_id, + }, + }) + } +) -```bash noCopy noReport -Connection to Redis in module 'workflow-engine-redis' established +type WorkflowInput = { + order_id: string +} + +export const trackOrderCreatedWorkflow = createWorkflow( + "track-order-created-workflow", + ({ order_id }: WorkflowInput) => { + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "*", + "customer.*", + "items.*", + ], + filters: { + id: order_id, + }, + }) + trackOrderCreatedStep({ + order: orders[0], + }) + } +) ``` +This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. -## Workflows +In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured in `medusa-config.ts` to track the event. -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/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) +### Execute Analytics Workflow + +After that, you can execute this workflow in a subscriber that runs when a product is created. + +create a subscriber at `src/subscribers/order-placed.ts` with the following content: + +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await trackOrderCreatedWorkflow(container).run({ + input: { + order_id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. + +You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the provider you integrated with (for example, PostHog) for the tracked event. + + +# PostHog Analytics Module Provider + +The PostHog Analytics Module Provider allows you to integrate [PostHog](https://posthog.com/) with Medusa. + +PostHog is an open-source product analytics platform that helps you track user interactions and analyze user behavior in your commerce application. + +By integrating PostHog with Medusa, you can track events such as cart additions, order completions, and user sign-ups, enabling you to gain insights into user behavior and optimize your application accordingly. + +The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). + +*** + +## Register the PostHog Analytics Module + +### Prerequisites + +- [PostHog account](https://app.posthog.com/signup) +- [PostHog API Key](https://posthog.com/docs/getting-started/api-key) + +Add the module into the `provider` object of the Analytics Module: + +You can use only one provider in your Medusa application. + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/analytics", + options: { + providers: [ + { + resolve: "@medusajs/analytics-posthog", + id: "posthog", + options: { + posthogEventsKey: process.env.POSTHOG_EVENTS_API_KEY, + posthogHost: process.env.POSTHOG_HOST, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +POSTHOG_EVENTS_API_KEY= +POSTHOG_HOST= +``` + +### PostHog Analytics Module Options + +|Option|Description|Default| +|---|---|---| +|\`eventsKey\`|The PostHog API key for tracking events. This is required to authenticate your requests to the PostHog API.|-| +|\`posthogHost\`|The PostHog API host URL.|\`https://eu.i.posthog.com\`| + +*** + +## Test out the Module + +To test the module out, you'll track in PostHog when an order is placed. + +You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event. + +For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content: + +```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { OrderDTO } from "@medusajs/framework/types" + +type StepInput = { + order: OrderDTO +} + +const trackOrderCreatedStep = createStep( + "track-order-created-step", + async ({ order }: StepInput, { container }) => { + const analyticsModuleService = container.resolve(Modules.ANALYTICS) + + await analyticsModuleService.track({ + event: "order_created", + userId: order.customer_id, + properties: { + order_id: order.id, + total: order.total, + items: order.items.map((item) => ({ + variant_id: item.variant_id, + product_id: item.product_id, + quantity: item.quantity, + })), + customer_id: order.customer_id, + }, + }) + } +) + +type WorkflowInput = { + order_id: string +} + +export const trackOrderCreatedWorkflow = createWorkflow( + "track-order-created-workflow", + ({ order_id }: WorkflowInput) => { + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "*", + "customer.*", + "items.*", + ], + filters: { + id: order_id, + }, + }) + trackOrderCreatedStep({ + order: orders[0], + }) + } +) +``` + +This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`. + +In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the PostHog Analytics Module Provider, in this case) to track the event. + +Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content: + +```ts title="src/subscribers/order-placed.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { trackOrderCreatedWorkflow } from "../workflows/track-order-created" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await trackOrderCreatedWorkflow(container).run({ + input: { + order_id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input. + +You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking your PostHog dashboard for the tracked event. + +*** + +## Additional Resources + +- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md) + + +# How to Create a Cache Module + +In this guide, you’ll learn how to create a Cache Module. + +## 1. Create Module Directory + +Start by creating a new directory for your module. For example, `src/modules/my-cache`. + +*** + +## 2. Create the Cache Service + +Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service. + +The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`: + +```ts title="src/modules/my-cache/service.ts" +import { ICacheService } from "@medusajs/framework/types" + +class MyCacheService implements ICacheService { + get(key: string): Promise { + throw new Error("Method not implemented.") + } + set(key: string, data: unknown, ttl?: number): Promise { + throw new Error("Method not implemented.") + } + invalidate(key: string): Promise { + throw new Error("Method not implemented.") + } +} + +export default MyCacheService +``` + +The service implements the required methods based on the desired caching mechanism. + +### Implement get Method + +The `get` method retrieves the value of a cached item based on its key. + +The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist. + +For example, to implement this method using Memcached: + +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + // ... + async get(cacheKey: string): Promise { + return new Promise((res, rej) => { + this.memcached.get(cacheKey, (err, data) => { + if (err) { + res(null) + } else { + if (data) { + res(JSON.parse(data)) + } else { + res(null) + } + } + }) + }) + } +} +``` + +### Implement set Method + +The `set` method is used to set an item in the cache. It accepts three parameters: + +1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item. +2. The second parameter is the data to be added to the cache. The data can be of any type. +3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache. + +For example, to implement this method using Memcached: + +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + protected TTL = 60 + // ... + async set( + key: string, + data: Record, + ttl: number = this.TTL // or any value + ): Promise { + return new Promise((res, rej) => + this.memcached.set( + key, JSON.stringify(data), ttl, (err) => { + if (err) { + rej(err) + } else { + res() + } + }) + ) + } +} +``` + +### Implement invalidate Method + +The `invalidate` method removes an item from the cache using its key. + +By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand. + +The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache. + +For example, to implement this method using Memcached: + +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + // ... + async invalidate(key: string): Promise { + return new Promise((res, rej) => { + this.memcached.del(key, (err) => { + if (err) { + rej(err) + } else { + res() + } + }) + }) + } +} +``` + +*** + +## 3. Create Module Definition File + +Create the file `src/modules/my-cache/index.ts` with the following content: + +```ts title="src/modules/my-cache/index.ts" +import MyCacheService from "./service" +import { Module } from "@medusajs/framework/utils" + +export default Module("my-cache", { + service: MyCacheService, +}) +``` + +This exports the module's definition, indicating that the `MyCacheService` is the main service of the module. + +*** + +## 4. Use Module + +To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/my-cache", + options: { + // any options + ttl: 30, + }, + }, + ], +}) +``` + + +# In-Memory Cache Module + +The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application. + +This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md). + +*** + +## Register the In-Memory Cache Module + +The In-Memory Cache Module is registered by default in your application. + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-inmemory", + options: { + // optional options + }, + }, + ], +}) +``` + +### In-Memory Cache Module Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`| + + +# Cache Module + +In this document, you'll learn what a Cache Module is and how to use it in your Medusa application. + +## What is a Cache Module? + +A Cache Module is used to cache the results of computations such as price selection or various tax calculations. + +The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture. + +### Default Cache Module + +By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). + +*** + +## How to Use the Cache Module? + +You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const cacheModuleService = container.resolve( + Modules.CACHE + ) + + await cacheModuleService.set("key", "value") + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`. + +*** + +## List of Cache Modules + +Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) + + +# Redis Cache Module + +The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module. + +Our Cloud offering automatically provisions a Redis instance and configures the Redis Cache Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. + +*** + +## Register the Redis Cache Module + +### Prerequisites + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" highlights={highlights} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.CACHE_REDIS_URL, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +CACHE_REDIS_URL= +``` + +### Redis Cache Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| +|\`redisOptions\`|An object of Redis options. Refer to the |No|-| +|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`| +|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`| + +*** + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'cache-redis' established +``` + + +# How to Create an Event Module + +In this guide, you’ll learn how to create an Event Module. + +## 1. Create Module Directory + +Start by creating a new directory for your module. For example, `src/modules/my-event`. + +*** + +## 2. Create the Event Service + +Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service. + +The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework: + +```ts title="src/modules/my-event/service.ts" +import { AbstractEventBusModuleService } from "@medusajs/framework/utils" +import { Message } from "@medusajs/types" + +class MyEventService extends AbstractEventBusModuleService { + async emit(data: Message | Message[], options: Record): Promise { + throw new Error("Method not implemented.") + } + async releaseGroupedEvents(eventGroupId: string): Promise { + throw new Error("Method not implemented.") + } + async clearGroupedEvents(eventGroupId: string): Promise { + throw new Error("Method not implemented.") + } +} + +export default MyEventService +``` + +The service implements the required methods based on the desired publish/subscribe logic. + +### eventToSubscribersMap\_ Property + +The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions. + +In your custom implementation, you can use this property to manage the subscribed handler functions: + +```ts +const eventSubscribers = + this.eventToSubscribersMap_.get(eventName) || [] +``` + +### emit Method + +The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks. + +An example implementation: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + async emit(data: Message | Message[], options: Record): Promise { + const events = Array.isArray(data) ? data : [data] + + for (const event of events) { + console.log(`Received the event ${event.name} with data ${event.data}`) + + // TODO push the event somewhere + } + } + // ... +} +``` + +The `emit` method receives the following parameters: + +- data: (\`object or array of objects\`) The emitted event(s). + + - name: (\`string\`) The name of the emitted event. + + - data: (\`object\`) The data payload of the event. + + - metadata: (\`object\`) Additional details of the emitted event. + + - eventGroupId: (string) A group ID that the event belongs to. + + - options: (\`object\`) Additional options relevant for the event service. + +### releaseGroupedEvents Method + +Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events. + +If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group. + +For example: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + protected groupedEventsMap_: Map + + constructor() { + // @ts-ignore + super(...arguments) + + this.groupedEventsMap_ = new Map() + } + + async releaseGroupedEvents(eventGroupId: string): Promise { + const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || [] + + for (const event of groupedEvents) { + const { options, ...eventBody } = event + + // TODO emit event + } + + await this.clearGroupedEvents(eventGroupId) + } + + // ... +} +``` + +The `releaseGroupedEvents` receives the group ID as a parameter. + +In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next. + +To add events to the grouped events map, you can do it in the `emit` method: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + // ... + async emit(data: Message | Message[], options: Record): Promise { + const events = Array.isArray(data) ? data : [data] + + for (const event of events) { + console.log(`Received the event ${event.name} with data ${event.data}`) + + if (event.metadata.eventGroupId) { + const groupedEvents = this.groupedEventsMap_.get( + event.metadata.eventGroupId + ) || [] + + groupedEvents.push(event) + + this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents) + continue + } + + // TODO push the event somewhere + } + } +} +``` + +### clearGroupedEvents Method + +If your Event Module supports grouped events, this method is used to remove the events of a group. + +For example: + +```ts title="src/modules/my-event/service.ts" +class MyEventService extends AbstractEventBusModuleService { + // from previous section + protected groupedEventsMap_: Map + + async clearGroupedEvents(eventGroupId: string): Promise { + this.groupedEventsMap_.delete(eventGroupId) + } + + // ... +} +``` + +The method accepts the group's name as a parameter. + +In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well. + +*** + +## 3. Create Module Definition File + +Create the file `src/modules/my-event/index.ts` with the following content: + +```ts title="src/modules/my-event/index.ts" +import MyEventService from "./service" +import { Module } from "@medusajs/framework/utils" + +export default Module("my-event", { + service: MyEventService, +}) +``` + +This exports the module's definition, indicating that the `MyEventService` is the main service of the module. + +*** + +## 4. Use Module + +To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/my-event", + options: { + // any options + }, + }, + ], +}) +``` + + +# Local Event Module + +The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment. + +This module is useful for development and testing, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md). + +*** + +## Register the Local Event Module + +The Local Event Module is registered by default in your application. + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/event-bus-local", + }, + ], +}) +``` + +*** + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Local Event Bus installed. This is not recommended for production. +``` + + +# Event Module + +In this document, you'll learn what an Event Module is and how to use it in your Medusa application. + +## What is an Event Module? + +An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. + +This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own. + +Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +### Default Event Module + +By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). + +*** + +## How to Use the Event Module? + +You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const eventModuleService = container.resolve( + Modules.EVENT + ) + + await eventModuleService.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`. + +*** + +## List of Event Modules + +Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) + + +# Redis Event Module + +The Redis Event Module uses Redis to implement Medusa's pub/sub events system. + +It's powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage. + +In production, it's recommended to use this module. + +Our Cloud offering automatically provisions a Redis instance and configures the Redis Event Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. + +*** + +## Register the Redis Event Module + +### Prerequisites + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.EVENTS_REDIS_URL, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +EVENTS_REDIS_URL= +``` + +### Redis Event Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| +|\`redisOptions\`|An object of Redis options. Refer to the |No|-| +|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`| +|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-| +|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-| +|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-| + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'event-redis' established +``` + + +# Local File Module Provider + +The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory. + +- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead. +- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory. + +*** + +## Register the Local File Module + +The Local File Module Provider is registered by default in your application. + +Add the module into the `providers` array of the File Module: + +The File Module accepts one provider only. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + { + resolve: "@medusajs/medusa/file", + options: { + providers: [ + { + resolve: "@medusajs/medusa/file-local", + id: "local", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +} +``` + +### Local File Module Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`| +|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`| + + +# File Module + +In this document, you'll learn about the File Module and its providers. + +## What is the File Module? + +The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well. + +*** + +## How to Use the File Module? + +You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const fileModuleService = container.resolve( + Modules.FILE + ) + + const { url } = await fileModuleService.retrieveFile("image.png") + + return new StepResponse(url) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + const url = step1() + + return new WorkflowResponse(url) + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow. + +*** + +### What is a File Module Provider? + +A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations. + +Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error. + +By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application. + +This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) +- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) + + +# S3 File Module Provider + +The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application. + +Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Refer to the [S3](https://docs.medusajs.com/cloud/s3/index.html.md) Cloud documentation for more details. + +## Prerequisites + +### AWS S3 + +- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin). +- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html). +- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey). +- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled: + 1. On your bucket's dashboard, click on the Permissions tab. + 2. Click on the Edit button of the Block public access (bucket settings) section. + 3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button. + 4. Confirm saving the changes by entering `confirm` in the pop-up that shows. + 5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button. + 6. In the form that opens: + - Choose the "ACLs enabled" card. + - Click on the "Save changes" button. + 7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button. + 8. In the form that opens, enable the Read permission for "Everyone (public access)". + 9. Check the "I understand the effects of these changes on my objects and buckets." checkbox. + 10. Click on the "Save changes" button. + +### MinIO + +- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new). +- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/). +- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). + +### DigitalOcean Spaces + +1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up). +2. Set up your R2 bucket: + - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information. + - Click "Create bucket" + - Enter a unique bucket name + - Select "Automatic" for location + - Choose "Standard" for storage class + - Confirm by clicking "Create bucket" +3. Configure public access: + - Make sure you have a [domain configured in your Cloudflare account](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/). + - On your bucket's dashboard, click on the Settings tab. + - In the General Section look for Custom Domains (recommended for production use) + - Click on the Add button to add your domain name. + - Enter the domain name you want to connect to and select Continue. + - Review the new record that will be added to the DNS table and select Connect Domain. +4. Retrieve credentials: + - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens): + - Click "Create User API token" + - Edit the "R2 Token" name + - Under Permissions, select Object Read & Write permission types + - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section. + - Once done, click the "Create User API Token" button. + - Copy the jurisdiction-specific endpoint for S3 clients to S3\_ENDPOINT into your environment variables. + - Copy the Access Key ID and Secret Access Key to the corresponding fields into your environment variables. + - Copy your custom domain to `S3_FILE_URL` with leading https:// into your environment variables. + +### Supabase S3 Storage + +### Cloudflare R2 + +*** + +## Register the S3 File Module + +Add the module into the `providers` array of the File Module: + +The File Module accepts one provider only. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/file", + options: { + providers: [ + { + resolve: "@medusajs/medusa/file-s3", + id: "s3", + options: { + file_url: process.env.S3_FILE_URL, + access_key_id: process.env.S3_ACCESS_KEY_ID, + secret_access_key: process.env.S3_SECRET_ACCESS_KEY, + region: process.env.S3_REGION, + bucket: process.env.S3_BUCKET, + endpoint: process.env.S3_ENDPOINT, + // other options... + }, + }, + ], + }, + }, + ], +} +``` + +### Additional Configuration for MinIO and Supabase + +If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object. + +For example: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/file", + options: { + providers: [ + { + resolve: "@medusajs/medusa/file-s3", + id: "s3", + options: { + // ... + additional_client_config: { + forcePathStyle: true, + }, + }, + }, + ], + }, + }, + ], +}) +``` + +### S3 File Module Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`file\_url\`|The base URL to upload files to.|-| +|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-| +|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-| +|\`region\`|The bucket's region code.|-| +|\`bucket\`|The bucket's name.|-| +|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-| +|\`prefix\`|A string to prefix each uploaded file's name.|-| +|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`| +|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`| +|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-| + +*** + +## Troubleshooting + + +# Locking Module + +In this document, you'll learn about the Locking Module and its providers. + +## What is the Locking Module? + +The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes that are trying to access the same resource at the same time, and ensures data consistency. + +Medusa uses the Locking Module to control concurrency, avoid race conditions, and protect parts of code that should not be executed by more than one process at a time. This is especially essential in distributed or multi-threaded environments. + +For example, Medusa uses the Locking Module in inventory management to ensure that only one transaction can update the stock levels at a time. By using the Locking Module in this scenario, Medusa prevents overselling an inventory item and keeps its quantity amounts accurate, even during high traffic periods or when receiving concurrent requests. + +*** + +## How to Use the Locking Module? + +You can use the Locking Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Locking Module's service and use its methods to execute an asynchronous job, acquire a lock, or release locks. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const lockingModuleService = container.resolve( + Modules.LOCKING + ) + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await lockingModuleService.execute("prod_123", async () => { + await productModuleService.deleteProduct("prod_123") + }) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the services of the Locking and Product modules from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `execute` method of the Locking Module to acquire a lock for the product with the ID `prod_123` and execute an asynchronous function, which deletes the product. + +*** + +## When to Use the Locking Module? + +You should use the Locking Module when you need to ensure that only one process can access a shared resource at a time. As mentioned in the inventory example previously, you don't want customers to order quantities of inventory that are not available, or to update the stock levels of an item concurrently. + +In those scenarios, you can use the Locking Module to acquire a lock for a resource and execute a critical section of code that should not be accessed by multiple processes simultaneously. + +*** + +## What is a Locking Module Provider? + +A Locking Module Provider implements the underlying logic of the Locking Module. It manages the locking mechanisms and ensures that only one process can access a shared resource at a time. + +Medusa provides [multiple Locking Module Providers](#list-of-locking-module-providers) that are suitable for development and production. You can also create a [custom Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md) to implement custom locking mechanisms or integrate with third-party services. + +### Default Locking Module Provider + +By default, Medusa uses the In-Memory Locking Module Provider. This provider uses a plain JavaScript map to store the locks. While this is useful for development, it is not recommended for production environments as it is only intended for use in a single-instance environment. + +To add more providers, you can register them in the `medusa-config.ts` file. For example: + +```ts +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + // add providers here... + ], + }, + }, + ], +}) +``` + +When you register other providers in `medusa-config.ts`, Medusa will set the default provider based on the following scenarios: + +|Scenario|Default Provider| +|---|---|---| +|One provider is registered.|The registered provider.| +|Multiple providers are registered and none of them has an |In-Memory Locking Module Provider.| +|Multiple providers and one of them has an |The provider with the | + +*** + +## List of Locking Module Providers + +Medusa provides the following Locking Module Providers. You can use one of them, or [Create a Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md). + +- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md) +- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md) + + +# PostgreSQL Locking Module Provider + +The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database. + +The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks. + +While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible. + +*** + +## Register the PostgreSQL Locking Module Provider + +To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-postgres", + id: "locking-postgres", + // set this if you want this provider to be used by default + // and you have other Locking Module Providers registered. + is_default: true, + }, + ], + }, + }, + ], +}) +``` + +### Run Migrations + +The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider: + +```bash +npx medusa db:migrate +``` + +This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database. + +*** + +## Use Provider with Locking Module + +The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: + +```ts title="medusa-config.ts" highlights={defaultHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-postgres", + id: "locking-postgres", + is_default: true, + }, + ], + }, + }, + ], +}) +``` + +If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods. + +For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const lockingModuleService = container.resolve( + Modules.LOCKING + ) + + await lockingModuleService.acquire("prod_123", { + provider: "lp_locking-postgres", + }) + } +) +``` + + +# Redis Locking Module Provider + +The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments. + +This provider is recommended for production environments where Medusa is running in a multi-instance setup. + +Our Cloud offering automatically provisions a Redis instance and configures the Redis Locking Module Provider for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. + +*** + +## Register the Redis Locking Module Provider + +### Prerequisites + +- [A redis server set up locally or a database in your deployed application.](https://redis.io/download) + +To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-redis", + id: "locking-redis", + // set this if you want this provider to be used by default + // and you have other Locking Module Providers registered. + is_default: true, + options: { + redisUrl: process.env.LOCKING_REDIS_URL, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variable: + +```bash +LOCKING_REDIS_URL= +``` + +Where `` is the URL of your Redis server, either locally or in the deployed environment. + +The default Redis URL in a local environment is `redis://localhost:6379`. + +### Redis Locking Module Provider Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| +|\`redisOptions\`|An object of Redis options. Refer to the |No|-| +|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`| +|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`| +|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`| +|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`| + +*** + +## Test out the Module + +To test out the Redis Locking Module Provider, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message logged in the terminal: + +```bash +info: Connection to Redis in "locking-redis" provider established +``` + +This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server. + +If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations. + +*** + +## Use Provider with Locking Module + +The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: + +```ts title="medusa-config.ts" highlights={defaultHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-redis", + id: "locking-redis", + is_default: true, + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods. + +For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const lockingModuleService = container.resolve( + Modules.LOCKING + ) + + await lockingModuleService.acquire("prod_123", { + provider: "lp_locking-redis", + }) + } +) +``` + + +# Local Notification Module Provider + +The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development. + +*** + +## Register the Local Notification Module + +The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel. + +Add the module into the `providers` array of the Notification Module: + +Only one provider can be defined for a channel. + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-local", + id: "local", + options: { + channels: ["email"], + }, + }, + ], + }, + }, + ], +}) +``` + +### Local Notification Module Options + +|Option|Description| +|---|---|---| +|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification, +it's important to specify its channels to make sure it's used when a notification for that channel is created.| + + +# Notification Module + +In this document, you'll learn about the Notification Module and its providers. + +## What is the Notification Module? + +The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well. + +*** + +## How to Use the Notification Module? + +You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + await notificationModuleService.createNotifications({ + to: "customer@gmail.com", + channel: "email", + template: "product-created", + data, + }) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `createNotifications` method of the Notification Module to send an email notification. + +Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). + +*** + +## What is a Notification Module Provider? + +A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification. + +By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal. + +Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) +- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) +- [Mailchimp](https://docs.medusajs.com/integrations/guides/mailchimp/index.html.md) +- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md) +- [Slack](https://docs.medusajs.com/integrations/guides/slack/index.html.md) +- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md) + +*** + +## Notification Module Provider Channels + +When you send a notification, you specify the channel to send it through, such as `email` or `sms`. + +You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel. + +For example: + +```ts title="medusa-config.ts" highlights={[["19"]]} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = { + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-local", + id: "notification", + options: { + channels: ["email"], + }, + }, + ], + }, + }, + ], +} +``` + +The `channels` option is an array of strings indicating the channels this provider is used for. + + +# Send Notification with the Notification Module + +In this guide, you'll learn about the different ways to send notifications using the Notification Module. + +## Using the Create Method + +In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { INotificationModuleService } from "@medusajs/framework/types" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const notificationModuleService: INotificationModuleService = + container.resolve(Modules.NOTIFICATION) + + await notificationModuleService.createNotifications({ + to: "user@gmail.com", + channel: "email", + template: "product-created", + data, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `create` method accepts an object or an array of objects having the following properties: + +- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number. +- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification. +- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here. +- data: (\`Record\\`) The data to pass along to the template, if necessary. + +For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md). + +*** + +## Using the sendNotificationsStep + +If you want to send a notification as part of a workflow, You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow. + +For example: + +```ts title="src/workflows/send-email.ts" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { + sendNotificationsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + id: string +} + +export const sendEmailWorkflow = createWorkflow( + "send-email-workflow", + ({ id }: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "*", + "variants.*", + ], + filters: { + id, + }, + }) + + sendNotificationsStep({ + to: "user@gmail.com", + channel: "email", + template: "product-created", + data: { + product_title: product[0].title, + product_image: product[0].images[0]?.url, + }, + }) + } +) +``` + +For a full list of input properties accepted, refer to the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) reference. + +You can then execute this workflow in a subscriber, API route, or scheduled job. + +For example, you can execute it when a product is created: + +```ts title="src/subscribers/product-created.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { sendEmailWorkflow } from "../workflows/send-email" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendEmailWorkflow(container).run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + + +# SendGrid Notification Module Provider + +The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers. + +## Register the SendGrid Notification Module + +### Prerequisites + +- [SendGrid account](https://signup.sendgrid.com) +- [Setup SendGrid single sender](https://docs.sendgrid.com/ui/sending-email/sender-verification) +- [SendGrid API Key](https://docs.sendgrid.com/ui/account-and-settings/api-keys) + +Add the module into the `providers` array of the Notification Module: + +Only one provider can be defined for a channel. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-sendgrid", + id: "sendgrid", + options: { + channels: ["email"], + api_key: process.env.SENDGRID_API_KEY, + from: process.env.SENDGRID_FROM, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +SENDGRID_API_KEY= +SENDGRID_FROM= +``` + +### SendGrid Notification Module Options + +|Option|Description| +|---|---|---| +||The channels this notification module is used to send notifications for. +Only one provider can be defined for a channel.| +| +| +| +| + +## SendGrid Templates + +When you send a notification, you must specify the ID of the template to use in SendGrid. + +Refer to [this SendGrid documentation guide](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates) on how to create templates for your different email types. + +*** + +## Test out the Module + +To test the module out, you'll listen to the `product.created` event and send an email when a product is created. + +Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) at `src/subscribers/product-created.ts` with the following content: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const notificationModuleService = container.resolve(Modules.NOTIFICATION) + const query = container.resolve("query") + + const { data: [product] } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: data.id, + }, + }) + + await notificationModuleService.createNotifications({ + to: "test@gmail.com", + channel: "email", + template: "product-created", + data: { + product_title: product.title, + product_image: product.images[0]?.url, + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +In this subscriber, you: + +- Resolve the Notification Module's main service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +- Retrieve the product's details using Query to pass them to the template in SendGrid. +- Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the specified email. By specifying the `email` channel, the SendGrid Notification Module Provider is used to send the notification. +- The `template` property of the `createNotifications` method's parameter specifies the ID of the template defined in SendGrid. +- The `data` property allows you to pass data to the template in SendGrid. For example, the product's title and image. + +Then, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +And create a product either using the [API route](https://docs.medusajs.com/api/admin#products_postproducts) or the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md). This runs the subscriber and sends an email using SendGrid. + +### Other Events to Handle + +Medusa emits other events that you can handle to send notifications using the SendGrid Notification Module Provider, such as `order.placed` when an order is placed. + +Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a complete list of events emitted by Medusa. + +### Sending Emails with SendGrid in Workflows + +You can also send an email using SendGrid in any [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). This allows you to send emails within your custom flows. + +You can use the [sendNotifcationStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid. + +For example: + +```ts title="src/workflows/send-email.ts" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { + sendNotificationsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + id: string +} + +export const sendEmailWorkflow = createWorkflow( + "send-email-workflow", + ({ id }: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "*", + "variants.*", + ], + filters: { + id, + }, + }) + + sendNotificationsStep({ + to: "test@gmail.com", + channel: "email", + template: "product-created", + data: { + product_title: product[0].title, + product_image: product[0].images[0]?.url, + }, + }) + } +) +``` + +This workflow works similarly to the subscriber. It retrieves the product's details using Query and sends an email using SendGrid (by specifying the `email` channel) to the `test@gmail.com` email. + +You can also execute this workflow in a subscriber. For example, you can execute it when a product is created: + +```ts title="src/subscribers/product-created.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { sendEmailWorkflow } from "../workflows/send-email" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendEmailWorkflow(container).run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid. + + +# Infrastructure Modules + +Medusa's architectural functionalities, such as emitting and subscribing to events or caching data, are all implemented in Infrastructure Modules. An Infrastructure Module is a package that can be installed and used in any Medusa application. These modules allow you to choose and integrate custom services for architectural purposes. + +For example, you can use our [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md) to handle event functionalities, or create a custom module that implements these functionalities with Memcached. Learn more in [the Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +This section of the documentation showcases Medusa's Infrastructure Modules, how they work, and how to use them in your Medusa application. + +## Analytics Module + +The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3). + +The Analytics Module exposes functionalities to track and analyze user interactions and system events. For example, tracking cart updates or completed orders. Learn more in the [Analytics Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md). + +{/* The Analytics Module has module providers that implement the underlying logic of integrating third-party services for tracking analytics. The following Analytics Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Analytics Module Provider guide](/references/analytics/provider). */} + +- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md) +- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md) + +## Cache Module + +A Cache Module is used to cache the results of computations such as price selection or various tax calculations. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md). + +The following Cache modules are provided by Medusa. You can also create your own cache module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) + +*** + +## Event Module + +An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). + +The following Event modules are provided by Medusa. You can also create your own event module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) + +*** + +## File Module + +The File Module handles file upload and storage of assets, such as product images. Refer to the [File Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) to learn more about it. + +The File Module has module providers that implement the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The following File Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create File Module Provider guide](https://docs.medusajs.com/references/file-provider-module/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md) +- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) + +*** + +## Locking Module + +The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes and ensures data consistency. Refer to the [Locking Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to learn more about it. + +The Locking Module uses module providers that implement the underlying logic of the locking mechanism. The following Locking Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Locking Module Provider guide](https://docs.medusajs.com/references/locking-module-provider/index.html.md). + +- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md) +- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md) + +*** + +## Notification Module + +The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notifcation Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it. + +The Notification Module has module providers that implement the underlying logic of sending notifications, typically through integrating a third-party service. The following modules are provided by Medusa. You can also create a custom provider as explained in the [Create Notification Module Provider guide](https://docs.medusajs.com/references/notification-provider-module/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) +- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) + +### Notification Module Provider Guides + +- [Send Notification](https://docs.medusajs.com/infrastructure-modules/notification/send-notification/index.html.md) +- [Create Notification Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) +- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md) + +*** + +## Workflow Engine Module + +A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Worklow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md). + +The following Workflow Engine modules are provided by Medusa. + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) + + +# How to Use the Workflow Engine Module + +In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them. + +*** + +## Resolve Workflow Engine Module's Service + +In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const workflowEngineModuleService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + // TODO use workflowEngineModuleService + } +) +``` + +This will resolve the service of the configured Workflow Engine Module, which is the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. + +You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods. + +*** + +## setStepSuccess + +This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step. + +### Example + +```ts +// other imports... +import { + TransactionHandlerType, +} from "@medusajs/framework/utils" + +await workflowEngineModuleService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, +}) +``` + +### Parameters + +- idempotencyKey: (\`object\`) The details of the step to set as successful. + + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. + + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. + + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. +- options: (\`object\`) Options to pass to the step. + + - container: (\`Container\`) An instance of the Medusa container. + +*** + +## setStepFailure + +This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed. + +### Example + +```ts +// other imports... +import { + TransactionHandlerType, +} from "@medusajs/framework/utils" + +await workflowEngineModuleService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, +}) +``` + +### Parameters + +- idempotencyKey: (\`object\`) The details of the step to set as failed. + + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. + + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. + + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status. +- options: (\`object\`) Options to pass to the step. + + - container: (\`Container\`) An instance of the Medusa container. + +*** + +## subscribe + +This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing. + +Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more. + +### Example + +```ts +const { transaction } = await helloWorldWorkflow(container).run() + +const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", +} + +await workflowEngineModuleService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + // unsubscribe + await workflowEngineModuleService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, +}) +``` + +### Parameters + +- subscriptionOptions: (\`object\`) The options for the subscription. + + - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow. + + - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. + + - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events. + + - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event. + +*** + +## unsubscribe + +This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result. + +### Example + +```ts +await workflowEngineModuleService.unsubscribe({ + workflowId: "hello-world", + transactionId: "transaction-id", + subscriberOrId: "hello-world-subscriber", +}) +``` + +### Parameters + +- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow. +- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events. + + +# In-Memory Workflow Engine Module + +The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions. + +This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md). + +*** + +## Register the In-Memory Workflow Engine Module + +The In-Memory Workflow Engine Module is registered by default in your application. + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/workflow-engine-inmemory", + }, + ], +}) +``` + + +# Workflow Engine Module + +In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. + +## What is a Workflow Engine Module? + +A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service. + +### Default Workflow Engine Module + +Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead. + +*** + +## How to Use the Workflow Engine Module? + +You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ + transaction_id: transaction_id, + }) + + return new StepResponse(workflowExecution) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + const workflowExecution = step1() + + return new WorkflowResponse(workflowExecution) + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow. + +*** + +## List of Workflow Engine Modules + +Medusa provides the following Workflow Engine Modules. + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) + + +# Redis Workflow Engine Module + +The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module. + +Our Cloud offering automatically provisions a Redis instance and configures the Redis Workflow Engine Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation. + +*** + +## Register the Redis Workflow Engine Module + +### Prerequisites + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" highlights={highlights} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.WE_REDIS_URL, + }, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +WE_REDIS_URL= +``` + +### Redis Workflow Engine Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |-| +|\`options\`|An object of Redis options. Refer to the |No|-| +|\`queueName\`|The name of the queue used to keep track of retries and timeouts.|No|\`medusa-workflows\`| +|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |-| + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'workflow-engine-redis' established +``` + + +## Workflows + +- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) +- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/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) - [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) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) -- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/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) +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) +- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/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) +- [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) +- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/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) +- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/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) +- [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) +- [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) +- [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) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/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) +- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) +- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) +- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) +- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) +- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) +- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) +- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) +- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) +- [deleteDraftOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteDraftOrdersWorkflow/index.html.md) +- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) +- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md) +- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) +- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md) +- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) +- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) +- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) +- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) +- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) +- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) +- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/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) +- [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) +- [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) +- [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) +- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/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) +- [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) +- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/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) +- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) +- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) +- [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) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/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) +- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) +- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/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) +- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/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) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/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) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/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) +- [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) +- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/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) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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) +- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/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) +- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/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) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/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) +- [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) +- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) +- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/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) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) +- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) +- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) +- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) +- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) +- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) +- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/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) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/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) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) +- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/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) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) +- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) +- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/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) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/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) +- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) +- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) +- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) +- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) +- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/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) +- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) +- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/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) +- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) +- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/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) +- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/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) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) +- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) +- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/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) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/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) +- [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) +- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/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) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) +- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/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) +- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/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) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) +- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/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) +- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/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) +- [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) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/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) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/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) +- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/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) +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/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) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/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) +- [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) +- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/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) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/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) +- [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) +- [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) +- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) +- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/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) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/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) +- [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) +- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/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) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/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) +- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) +- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/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) +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) + + +## Steps + +- [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) +- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/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) +- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/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) +- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) +- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) +- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) +- [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) +- [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) +- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) +- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) +- [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) +- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) +- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) +- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/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) +- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md) +- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) +- [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/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) +- [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) +- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/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) +- [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) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) +- [deleteDraftOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteDraftOrdersStep/index.html.md) +- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md) +- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) +- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) +- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) +- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/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) +- [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) +- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) +- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) +- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) +- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) +- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/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) +- [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) +- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) +- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/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) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/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) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/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) +- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/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) +- [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) +- [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) +- [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) +- [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) +- [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) +- [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) +- [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) +- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/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) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/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) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/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) +- [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) +- [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) +- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/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) +- [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) +- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/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) +- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/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) +- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/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) +- [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) +- [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) +- [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) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) +- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) +- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/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) +- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md) +- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) +- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md) +- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/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) +- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) +- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/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) +- [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) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) +- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/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) +- [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) +- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) +- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) +- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) +- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/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) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/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) +- [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) +- [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) +- [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) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/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) +- [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) +- [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) +- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/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) +- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/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) +- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) + + +# Events Reference + +This documentation page includes the list of all events emitted by [Medusa's workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md). + +## Auth Events + +### Summary + +|Event|Description| +|---|---| +|auth.password\_reset|Emitted when a reset password token is generated. You can listen to this event +to send a reset password email to the user or customer, for example.| + +### auth.password\_reset + +Emitted when a reset password token is generated. You can listen to this event +to send a reset password email to the user or customer, for example. + +#### Payload + +```ts +{ + entity_id, // The identifier of the user or customer. For example, an email address. + actor_type, // The type of actor. For example, "customer", "user", or custom. + token, // The generated token. +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) + +*** + +## Cart Events + +### Summary + +|Event|Description| +|---|---| +|cart.created|Emitted when a cart is created.| +|cart.updated|Emitted when a cart's details are updated.| +|cart.region\_updated|Emitted when the cart's region is updated. This +event is emitted alongside the | +|cart.customer\_transferred|Emitted when the customer in the cart is transferred.| + +### cart.created + +Emitted when a cart is created. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/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) -- [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) -- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) -- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/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) + +*** + +### cart.updated + +Emitted when a cart's details are updated. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/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) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) + +*** + +### cart.region\_updated + +Emitted when the cart's region is updated. This +event is emitted alongside the `cart.updated` event. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) + +*** + +### cart.customer\_transferred + +Emitted when the customer in the cart is transferred. + +#### Payload + +```ts +{ + id, // The ID of the cart + customer_id, // The ID of the customer +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) + +*** + +## Customer Events + +### Summary + +|Event|Description| +|---|---| +|customer.created|Emitted when a customer is created.| +|customer.updated|Emitted when a customer is updated.| +|customer.deleted|Emitted when a customer is deleted.| + +### customer.created + +Emitted when a customer is created. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) + +*** + +### customer.updated + +Emitted when a customer is updated. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) + +*** + +### customer.deleted + +Emitted when a customer is deleted. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/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) -- [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) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/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) -- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) -- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) -- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) -- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) -- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) -- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) -- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) -- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [deleteDraftOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteDraftOrdersWorkflow/index.html.md) -- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) -- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md) -- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) -- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md) -- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) -- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) -- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) -- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) -- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) -- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) -- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/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) -- [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) -- [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) -- [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) -- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/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) -- [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) -- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/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) -- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) -- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) -- [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) + +*** + +## Fulfillment Events + +### Summary + +|Event|Description| +|---|---| +|shipment.created|Emitted when a shipment is created for an order.| +|delivery.created|Emitted when a fulfillment is marked as delivered.| + +### shipment.created + +Emitted when a shipment is created for an order. + +#### Payload + +```ts +{ + id, // the ID of the shipment + no_notification, // (boolean) whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) + +*** + +### delivery.created + +Emitted when a fulfillment is marked as delivered. + +#### Payload + +```ts +{ + id, // the ID of the fulfillment +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) + +*** + +## Invite Events + +### Summary + +|Event|Description| +|---|---| +|invite.accepted|Emitted when an invite is accepted.| +|invite.created|Emitted when invites are created. You can listen to this event +to send an email to the invited users, for example.| +|invite.deleted|Emitted when invites are deleted.| +|invite.resent|Emitted when invites should be resent because their token was +refreshed. You can listen to this event to send an email to the invited users, +for example.| + +### invite.accepted + +Emitted when an invite is accepted. + +#### Payload + +```ts +{ + id, // The ID of the invite +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) + +*** + +### invite.created + +Emitted when invites are created. You can listen to this event +to send an email to the invited users, for example. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) + +*** + +### invite.deleted + +Emitted when invites are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) + +*** + +### invite.resent + +Emitted when invites should be resent because their token was +refreshed. You can listen to this event to send an email to the invited users, +for example. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/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) -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/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) -- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/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) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/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) + +*** + +## Order Edit Events + +### Summary + +|Event|Description| +|---|---| +|order-edit.requested|Emitted when an order edit is requested.| +|order-edit.confirmed|Emitted when an order edit request is confirmed.| +|order-edit.canceled|Emitted when an order edit request is canceled.| + +### order-edit.requested + +Emitted when an order edit is requested. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) + +*** + +### order-edit.confirmed + +Emitted when an order edit request is confirmed. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) + +*** + +### order-edit.canceled + +Emitted when an order edit request is canceled. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) -- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) -- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/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) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) + +*** + +## Order Events + +### Summary + +|Event|Description| +|---|---| +|order.updated|Emitted when the details of an order or draft order is updated. This +doesn't include updates made by an edit.| +|order.placed|Emitted when an order is placed, or when a draft order is converted to an +order.| +|order.canceled|Emitted when an order is canceld.| +|order.completed|Emitted when orders are completed.| +|order.archived|Emitted when an order is archived.| +|order.fulfillment\_created|Emitted when a fulfillment is created for an order.| +|order.fulfillment\_canceled|Emitted when an order's fulfillment is canceled.| +|order.return\_requested|Emitted when a return request is confirmed.| +|order.return\_received|Emitted when a return is marked as received.| +|order.claim\_created|Emitted when a claim is created for an order.| +|order.exchange\_created|Emitted when an exchange is created for an order.| +|order.transfer\_requested|Emitted when an order is requested to be transferred to +another customer.| + +### order.updated + +Emitted when the details of an order or draft order is updated. This +doesn't include updates made by an edit. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) +- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) + +*** + +### order.placed + +Emitted when an order is placed, or when a draft order is converted to an +order. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + +*** + +### order.canceled + +Emitted when an order is canceld. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) -- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/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) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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) -- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) + +*** + +### order.completed + +Emitted when orders are completed. + +#### Payload + +```ts +[{ + id, // The ID of the order +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/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) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/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) + +*** + +### order.archived + +Emitted when an order is archived. + +#### Payload + +```ts +[{ + id, // The ID of the order +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) + +*** + +### order.fulfillment\_created + +Emitted when a fulfillment is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + fulfillment_id, // The ID of the fulfillment + no_notification, // (boolean) Whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +*** + +### order.fulfillment\_canceled + +Emitted when an order's fulfillment is canceled. + +#### Payload + +```ts +{ + order_id, // The ID of the order + fulfillment_id, // The ID of the fulfillment + no_notification, // (boolean) Whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) + +*** + +### order.return\_requested + +Emitted when a return request is confirmed. + +#### Payload + +```ts +{ + order_id, // The ID of the order + return_id, // The ID of the return +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) + +*** + +### order.return\_received + +Emitted when a return is marked as received. + +#### Payload + +```ts +{ + order_id, // The ID of the order + return_id, // The ID of the return +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/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) -- [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) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/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) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/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) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) -- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) -- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/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) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/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) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/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) -- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/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) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) -- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/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) -- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) -- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/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) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) -- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/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) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +*** + +### order.claim\_created + +Emitted when a claim is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + claim_id, // The ID of the claim +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) + +*** + +### order.exchange\_created + +Emitted when an exchange is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + exchange_id, // The ID of the exchange +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) + +*** + +### order.transfer\_requested + +Emitted when an order is requested to be transferred to +another customer. + +#### Payload + +```ts +{ + id, // The ID of the order + order_change_id, // The ID of the order change created for the transfer +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) -- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) -- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/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) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) -- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/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) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/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) -- [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) -- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/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) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) -- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) + +*** + +## Payment Events + +### Summary + +|Event|Description| +|---|---| +|payment.captured|Emitted when a payment is captured.| +|payment.refunded|Emitted when a payment is refunded.| + +### payment.captured + +Emitted when a payment is captured. + +#### Payload + +```ts +{ + id, // the ID of the payment +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) - [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) + +*** + +### payment.refunded + +Emitted when a payment is refunded. + +#### Payload + +```ts +{ + id, // the ID of the payment +} +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/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) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/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) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/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) -- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) + +*** + +## Product Category Events + +### Summary + +|Event|Description| +|---|---| +|product-category.created|Emitted when product categories are created.| +|product-category.updated|Emitted when product categories are updated.| +|product-category.deleted|Emitted when product categories are deleted.| + +### product-category.created + +Emitted when product categories are created. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) + +*** + +### product-category.updated + +Emitted when product categories are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) + +*** + +### product-category.deleted + +Emitted when product categories are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) + +*** + +## Product Collection Events + +### Summary + +|Event|Description| +|---|---| +|product-collection.created|Emitted when product collections are created.| +|product-collection.updated|Emitted when product collections are updated.| +|product-collection.deleted|Emitted when product collections are deleted.| + +### product-collection.created + +Emitted when product collections are created. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) + +*** + +### product-collection.updated + +Emitted when product collections are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) + +*** + +### product-collection.deleted + +Emitted when product collections are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) + +*** + +## Product Option Events + +### Summary + +|Event|Description| +|---|---| +|product-option.updated|Emitted when product options are updated.| +|product-option.created|Emitted when product options are created.| +|product-option.deleted|Emitted when product options are deleted.| + +### product-option.updated + +Emitted when product options are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) + +*** + +### product-option.created + +Emitted when product options are created. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) + +*** + +### product-option.deleted + +Emitted when product options are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) + +*** + +## Product Tag Events + +### Summary + +|Event|Description| +|---|---| +|product-tag.updated|Emitted when product tags are updated.| +|product-tag.created|Emitted when product tags are created.| +|product-tag.deleted|Emitted when product tags are deleted.| + +### product-tag.updated + +Emitted when product tags are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) + +*** + +### product-tag.created + +Emitted when product tags are created. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) + +*** + +### product-tag.deleted + +Emitted when product tags are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) + +*** + +## Product Type Events + +### Summary + +|Event|Description| +|---|---| +|product-type.updated|Emitted when product types are updated.| +|product-type.created|Emitted when product types are created.| +|product-type.deleted|Emitted when product types are deleted.| + +### product-type.updated + +Emitted when product types are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) + +*** + +### product-type.created + +Emitted when product types are created. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) + +*** + +### product-type.deleted + +Emitted when product types are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) + +*** + +## Product Variant Events + +### Summary + +|Event|Description| +|---|---| +|product-variant.updated|Emitted when product variants are updated.| +|product-variant.created|Emitted when product variants are created.| +|product-variant.deleted|Emitted when product variants are deleted.| + +### product-variant.updated + +Emitted when product variants are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) - [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/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) -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) + +*** + +### product-variant.created + +Emitted when product variants are created. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/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) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### product-variant.deleted + +Emitted when product variants are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) + +*** + +## Product Events + +### Summary + +|Event|Description| +|---|---| +|product.updated|Emitted when products are updated.| +|product.created|Emitted when products are created.| +|product.deleted|Emitted when products are deleted.| + +### product.updated + +Emitted when products are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### product.created + +Emitted when products are created. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### product.deleted + +Emitted when products are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) -- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/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) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/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) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/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) -- [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) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/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) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) + +*** + +## Region Events + +### Summary + +|Event|Description| +|---|---| +|region.updated|Emitted when regions are updated.| +|region.created|Emitted when regions are created.| +|region.deleted|Emitted when regions are deleted.| + +### region.updated + +Emitted when regions are updated. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) + +*** + +### region.created + +Emitted when regions are created. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) + +*** + +### region.deleted + +Emitted when regions are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) -- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/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) -- [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) + +*** + +## Sales Channel Events + +### Summary + +|Event|Description| +|---|---| +|sales-channel.created|Emitted when sales channels are created.| +|sales-channel.updated|Emitted when sales channels are updated.| +|sales-channel.deleted|Emitted when sales channels are deleted.| + +### sales-channel.created + +Emitted when sales channels are created. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [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) + +*** + +### sales-channel.updated + +Emitted when sales channels are updated. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/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) -- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/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) -- [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) -- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/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) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/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) -- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) -- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) + +*** + +### sales-channel.deleted + +Emitted when sales channels are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) + +*** + +## User Events + +### Summary + +|Event|Description| +|---|---| +|user.created|Emitted when users are created.| +|user.updated|Emitted when users are updated.| +|user.deleted|Emitted when users are deleted.| + +### user.created + +Emitted when users are created. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) + +*** + +### user.updated + +Emitted when users are updated. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) + +*** + +### user.deleted + +Emitted when users are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. + - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) - [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) -- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) -## Steps +# 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. + +## db:setup + +Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. + +It also updates your `.env` file with the database name. + +```bash +npx medusa db:setup --db +``` + +Use this command if you're setting up a Medusa project or database manually. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--db \\`|The database name.|Yes|-| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--no-interactive\`|Disable the command's prompts.|No|-| + +*** + +## db:create + +Creates a database for the Medusa application with the specified name, if it doesn't exit. + +It also updates your `.env` file with the database name. + +```bash +npx medusa db:create --db +``` + +Use this command if you want to only create a database. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--db \\`|The database name.|Yes|-| +|\`--no-interactive\`|Disable the command's prompts.|No|-| + +*** + +## db:generate + +Generate a migration file for the latest changes in one or more modules. + +```bash +npx medusa db:generate +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| + +*** + +## db:migrate + +Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. + +```bash +npx medusa db:migrate +``` + +Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from +|No|Data migration scripts are run by default starting from +| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| + +*** + +## db:rollback + +Revert the last migrations ran on one or more modules. + +```bash +npx medusa db:rollback +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| + +*** + +## db:sync-links + +Sync the database with the link definitions in your application, including the definitions in Medusa's modules. + +```bash +npx medusa db:sync-links +``` + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--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\`| + + +# 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). + +```bash +npx medusa exec [file] [args...] +``` + +## Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| + + +# 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.| + + +# plugin Commands - Medusa CLI Reference + +Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. + +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## plugin:publish + +Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. + +```bash +npx medusa plugin:publish +``` + +*** + +## plugin:add + +Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. + +```bash +npx medusa plugin:add [names...] +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| + +*** + +## plugin:develop + +Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. + +```bash +npx medusa plugin:develop +``` + +*** + +## plugin:db:generate + +Generate migrations for all modules in a plugin. + +```bash +npx medusa plugin:db:generate +``` + +*** + +## plugin:build + +Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. + +```bash +npx medusa plugin:build +``` + + +# 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. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + +# user Command - Medusa CLI Reference + +Create a new admin user. + +```bash +npx medusa user --email [--password ] +``` + +## Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \\`|The user's email.|Yes|-| +|\`-p \\`|The user's password.|No|-| +|\`-i \\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| + + +# Medusa CLI Reference + +The Medusa CLI tool provides commands that facilitate your development. + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +## Usage + +In your Medusa application's directory, you can use the Medusa CLI tool using NPX. + +For example: + +```bash +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. + +## db:setup + +Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. + +It also updates your `.env` file with the database name. + +```bash +npx medusa db:setup --db +``` + +Use this command if you're setting up a Medusa project or database manually. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--db \\`|The database name.|Yes|-| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--no-interactive\`|Disable the command's prompts.|No|-| + +*** + +## db:create + +Creates a database for the Medusa application with the specified name, if it doesn't exit. + +It also updates your `.env` file with the database name. + +```bash +npx medusa db:create --db +``` + +Use this command if you want to only create a database. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--db \\`|The database name.|Yes|-| +|\`--no-interactive\`|Disable the command's prompts.|No|-| + +*** + +## db:generate + +Generate a migration file for the latest changes in one or more modules. + +```bash +npx medusa db:generate +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| + +*** + +## db:migrate + +Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. + +```bash +npx medusa db:migrate +``` + +Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from +|No|Data migration scripts are run by default starting from +| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| + +*** + +## db:rollback + +Revert the last migrations ran on one or more modules. + +```bash +npx medusa db:rollback +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| + +*** + +## db:sync-links + +Sync the database with the link definitions in your application, including the definitions in Medusa's modules. + +```bash +npx medusa db:sync-links +``` + +### Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--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\`| + + +# 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). + +```bash +npx medusa exec [file] [args...] +``` + +## Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| + + +# 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.| + + +# plugin Commands - Medusa CLI Reference + +Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. + +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## plugin:publish + +Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. + +```bash +npx medusa plugin:publish +``` + +*** + +## plugin:add + +Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. + +```bash +npx medusa plugin:add [names...] +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| + +*** + +## plugin:develop + +Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. + +```bash +npx medusa plugin:develop +``` + +*** + +## plugin:db:generate + +Generate migrations for all modules in a plugin. + +```bash +npx medusa plugin:db:generate +``` + +*** + +## plugin:build + +Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. + +```bash +npx medusa plugin:build +``` + + +# 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. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + +# user Command - Medusa CLI Reference + +Create a new admin user. + +```bash +npx medusa user --email [--password ] +``` + +## Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \\`|The user's email.|Yes|-| +|\`-p \\`|The user's password.|No|-| +|\`-i \\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| + + +# Medusa CLI Reference + +The Medusa CLI tool provides commands that facilitate your development. + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +## Usage + +In your Medusa application's directory, you can use the Medusa CLI tool using NPX. + +For example: + +```bash +npx medusa --help +``` + +*** + + +# Authentication in JS SDK + +In this guide, you'll learn about the default authentication setup when using the JS SDK, how to customize it, and how to send authenticated requests to Medusa's APIs. + +## Default Authentication Settings in JS SDK + +The JS SDK facilitates authentication by storing and managing the necessary authorization headers or sessions for you. + +There are three types of authentication: + +|Method|Description|When to use| +|---|---|---| +|JWT token (default)|When you log in a user, the JS SDK stores the JWT for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK clears the stored JWT.|| +|Cookie session|When you log in a user, the JS SDK stores the session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK destroys the session cookie using Medusa's API.|| +|Secret API Key|Only available for admin users. You pass the API key in the JS SDK configurations, and it's always passed in the headers of all requests to the Medusa API.|| + +*** + +## JS SDK Authentication Configurations + +The JS SDK provides a set of configurations to customize the authentication method and storage. You can set these configurations when initializing the SDK. + +For a full list of JS SDK configurations and their possible values, check out the [JS SDK Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#js-sdk-configurations/index.html.md) documentation. + +### Authentication Type + +By default, the JS SDK uses JWT token (`jwt`) authentication. You can change the authentication method or type by setting the `auth.type` configuration to `session`. + +For example: + +```ts +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + // ... + auth: { + type: "session", + }, +}) +``` + +To use a secret API key instead, pass it in the `apiKey` configuration instead: + +```ts +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + // ... + apiKey: "your-api-key", +}) +``` + +The provided API key will be passed in the headers of all requests to the Medusa API. + +### Change JWT Authentication Storage + +By default, the JS SDK stores the JWT token in the `localStorage` under the `medusa_auth_token` key. + +Some environments or use cases may require a different storage method or `localStorage` may not be available. For example, if you're building a mobile app with React Native, you might want to use `AsyncStorage` instead of `localStorage`. + +You can change the storage method by setting the `auth.jwtTokenStorageMethod` configuration to one of the following values: + +|Value|Description| +|---|---| +|\`local\`|Uses | +|\`session\`|Uses | +|\`memory\`|Uses a memory storage method. This means the token will be cleared when the user refreshes the page or closes the browser tab or window. This is also useful when using the JS SDK in a server-side environment.| +|\`custom\`|Uses a custom storage method. This means you can provide your own implementation of the storage method. For example, you can use | +|\`nostore\`|Does not store the JWT token. This means you have to manually set the authorization header for each request. This is useful when you want to use a different authentication method or when you're using the JS SDK in a server-side environment.| + +#### Custom Authentication Storage in JS SDK + +To use a custom storage method, you need to set the `auth.jwtTokenStorageMethod` configuration to `custom` and provide your own implementation of the storage method in the `auth.storage` configuration. + +The object or class passed to `auth.storage` configuration must have the following methods: + +- `setItem`: A function that accepts a key and value to store the JWT token. +- `getItem`: A function that accepts a key to retrieve the JWT token. +- `removeItem`: A function that accepts a key to remove the JWT token from storage. + +For example, to use `AsyncStorage` in React Native: + +```ts +import AsyncStorage from "@react-native-async-storage/async-storage" +import Medusa from "@medusajs/js-sdk" + +let MEDUSA_BACKEND_URL = "http://localhost:9000" + +if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { + MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL +} + +export const sdk = new Medusa({ + baseUrl: MEDUSA_BACKEND_URL, + debug: process.env.NODE_ENV === "development", + publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, + auth: { + type: "jwt", + jwtTokenStorageMethod: "custom", + storge: AsyncStorage, + }, +}) +``` + +In this example, you specify the `jwtTokenStorageMethod` as `custom` and set the `storage` configuration to `AsyncStorage`. This way, the JS SDK will use `AsyncStorage` to store and manage the JWT token instead of `localStorage`. + +### Change Cookie Session Credentials Options + +By default, if you set the `auth.type` configuration in the JS SDK to `session`, the JS SDK will pass the `credentials: include` option in the underlying [fetch requests](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials). + +However, some platforms or environments may not support passing this option. For example, if you're using the JS SDK in a server-side environment or a mobile app, you might want to set the `credentials` option to `same-origin` or `omit`. + +You can change the `credentials` option by setting the `auth.fetchCredentials` configuration to one of the following values: + +|Value|Description| +|---|---| +|\`include\`|Passes the | +|\`same-origin\`|Passes the | +|\`omit\`|Passes the | + +For example: + +```ts +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + // ... + auth: { + type: "session", + fetchCredentials: "same-origin", + }, +}) +``` + +In this example, you set the `fetchCredentials` configuration to `same-origin`, which means the JS SDK will include cookies and authorization headers in the requests to the Medusa API only if the request is made to the same origin as the current page. + +*** + +## Sending Authenticated Requests in JS SDK + +If you're using an API key for authentication, you don't need to log in the user. + +The JS SDK has an `auth.login` method that allows you to login admin users, customers, or any [actor type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) with any [auth provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). + +Not only does this method log in the user, but it also stores the JWT token or session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. + +For example: + +### Admin User + +```ts +sdk.auth.login("user", "emailpass", { + email, + password, +}) +.then((data) => { + if (typeof data === "object" && data.location){ + // authentication requires more actions + } + // user is authenticated +}) +.catch((error) => { + // authentication failed +}) +``` + +### Customer + +```ts +sdk.auth.login("customer", "emailpass", { + email, + password, +}) +.then((data) => { + if (typeof data === "object" && data.location){ + // authentication requires more actions + } + // customer is authenticated +}) +.catch((error) => { + // authentication failed +}) +``` + +### Custom + +```ts +sdk.auth.login("manager", "emailpass", { + email, + password, +}) +.then((data) => { + if (typeof data === "object" && data.location){ + // authentication requires more actions + } + // manager is authenticated +}) +.catch((error) => { + // authentication failed +}) +``` + +In this example, you call the `sdk.auth.login` method passing it the actor type (for example, `user`), the provider (`emailpass`), and the credentials. + +If the authentication is successful, there are two types of returned data: + +- An object with a `location` property: This means the authentication requires more actions, which happens when using third-party authentication providers, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md). In that case, you need to redirect the customer to the location to complete their authentication. + - Refer to the [Third-Party Login in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md) guide for an example implementation. +- A string: This means the authentication was successful, and the user is logged in. The JS SDK automatically stores the JWT token or session cookie for you and includes it in the headers of all requests to the Medusa API. All requests you send afterwards will be authenticated with the stored token or session cookie. + +If the authentication fails, the `catch` block will be executed, and you can handle the error accordingly. + +You can learn more about this method in the [auth.login reference](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md). + +### Manually Set JWT Token + +If you need to set the JWT token manually, you can use the `sdk.client.setToken` method. All subsequent requests will be authenticated with the provided token. + +For example: + +```ts +sdk.client.setToken("your-jwt-token") + +// all requests sent after this will be authenticated with the provided token +``` + +You can also clear the token manually as explained in the [Manually Clearing JWT Token](#manually-clearing-jwt-token) section. + +*** + +## Logout in JS SDK + +If you're using an API key for authentication, you can't log out the user. You'll have to unset the API key in the JS SDK configurations. + +The JS SDK has an `auth.logout` method that allows you to log out the currently authenticated user. + +If the JS SDK's authentication type is `jwt`, the method will only clear the stored JWT token from the local storage. If the authentication type is `session`, the method will destroy the session cookie using Medusa's `/auth/session` API route. + +Any request sent after logging out will not be authenticated, and you will need to log in again to authenticate the user. + +For example: + +```ts +sdk.auth.logout() +.then(() => { + // user is logged out +}) +``` + +You can learn more about this method in the [auth.logout reference](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md). + +### Manually Clearing JWT Token + +If you need to clear the JWT token manually, you can use the `sdk.client.clearToken` method. This will remove the token from the local storage and all subsequent requests will not be authenticated. + +For example: + +```ts +sdk.client.clearToken() + +// all requests sent after this will not be authenticated +``` + + +# Medusa JS SDK + +In this documentation, you'll learn how to install and use Medusa's JS SDK. + +## What is Medusa JS SDK? + +Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts. + +*** + +## How to Install Medusa JS SDK? + +The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations. + +To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command: + +```bash npm2yarn +npm install @medusajs/js-sdk@latest @medusajs/types@latest +``` + +You install two libraries: + +- `@medusajs/js-sdk`: the Medusa JS SDK. +- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development. + +*** + +## Setup JS SDK + +In your project, create the following `config.ts` file: + +For admin customizations, create this file at `src/admin/lib/config.ts`. + +### Admin (Medusa project) + +```ts title="src/admin/lib/config.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", + }, +}) +``` + +### Admin (Medusa Plugin) + +```ts title="src/admin/lib/config.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: __BACKEND_URL__ || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +### Storefront + +```ts title="config.ts" +import Medusa from "@medusajs/js-sdk" + +let MEDUSA_BACKEND_URL = "http://localhost:9000" + +if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { + MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL +} + +export const sdk = new Medusa({ + baseUrl: MEDUSA_BACKEND_URL, + debug: process.env.NODE_ENV === "development", + publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, +}) +``` + +In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md) chapter. + +### JS SDK Configurations + +The `Medusa` initializer accepts as a parameter an object with the following properties: + +|Property|Description|Default| +|---|---|---|---|---| +|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-| +|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-| +|\`auth.type\`|A string that specifies the user authentication method to use.|-| +|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`| +|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`| +|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-| +|\`auth.fetchCredentials\`|By default, if |\`include\`| +|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-| +|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-| +|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`| +|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's | + +*** + +## Manage Authentication in JS SDK + +The JS SDK supports different types of authentication methods and allow you to flexibly configure them. + +To learn more about configuring authentication in the JS SDK and sending authenticated requests, refer to the [Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/auth/overview/index.html.md) guide. + +*** + +## Send Requests to Custom Routes + +The sidebar shows the different methods that you can use to send requests to Medusa's API routes. + +To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request. + +For example, to send a request to a custom route at `http://localhost:9000/custom`: + +### GET + +```ts +sdk.client.fetch(`/custom`) +.then((data) => { + console.log(data) +}) +``` + +### POST + +```ts +sdk.client.fetch(`/custom`, { + method: "post", + body: { + id: "123", + }, +}).then((data) => { + console.log(data) +}) +``` + +### DELETE + +```ts +sdk.client.fetch(`/custom`, { + method: "delete", +}).then(() => { + console.log("success") +}) +``` + +The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK. + +In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method. + +The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned. + +*** + +## Stream Server-Sent Events + +The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server. + +For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`: + +```ts title="src/api/admin/stream/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("data: Streaming data...\n\n") + }, 3000) + + req.on("close", () => { + clearInterval(interval) + res.end() + }) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` + +Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data: + +```tsx title="src/admin/route/stream/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading, Button, Text } from "@medusajs/ui" +import { useState } from "react" +import { sdk } from "../../lib/sdk" + +const StreamTestPage = () => { + const [messages, setMessages] = useState([]) + const [isStreaming, setIsStreaming] = useState(false) + const [abortStream, setAbortStream] = useState<(() => void) | null>(null) + + const startStream = async () => { + setIsStreaming(true) + setMessages([]) + + const { stream, abort } = await sdk.client.fetchStream("/admin/stream") + + if (!stream) { + console.error("Failed to start stream") + setIsStreaming(false) + return + } + + // Store the abort function for the abort button + setAbortStream(() => abort) + + try { + for await (const chunk of stream) { + // Since the server sends plain text, convert to string + const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk)) + setMessages((prev) => [...prev, message.trim()]) + } + } catch (error) { + // Don't log abort errors as they're expected when user clicks abort + if (error instanceof Error && error.name !== "AbortError") { + console.error("Stream error:", error) + } + } finally { + setIsStreaming(false) + setAbortStream(null) + } + } + + const handleAbort = () => { + if (abortStream) { + abortStream() + setIsStreaming(false) + setAbortStream(null) + } + } + + return ( + + + fetchStream Example + + +
+
+ + + +
+ +
+ {messages.length === 0 ? ( + No messages yet... + ) : ( + messages.map((msg, index) => ( +
+ {msg} +
+ )) + )} +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Stream Test", +}) + +export default StreamTestPage +``` + +`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties: + +- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data. +- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server. + +In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI. + +*** + +## Handle Errors + +If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties: + +- `status`: The HTTP status code of the response. +- `statusText`: The error code. For example, `Unauthorized`. +- `message`: The error message. For example, `Invalid credentials`. + +You can use these properties to handle errors in your application. + +For example: + +### Promise + +```ts +sdk.store.customer.listAddress() +.then(({ addresses, count, offset, limit }) => { + // no errors occurred + // do something with the data + console.log(addresses) +}) +.catch((error) => { + const fetchError = error as FetchError + + if (fetchError.statusText === "Unauthorized") { + // redirect to login page + } else { + // handle other errors + } +}) +``` + +### Async/Await + +```ts +try { + const { + addresses, + count, + offset, + limit, + } = await sdk.store.customer.listAddress() + // no errors occurred + // do something with the data + console.log(addresses) +} catch (error) { + const fetchError = error as FetchError + + if (fetchError.statusText === "Unauthorized") { + // redirect to login page + } else { + // handle other errors + } +} +``` + +In the example above, you handle errors in two ways: + +- Since the JS SDK's methods return a Promise, you can use the `catch` method to handle errors. +- You can use the `try...catch` statement to handle errors when using `async/await`. This is useful when you're executing the methods as part of a larger function. + +In the `catch` method or statement, you have access to the error object of type `FetchError`. + +An example of handling the error is to check if the error's `statusText` is `Unauthorized`. If so, you can redirect the customer to the login page. Otherwise, you can handle other errors by showing an alert, for example. + +*** + +## Pass Headers in Requests + +There are two ways to pass custom headers in requests when using the JS SDK: + +1. Using the `globalHeaders` configuration: This is useful when you want to pass the same headers in all requests. For example, if you want to pass a custom header for tracking purposes: + +```ts +const sdk = new Medusa({ + // ... + globalHeaders: { + "x-tracking-id": "123456789", + }, +}) +``` + +2. Using the headers parameter of a specific method. Every method has as a last parameter a headers parameter, which is an object of headers to pass in the request. This is useful when you want to pass a custom header in specific requests. For example, to disable HTTP compression for specific requests: + +```ts +sdk.store.product.list({ + limit, + offset, +}, { + "x-no-compression": "false", +}) +``` + +In the example above, you pass the `x-no-compression` header in the request to disable HTTP compression. You pass it as the last parameter of the `sdk.store.product.list` method. + +The JS SDK appends request-specific headers to authentication headers and headers configured in the `globalHeaders` configuration. So, in the example above, the `x-no-compression` header is passed in the request along with the authentication headers and any headers configured in the `globalHeaders` configuration. + +*** + +## Medusa JS SDK Tips + +### Use Tanstack (React) Query in Admin Customizations + +In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes. + +Tanstack Query is installed by default in your Medusa application. + +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. + +Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests. + +For example: + +### Query + +```tsx title="src/admin/widgets/product-widget.ts" +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" +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 +``` + +Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation). + +### Cache in Next.js Projects + +Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request. + +In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching. + +For example: + +```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]} +sdk.store.product.list({}, { + next: { + tags: ["products"], + }, +}) +``` + +The `tags` property accepts an array of tags that the data is cached under. + +Then, to purge the cache later, use Next.js's `revalidateTag` utility: + +```ts +import { revalidateTag } from "next/cache" + +// ... + +revalidateTag("products") +``` + +Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag). + + +# Implement Custom Line Item Pricing in Medusa + +In this guide, you'll learn how to add line items with custom prices to a cart in Medusa. + +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 managing carts and adding line items to them. + +By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products. + +To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +### Summary + +This guide will teach you how to: + +- Install and set up Medusa. +- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver. +- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service. + +![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg) + +- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.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. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed 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 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 credentials and submit the form. Afterwards, 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: Integrate GoldAPI.io + +### Prerequisites + +- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io) + +To integrate third-party services into Medusa, you create a custom module. 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 this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price. + +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/metal-prices`. + +![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.jpg) + +### Create Module's 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. + +In this section, you'll create the Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals. + +Start by creating the file `src/modules/metal-prices/service.ts` with the following content: + +![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg) + +```ts title="src/modules/metal-prices/service.ts" +type Options = { + accessToken: string + sandbox?: boolean +} + +export default class MetalPricesModuleService { + protected options_: Options + + constructor({}, options: Options) { + this.options_ = options + } +} +``` + +A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options: + +- `accessToken`: The access token for the GoldAPI.io service. +- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment. + +The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property. + +A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). + +#### Add Method to Retrieve Metal Prices + +Next, you'll add the method to retrieve the metal prices from the third-party service. + +First, add the following types at the beginning of `src/modules/metal-prices/service.ts`: + +```ts title="src/modules/metal-prices/service.ts" +export enum MetalSymbols { + Gold = "XAU", + Silver = "XAG", + Platinum = "XPT", + Palladium = "XPD" +} + +export type PriceResponse = { + metal: MetalSymbols + currency: string + exchange: string + symbol: string + price: number + [key: string]: unknown +} + +``` + +The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint. + +Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class: + +```ts title="src/modules/metal-prices/service.ts" +import { MedusaError } from "@medusajs/framework/utils" + +// ... + +export default class MetalPricesModuleService { + // ... + async getMetalPrice( + symbol: MetalSymbols, + currency: string + ): Promise { + const upperCaseSymbol = symbol.toUpperCase() + const upperCaseCurrency = currency.toUpperCase() + + return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, { + headers: { + "x-access-token": this.options_.accessToken, + "Content-Type": "application/json", + }, + redirect: "follow", + }).then((response) => response.json()) + .then((response) => { + if (response.error) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + response.error + ) + } + + return response + }) + } +} +``` + +The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers. + +If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`. -- [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) -- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/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) -- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/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) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) -- [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) -- [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) -- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) -- [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) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) -- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) -- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) -- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/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) -- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md) -- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/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) -- [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) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/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) -- [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) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [deleteDraftOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteDraftOrdersStep/index.html.md) -- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md) -- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) -- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) -- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) -- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/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) -- [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) -- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) -- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) -- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) -- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/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) -- [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) -- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) -- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/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) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/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) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/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) -- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) -- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) -- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/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) -- [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) -- [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) -- [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) -- [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) -- [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) -- [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) -- [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) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/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) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) -- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/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) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/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) -- [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) -- [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) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/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) -- [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) -- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/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) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/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) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/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) -- [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) -- [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) -- [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) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) -- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) -- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/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) -- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md) -- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) -- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md) -- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/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) -- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) -- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/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) -- [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) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/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) -- [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) -- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) -- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) -- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/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) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/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) -- [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) -- [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) -- [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) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/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) -- [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) -- [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) -- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/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) -- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/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) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) +#### Add Helper Methods + +You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings: + +```ts title="src/modules/metal-prices/service.ts" +export default class MetalPricesModuleService { + // ... + async getMetalSymbols(): Promise { + return Object.values(MetalSymbols) + } +} +``` + +The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol: + +```ts title="src/modules/metal-prices/service.ts" +export default class MetalPricesModuleService { + // ... + async getMetalSymbol(name: string): Promise { + const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() + return MetalSymbols[formattedName as keyof typeof MetalSymbols] + } +} +``` + +You'll use these methods in later steps. + +### 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/metal-prices/index.ts` with the following content: + +![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg) + +```ts title="src/modules/metal-prices/index.ts" +import { Module } from "@medusajs/framework/utils" +import MetalPricesModuleService from "./service" + +export const METAL_PRICES_MODULE = "metal-prices" + +export default Module(METAL_PRICES_MODULE, { + service: MetalPricesModuleService, +}) +``` + +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 `metal-prices`. +2. An object with a required property `service` indicating the module's service. + +### 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/metal-prices", + options: { + accessToken: process.env.GOLD_API_TOKEN, + sandbox: process.env.GOLD_API_SANDBOX === "true", + }, + }, + ], +}) +``` + +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. + +The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables. + +You'll find the access token at the top of your GoldAPI.io dashboard. + +![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png) + +Set the access token as an environment variable in `.env`: + +```bash +GOLD_API_TOKEN= +``` + +You'll start using the module in the next steps. + +*** + +## Step 3: Add Custom Item to Cart Workflow + +In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in 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. Then, you execute the workflow from other customizations, such as in an endpoint. + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) + +The workflow you'll implement in this section has the following steps: + +- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query. +- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query +- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service. +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart. +- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query. + +`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`. + +### getVariantMetalPricesStep + +The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input. + +To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content: + +![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg) + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { ProductVariantDTO } from "@medusajs/framework/types" +import { METAL_PRICES_MODULE } from "../../modules/metal-prices" +import MetalPricesModuleService from "../../modules/metal-prices/service" + +export type GetVariantMetalPricesStepInput = { + variant: ProductVariantDTO & { + calculated_price?: { + calculated_amount: number + } + } + currencyCode: string + quantity?: number +} + +export const getVariantMetalPricesStep = createStep( + "get-variant-metal-prices", + async ({ + variant, + currencyCode, + quantity = 1, + }: GetVariantMetalPricesStepInput, { container }) => { + const metalPricesModuleService: MetalPricesModuleService = + container.resolve(METAL_PRICES_MODULE) + + // TODO + } +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `get-variant-metal-prices`. +2. An async function that receives two parameters: + - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees. + - 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, so far you only resolve the Metal Prices Module's service from the Medusa container. + +Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +import { MedusaError } from "@medusajs/framework/utils" +``` + +And replace the `TODO` in the step function with the following: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +const variantMetal = variant.options.find( + (option) => option.option?.title === "Metal" +)?.value +const metalSymbol = await metalPricesModuleService + .getMetalSymbol(variantMetal || "") + +if (!metalSymbol) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol." + ) +} + +if (!variant.weight) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant doesn't have weight. Make sure the variant has weight to calculate its price." + ) +} + +// TODO retrieve custom price +``` + +In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service. + +If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight. + +Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following: + +```ts title="src/workflows/steps/get-variant-metal-prices.ts" +let price = variant.calculated_price?.calculated_amount || 0 +const weight = variant.weight +const { price: metalPrice } = await metalPricesModuleService.getMetalPrice( + metalSymbol as MetalSymbols, currencyCode +) +price += (metalPrice * weight * quantity) + +return new StepResponse(price) +``` + +In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service. + +Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it. + +Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price. + +### Create addCustomToCartWorkflow + +Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart. + +Create the file `src/workflows/add-custom-to-cart.ts` with the following content: + +![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg) + +```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights} +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { QueryContext } from "@medusajs/framework/utils" + +type AddCustomToCartWorkflowInput = { + cart_id: string + item: { + variant_id: string + quantity: number + metadata?: Record + } +} + +export const addCustomToCartWorkflow = createWorkflow( + "add-custom-to-cart", + ({ cart_id, item }: AddCustomToCartWorkflowInput) => { + const { data: carts } = useQueryGraphStep({ + entity: "cart", + filters: { id: cart_id }, + fields: ["id", "currency_code"], + }) + + const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "*", + "options.*", + "options.option.*", + "calculated_price.*", + ], + filters: { + id: item.variant_id, + }, + options: { + throwIfKeyNotFound: true, + }, + context: { + calculated_price: QueryContext({ + currency_code: carts[0].currency_code, + }), + }, + }).config({ name: "retrieve-variant" }) + // TODO add more steps + } +) +``` -# Events Reference +You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters: -This documentation page includes the list of all events emitted by [Medusa's workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md). +1. The workflow's unique name, which is `add-custom-to-cart`. +2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata. -## Auth Events +In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code. -### Summary +You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context. -|Event|Description| -|---|---| -|auth.password\_reset|Emitted when a reset password token is generated. You can listen to this event -to send a reset password email to the user or customer, for example.| +Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import: -### auth.password\_reset +```ts title="src/workflows/add-custom-to-cart.ts" +import { + getVariantMetalPricesStep, + GetVariantMetalPricesStepInput, +} from "./steps/get-variant-metal-prices" +``` -Emitted when a reset password token is generated. You can listen to this event -to send a reset password email to the user or customer, for example. +Then, replace the `TODO` in the workflow with the following: -#### Payload +```ts title="src/workflows/add-custom-to-cart.ts" +const price = getVariantMetalPricesStep({ + variant: variants[0], + currencyCode: carts[0].currency_code, + quantity: item.quantity, +} as unknown as GetVariantMetalPricesStepInput) -```ts -{ - entity_id, // The identifier of the user or customer. For example, an email address. - actor_type, // The type of actor. For example, "customer", "user", or custom. - token, // The generated token. -} +// TODO add item with custom price to cart ``` -#### Workflows Emitting this Event +You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file: -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) +```ts title="src/workflows/add-custom-to-cart.ts" +import { transform } from "@medusajs/framework/workflows-sdk" +import { addToCartWorkflow } from "@medusajs/medusa/core-flows" +``` -*** +Then, replace the `TODO` in the workflow with the following: -## Cart Events +```ts title="src/workflows/add-custom-to-cart.ts" +const itemToAdd = transform({ + item, + price, +}, (data) => { + return [{ + ...data.item, + unit_price: data.price, + }] +}) -### Summary +addToCartWorkflow.runAsStep({ + input: { + items: itemToAdd, + cart_id, + }, +}) -|Event|Description| -|---|---| -|cart.created|Emitted when a cart is created.| -|cart.updated|Emitted when a cart's details are updated.| -|cart.region\_updated|Emitted when the cart's region is updated. This -event is emitted alongside the | -|cart.customer\_transferred|Emitted when the customer in the cart is transferred.| +// TODO retrieve and return cart +``` -### cart.created +You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart. -Emitted when a cart is created. +A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). -#### Payload +Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file: -```ts -{ - id, // The ID of the cart -} +```ts title="src/workflows/add-custom-to-cart.ts" +import { WorkflowResponse } from "@medusajs/framework/workflows-sdk" ``` -#### Workflows Emitting this Event +And replace the last `TODO` in the workflow with the following: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +```ts title="src/workflows/add-custom-to-cart.ts" +const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + filters: { id: cart_id }, + fields: ["id", "items.*"], +}).config({ name: "refetch-cart" }) -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +return new WorkflowResponse({ + cart: updatedCarts[0], +}) +``` + +In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart. + +In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart. *** -### cart.updated +## Step 4: Create Add Custom Item to Cart API Route -Emitted when a cart's details are updated. +Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route. -#### Payload +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/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart. -```ts -{ - id, // The ID of the cart -} -``` +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). -#### Workflows Emitting this Event +### Create API Route -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. -- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) +The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content: -*** +![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg) -### cart.region\_updated +```ts title="src/api/store/carts/[id]/line-items-metals/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/framework/types" +import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart" -Emitted when the cart's region is updated. This -event is emitted alongside the `cart.updated` event. +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const item = req.validatedBody -#### Payload + const { result } = await addCustomToCartWorkflow(req.scope) + .run({ + input: { + cart_id: id, + item, + }, + }) -```ts -{ - id, // The ID of the cart + res.status(200).json({ cart: result.cart }) } ``` -#### Workflows Emitting this Event +Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +1. A request object with details and context on the request, such as path and body parameters. +2. A response object to manipulate and send the response. -- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) +In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). -*** +Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart. -### cart.customer\_transferred +Finally, you return a response with the updated cart's details. -Emitted when the customer in the cart is transferred. +### Add Request Body Validation Middleware -#### Payload +To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema. -```ts -{ - id, // The ID of the cart - customer_id, // The ID of the customer -} -``` +A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory. -#### Workflows Emitting this Event +Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content: -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) +![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg) -*** +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { + StoreAddCartLineItem, +} from "@medusajs/medusa/api/store/carts/validators" -## Customer Events +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/carts/:id/line-items-metals", + method: "POST", + middlewares: [ + validateAndTransformBody( + StoreAddCartLineItem + ), + ], + }, + ], +}) +``` -### Summary +In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes. -|Event|Description| -|---|---| -|customer.created|Emitted when a customer is created.| -|customer.updated|Emitted when a customer is updated.| -|customer.deleted|Emitted when a customer is deleted.| +You pass in the `routes` array an object having the following properties: -### customer.created +- `matcher`: The route to apply the middleware on. +- `method`: The HTTP method to apply the middleware on for the specified API route. +- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). -Emitted when a customer is created. +Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters. -#### Payload +Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). -```ts -[{ - id, // The ID of the customer -}] -``` +### Prepare to Test API Route -#### Workflows Emitting this Event +Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +#### Create Product with Metal Variant -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) +You'll first create a product that has a `Metal` option, and variant(s) with values for this option. -*** +Start the Medusa application with the following command: -### customer.updated +```bash npm2yarn +npm run dev +``` -Emitted when a customer is updated. +Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step. -#### Payload +Once you log in, click on Products in the sidebar, then click the Create button at the top right. -```ts -[{ - id, // The ID of the customer -}] -``` +![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png) -#### Workflows Emitting this Event +Then, in the Create Product form: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +1. Enter a name for the product, and optionally enter other details like description. +2. Enable the "Yes, this is a product with variants" toggle. +3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values. -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +Once you're done, click the Continue button. -*** +![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png) -### customer.deleted +You can skip the next two steps by clicking the Continue button again, then the Publish button. -Emitted when a customer is deleted. +Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that: -#### Payload +- Scroll to the Variants section and find the Gold variant. +- Click on the three-dots icon at its right. +- Choose "Edit" from the dropdown. -```ts -[{ - id, // The ID of the customer -}] -``` +![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png) -#### Workflows Emitting this Event +In the side window that opens, find the Weight field, enter the weight, and click the Save button. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) +Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that: -*** +1. Click on the three-dots icon at the top right of the Variants section. +2. Choose "Edit Prices" from the dropdown. -## Fulfillment Events +![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png) -### Summary +For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button. -|Event|Description| -|---|---| -|shipment.created|Emitted when a shipment is created for an order.| -|delivery.created|Emitted when a fulfillment is marked as delivered.| +![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png) -### shipment.created +You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data. -Emitted when a shipment is created for an order. +![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png) -#### Payload +#### Retrieve Publishable API Key -```ts -{ - id, // the ID of the shipment - no_notification, // (boolean) whether to notify the customer -} -``` +All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it. -#### Workflows Emitting this Event +To retrieve the publishable API key, on the Medusa Admin: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +1. Click on Settings in the sidebar at the bottom left. +2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list. -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) +![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png) -*** +3. Click on the publishable API key to copy it. -### delivery.created +![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png) -Emitted when a fulfillment is marked as delivered. +You'll use this key when you test the API route. -#### Payload +### Test API Route -```ts -{ - id, // the ID of the fulfillment -} +To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route: + +```bash +curl 'localhost:9000/store/regions' \ +-H 'x-publishable-api-key: {api_key}' ``` -#### Workflows Emitting this Event +Make sure to replace `{api_key}` with the publishable API key you copied earlier. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +This will return a list of regions. Copy the ID of one of the regions. -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +Then, send a `POST` request to the `/store/carts` API route to create a cart: -*** +```bash +curl -X POST 'localhost:9000/store/carts' \ +-H 'x-publishable-api-key: {api_key}' \ +-H 'Content-Type: application/json' \ +--data '{ + "region_id": "{region_id}" +}' +``` -## Invite Events +Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request. -### Summary +This will return the created cart. Copy the ID of the cart to use it next. -|Event|Description| -|---|---| -|invite.accepted|Emitted when an invite is accepted.| -|invite.created|Emitted when invites are created. You can listen to this event -to send an email to the invited users, for example.| -|invite.deleted|Emitted when invites are deleted.| -|invite.resent|Emitted when invites should be resent because their token was -refreshed. You can listen to this event to send an email to the invited users, -for example.| +Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route: -### invite.accepted +```bash +curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \ +-H 'x-publishable-api-key: {api_key}' \ +-H 'Content-Type: application/json' \ +--data '{ + "variant_id": "{variant_id}", + "quantity": 1 +}' +``` -Emitted when an invite is accepted. +Make sure to replace: -#### Payload +- `{api_key}` with the publishable API key you copied earlier. +- `{cart_id}` with the ID of the cart you created. +- `{variant_id}` with the ID of the Gold variant you created. -```ts +This will return the cart's details, where you can see in its `items` array the item with the custom price: + +```json title="Example Response" { - id, // The ID of the invite + "cart": { + "items": [ + { + "variant_id": "{variant_id}", + "quantity": 1, + "is_custom_price": true, + // example custom price + "unit_price": 2000 + } + ] + } } ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart. -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item. *** -### invite.created - -Emitted when invites are created. You can listen to this event -to send an email to the invited users, for example. - -#### Payload +## Next Steps -```ts -[{ - id, // The ID of the invite -}] -``` +You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart. -#### Workflows Emitting this Event +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. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +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). -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) -*** +# Implement Quote Management in Medusa -### invite.deleted +In this guide, you'll learn how to implement quote management in Medusa. -Emitted when invites are deleted. +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. -#### Payload +By default, the Medusa application provides standard commerce features for orders and carts. However, Medusa's customization capabilities facilitate extending existing features to implement quote-management features. -```ts -[{ - id, // The ID of the invite -}] -``` +By building quote management features, you allow customers to request a quote for a set of products and, once the merchant and customer reach an agreement, you create an order for that quote. Quote management is useful in many use cases, including B2B stores. -#### Workflows Emitting this Event +This guide is based on the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) explaining how to implement some of its quote management features. You can refer to the B2B starter for other features not covered in this guide. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +## Summary -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +By following this guide, you'll add the following features to Medusa: -*** +1. Customers can request a quote for a set of products. +2. Merchants can manage quotes in the Medusa Admin dashboard. They can reject a quote or send a counter-offer, and they can make edits to item prices and quantities. +3. Customers can accept or reject a quote once it's been sent by the merchant. +4. Once the customer accepts a quote, it's converted to an order in Medusa. -### invite.resent +![Quote management system workflow diagram illustrating the complete quote lifecycle: customers request quotes from cart items, merchants review and create detailed quotes with pricing, quotes are sent to customers for approval, and upon acceptance, quotes are automatically converted into processable orders within the Medusa e-commerce platform](https://res.cloudinary.com/dza7lstvk/image/upload/v1741173690/Medusa%20Resources/quote-management-summary_xd319j.jpg) -Emitted when invites should be resent because their token was -refreshed. You can listen to this event to send an email to the invited users, -for example. +To implement these features, you'll be customizing the Medusa server and the Medusa Admin dashboard. -#### Payload +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. -```ts -[{ - id, // The ID of the invite -}] -``` +- [Quote Management Repository](https://github.com/medusajs/examples/tree/main/quote-management): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741171875/OpenApi/quote-management_tbk552.yml): Import this OpenApi Specs file into tools like Postman. -#### Workflows Emitting this Event +*** -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +## Step 1: Install a Medusa Application -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) +### Prerequisites -*** +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) -## Order Edit Events +Start by installing the Medusa application on your machine with the following command: -### Summary +```bash +npx create-medusa-app@latest +``` -|Event|Description| -|---|---| -|order-edit.requested|Emitted when an order edit is requested.| -|order-edit.confirmed|Emitted when an order edit request is confirmed.| -|order-edit.canceled|Emitted when an order edit request is canceled.| +You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). -### order-edit.requested +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. -Emitted when an order edit is requested. +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). -#### Payload +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. Afterwards, you can log in with the new user and explore the dashboard. -```ts -{ - order_id, // The ID of the order - actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order -} -``` +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. -#### Workflows Emitting this Event +*** -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +## Step 2: Add Quote Module -- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) +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 and link its data models to other modules' data models, such as orders and carts. -### order-edit.confirmed +In this step, you'll build a Quote Module that defines the necessary data model to store quotes. -Emitted when an order edit request is confirmed. +Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). -#### Payload +### Create Module Directory -```ts -{ - order_id, // The ID of the order - actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order -} -``` +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/quote`. -#### Workflows Emitting this Event +![Diagram showcasing the directory structure after adding the Quote Module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074268/Medusa%20Resources/quote-1_lxgyyg.jpg) -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +### Create Data Models -- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +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 documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md). -### order-edit.canceled +For the Quote Module, you need to define a `Quote` data model that represents a quote requested by a customer. -Emitted when an order edit request is canceled. +So, start by creating the `Quote` data model. Create the file `src/modules/quote/models/quote.ts` with the following content: -#### Payload +![Diagram showcasing the directory structure after adding the quote model](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074453/Medusa%20Resources/quote-2_lh012l.jpg) -```ts -{ - order_id, // The ID of the order - actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order +```ts title="src/modules/quote/models/quote.ts" highlights={quoteModelHighlights} +import { model } from "@medusajs/framework/utils" + +export enum QuoteStatus { + PENDING_MERCHANT = "pending_merchant", + PENDING_CUSTOMER = "pending_customer", + ACCEPTED = "accepted", + CUSTOMER_REJECTED = "customer_rejected", + MERCHANT_REJECTED = "merchant_rejected", } + +export const Quote = model.define("quote", { + id: model.id().primaryKey(), + status: model + .enum(Object.values(QuoteStatus)) + .default(QuoteStatus.PENDING_MERCHANT), + customer_id: model.text(), + draft_order_id: model.text(), + order_change_id: model.text(), + cart_id: model.text(), +}) ``` -#### Workflows Emitting this Event +You define the `Quote` 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 following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +`Quote` has the following properties: -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- `id`: A unique identifier for the quote. +- `status`: The status of the quote, which can be one of the following: + - `pending_merchant`: The quote is pending the merchant's approval or rejection. + - `pending_customer`: The quote is pending the customer's acceptance or rejection. + - `accepted`: The quote has been accepted by the customer and converted to an order. + - `customer_rejected`: The customer has rejected the quote. + - `merchant_rejected`: The merchant has rejected the quote. +- `customer_id`: The ID of the customer who requested the quote. You'll later learn how to link this to a customer record. +- `draft_order_id`: The ID of the draft order created for the quote. You'll later learn how to link this to an order record. +- `order_change_id`: The ID of the order change created for the quote. An [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) is a record of changes made to an order, such as price or quantity updates of the order's items. These changes are later applied to the order. You'll later learn how to link this to an order change record. +- `cart_id`: The ID of the cart that the quote was created from. The cart will hold the items that the customer wants a quote for. You'll later learn how to link this to a cart record. -*** +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). -## Order Events +### Create Module's Service -### Summary +You now have the necessary data model in the Quote Module, but you need to define the logic to manage it. You do this by creating a service in the module. -|Event|Description| -|---|---| -|order.updated|Emitted when the details of an order or draft order is updated. This -doesn't include updates made by an edit.| -|order.placed|Emitted when an order is placed, or when a draft order is converted to an -order.| -|order.canceled|Emitted when an order is canceld.| -|order.completed|Emitted when orders are completed.| -|order.archived|Emitted when an order is archived.| -|order.fulfillment\_created|Emitted when a fulfillment is created for an order.| -|order.fulfillment\_canceled|Emitted when an order's fulfillment is canceled.| -|order.return\_requested|Emitted when a return request is confirmed.| -|order.return\_received|Emitted when a return is marked as received.| -|order.claim\_created|Emitted when a claim is created for an order.| -|order.exchange\_created|Emitted when an exchange is created for an order.| -|order.transfer\_requested|Emitted when an order is requested to be transferred to -another customer.| +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. -### order.updated +Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md). -Emitted when the details of an order or draft order is updated. This -doesn't include updates made by an edit. +To create the Quote Module's service, create the file `src/modules/quote/service.ts` with the following content: -#### Payload +![Directory structure after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1741075946/Medusa%20Resources/quote-4_hg4bnr.jpg) -```ts -{ - id, // The ID of the order -} +```ts title="src/modules/quote/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import { Quote } from "./models/quote" + +class QuoteModuleService extends MedusaService({ + Quote, +}) {} + +export default QuoteModuleService ``` -#### Workflows Emitting this Event +The `QuoteModuleService` 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. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +So, the `QuoteModuleService` class now has methods like `createQuotes` and `retrieveQuote`. -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) -- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) +Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). -*** +You'll use this service later when you implement custom flows for quote management. -### order.placed +### Export Module Definition -Emitted when an order is placed, or when a draft order is converted to an -order. +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. -#### Payload +So, create the file `src/modules/quote/index.ts` with the following content: -```ts -{ - id, // The ID of the order -} -``` +![Directory structure after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076106/Medusa%20Resources/quote-5_ngitn1.jpg) -#### Workflows Emitting this Event +```ts title="src/modules/quote/index.ts" +import { Module } from "@medusajs/framework/utils" +import QuoteModuleService from "./service" -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +export const QUOTE_MODULE = "quote" -- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +export default Module(QUOTE_MODULE, { + service: QuoteModuleService, +}) +``` -*** +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: -### order.canceled +1. The module's name, which is `quote`. +2. An object with a required property `service` indicating the module's service. -Emitted when an order is canceld. +You also export the module's name as `QUOTE_MODULE` so you can reference it later. -#### Payload +### Add Module to Medusa's Configurations -```ts -{ - id, // The ID of the order -} -``` +Once you finish building the module, add it to Medusa's configurations to start using it. -#### Workflows Emitting this Event +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/quote", + }, + ], +}) +``` -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) +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 -### order.completed +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. -Emitted when orders are completed. +Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md). -#### Payload +Medusa's CLI tool generates the migrations for you. To generate a migration for the Quote Module, run the following command in your Medusa application's directory: -```ts -[{ - id, // The ID of the order -}] +```bash +npx medusa db:generate quote ``` -#### Workflows Emitting this Event +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/quote` that holds the generated migration. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +![The directory structure of the Quote Module after generating the migration](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076301/Medusa%20Resources/quote-6_adzf76.jpg) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +Then, to reflect these migrations on the database, run the following command: + +```bash +npx medusa db:migrate +``` + +The table for the `Quote` data model is now created in the database. *** -### order.archived +## Step 3: Define Links to Other Modules -Emitted when an order is archived. +When you defined the `Quote` data model, you added properties that store the ID of records managed by other modules. For example, the `customer_id` property stores the ID of the customer that requested the quote, but customers are managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). -#### Payload +Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules. -```ts -[{ - id, // The ID of the order -}] -``` +Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it. -#### Workflows Emitting this Event +To learn more about module isolation, refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +In this step, you'll define the following links between the Quote Module's data model and data models in other modules: -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) +1. `Quote` \<> `Cart` data model of the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md): link quotes to the carts they were created from. +2. `Quote` \<> `Customer` data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md): link quotes to the customers who requested them. +3. `Quote` \<> `OrderChange` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to the order changes that record adjustments made to the quote's draft order. +4. `Quote` \<> `Order` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to their draft orders that are later converted to orders. -*** +### Define Quote \<> Cart Link -### order.fulfillment\_created +You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, to define the link between the `Quote` and `Cart` data models, create the file `src/links/quote-cart.ts` with the following content: -Emitted when a fulfillment is created for an order. +![Directory structure after adding the quote-cart link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741077395/Medusa%20Resources/quote-7_xrvodi.jpg) -#### Payload +```ts title="src/links/quote-cart.ts" highlights={quoteCartHighlights} +import { defineLink } from "@medusajs/framework/utils" +import QuoteModule from "../modules/quote" +import CartModule from "@medusajs/medusa/cart" -```ts -{ - order_id, // The ID of the order - fulfillment_id, // The ID of the fulfillment - no_notification, // (boolean) Whether to notify the customer -} +export default defineLink( + { + linkable: QuoteModule.linkable.quote.id, + field: "cart_id", + }, + CartModule.linkable.cart, + { + readOnly: true, + } +) ``` -#### Workflows Emitting this Event +You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Quote` data model from the `QuoteModule` module, specifying that its `cart_id` property holds the ID of the linked record. +2. An object indicating the second data model part of the link. You pass the link configurations for the `Cart` data model from the `CartModule` module. +3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`. -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +You'll now be able to retrieve the cart that a quote was created from, as you'll see in later steps. -*** +### Define Quote \<> Customer Link -### order.fulfillment\_canceled +Next, you'll define the link between the `Quote` and `Customer` data model of the Customer Module. So, create the file `src/links/quote-customer.ts` with the following content: -Emitted when an order's fulfillment is canceled. +![Directory structure after adding the quote-customer link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078047/Medusa%20Resources/quote-8_bbngmh.jpg) -#### Payload +```ts title="src/links/quote-customer.ts" +import { defineLink } from "@medusajs/framework/utils" +import QuoteModule from "../modules/quote" +import CustomerModule from "@medusajs/medusa/customer" -```ts -{ - order_id, // The ID of the order - fulfillment_id, // The ID of the fulfillment - no_notification, // (boolean) Whether to notify the customer -} +export default defineLink( + { + linkable: QuoteModule.linkable.quote.id, + field: "customer_id", + }, + CustomerModule.linkable.customer, + { + readOnly: true, + } +) ``` -#### Workflows Emitting this Event +You define the link between the `Quote` and `Customer` data models in the same way as the `Quote` and `Cart` link. In the first object parameter of `defineLink`, you pass the linkable configurations of the `Quote` data model, specifying the `customer_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `Customer` data model from the Customer Module. You also configure the link to be read-only. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +### Define Quote \<> OrderChange Link -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) +Next, you'll define the link between the `Quote` and `OrderChange` data model of the Order Module. So, create the file `src/links/quote-order-change.ts` with the following content: -*** +![Directory structure after adding the quote-order-change link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078511/Medusa%20Resources/quote-11_faac5m.jpg) -### order.return\_requested +```ts title="src/links/quote-order-change.ts" +import { defineLink } from "@medusajs/framework/utils" +import QuoteModule from "../modules/quote" +import OrderModule from "@medusajs/medusa/order" -Emitted when a return request is confirmed. +export default defineLink( + { + linkable: QuoteModule.linkable.quote.id, + field: "order_change_id", + }, + OrderModule.linkable.orderChange, + { + readOnly: true, + } +) +``` -#### Payload +You define the link between the `Quote` and `OrderChange` data models in the same way as the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `order_change_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `OrderChange` data model from the Order Module. You also configure the link to be read-only. -```ts -{ - order_id, // The ID of the order - return_id, // The ID of the return -} -``` +### Define Quote \<> Order Link -#### Workflows Emitting this Event +Finally, you'll define the link between the `Quote` and `Order` data model of the Order Module. So, create the file `src/links/quote-order.ts` with the following content: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +![Directory structure after adding the quote-order link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078607/Medusa%20Resources/quote-12_ixr2f7.jpg) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +```ts title="src/links/quote-order.ts" +import { defineLink } from "@medusajs/framework/utils" +import QuoteModule from "../modules/quote" +import OrderModule from "@medusajs/medusa/order" -*** +export default defineLink( + { + linkable: QuoteModule.linkable.quote.id, + field: "draft_order_id", + }, + { + linkable: OrderModule.linkable.order.id, + alias: "draft_order", + }, + { + readOnly: true, + } +) +``` -### order.return\_received +You define the link between the `Quote` and `Order` data models similar to the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `draft_order_id` property as the link field. -Emitted when a return is marked as received. +In the second object parameter, you pass the linkable configurations of the `Order` data model from the Order Module. You also set an `alias` property to `draft_order`. This allows you later to retrieve the draft order of a quote with the `draft_order` alias rather than the default `order` alias. Finally, you configure the link to be read-only. -#### Payload +You've finished creating the links that allow you to retrieve data related to quotes. You'll see how to use these links in later steps. -```ts -{ - order_id, // The ID of the order - return_id, // The ID of the return -} -``` +*** -#### Workflows Emitting this Event +## Step 4: Implement Create Quote Workflow -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You're now ready to start implementing quote-management features. The first one you'll implement is the ability for customers to request a quote for a set of items in their cart. -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +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. -*** +So, in this section, you'll learn how to create a workflow that creates a quote for a customer. -### order.claim\_created +Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -Emitted when a claim is created for an order. +The workflow will have the following steps: -#### Payload +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart that the customer wants a quote for. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the customer requesting the quote. +- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md): Create the draft order for the quote. +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md): Create the order change for the draft order. +- [createQuotesStep](#createQuotesStep): Create the quote for the customer. -```ts -{ - order_id, // The ID of the order - claim_id, // The ID of the claim -} -``` +The first four steps are provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createQuotesStep` step. -#### Workflows Emitting this Event +### createQuotesStep -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +In the last step of the workflow, you'll create a quote for the customer using the Quote Module's service. -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) +To create a step, create the file `src/workflows/steps/create-quotes.ts` with the following content: -*** +![Directory structure after adding the create-quotes step](https://res.cloudinary.com/dza7lstvk/image/upload/v1741085446/Medusa%20Resources/quote-13_tv9i23.jpg) -### order.exchange\_created +```ts title="src/workflows/steps/create-quotes.ts" highlights={createQuotesStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { QUOTE_MODULE } from "../../modules/quote" +import QueryModuleService from "../../modules/quote/service" -Emitted when an exchange is created for an order. +type StepInput = { + draft_order_id: string; + order_change_id: string; + cart_id: string; + customer_id: string; +}[] -#### Payload +export const createQuotesStep = createStep( + "create-quotes", + async (input: StepInput, { container }) => { + const quoteModuleService: QueryModuleService = container.resolve( + QUOTE_MODULE + ) -```ts -{ - order_id, // The ID of the order - exchange_id, // The ID of the exchange -} -``` + const quotes = await quoteModuleService.createQuotes(input) -#### Workflows Emitting this Event + return new StepResponse( + quotes, + quotes.map((quote) => quote.id) + ) + } +) +``` -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +1. The step's unique name, which is `create-quotes`. +2. An async function that receives two parameters: + - The step's input, which is in this case an array of quotes to create. + - 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 resolve the Quote Module's service from the Medusa container using the `resolve` method of the container, passing it the module's name as a parameter. -### order.transfer\_requested +Then, you create the quotes using the `createQuotes` method. As you remember, the Quote Module's service extends the `MedusaService` which generates data-management methods for you. -Emitted when an order is requested to be transferred to -another customer. +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: -#### Payload +1. The step's output, which is the quotes created. +2. Data to pass to the step's compensation function, which you'll add next. -```ts -{ - id, // The ID of the order - order_change_id, // The ID of the order change created for the transfer -} -``` +#### Add Compensation to Step -#### Workflows Emitting this Event +A step can have a compensation function that 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 following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +To add a compensation function to a step, pass it as a third-parameter to `createStep`: -- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) +```ts title="src/workflows/steps/create-quotes.ts" +export const createQuotesStep = createStep( + // ... + async (quoteIds, { container }) => { + if (!quoteIds) { + return + } + + const quoteModuleService: QueryModuleService = container.resolve( + QUOTE_MODULE + ) -*** + await quoteModuleService.deleteQuotes(quoteIds) + } +) +``` -## Payment Events +The compensation function accepts two parameters: -### Summary +1. The data passed from the step in the second parameter of `StepResponse`, which in this case is an array of quote IDs. +2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -|Event|Description| -|---|---| -|payment.captured|Emitted when a payment is captured.| -|payment.refunded|Emitted when a payment is refunded.| +In the compensation function, you resolve the Quote Module's service from the Medusa container and call the `deleteQuotes` method to delete the quotes created in the step. -### payment.captured +### createRequestForQuoteWorkflow -Emitted when a payment is captured. +You can now create the workflow using the steps provided by Medusa and your custom step. -#### Payload +To create the workflow, create the file `src/workflows/create-request-for-quote.ts` with the following content: -```ts -{ - id, // the ID of the payment -} -``` +```ts title="src/workflows/create-request-for-quote.ts" highlights={createRequestForQuoteHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports" +import { + beginOrderEditOrderWorkflow, + createOrderWorkflow, + CreateOrderWorkflowInput, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { OrderStatus } from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/workflows-sdk" +import { CreateOrderLineItemDTO } from "@medusajs/framework/types" +import { createQuotesStep } from "./steps/create-quotes" -#### Workflows Emitting this Event +type WorkflowInput = { + cart_id: string; + customer_id: string; +}; -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +export const createRequestForQuoteWorkflow = createWorkflow( + "create-request-for-quote", + (input: WorkflowInput) => { + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "id", + "sales_channel_id", + "currency_code", + "region_id", + "customer.id", + "customer.email", + "shipping_address.*", + "billing_address.*", + "items.*", + "shipping_methods.*", + "promotions.code", + ], + filters: { id: input.cart_id }, + options: { + throwIfKeyNotFound: true, + }, + }) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id", "customer"], + filters: { id: input.customer_id }, + options: { + throwIfKeyNotFound: true, + }, + }).config({ name: "customer-query" }) -*** + // TODO create order + } +) +``` -### payment.refunded +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. -Emitted when a payment is refunded. +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 having the ID of the customer requesting the quote, and the ID of their cart. -#### Payload +In the workflow's constructor function, you use `useQueryGraphStep` to retrieve the cart and customer details using the IDs passed as an input to the workflow. -```ts -{ - id, // the ID of the payment -} -``` +`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), whic allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array. -#### Workflows Emitting this Event +Next, you want to create the draft order for the quote. Replace the `TODO` in the workflow with the following: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +```ts title="src/workflows/create-request-for-quote.ts" +const orderInput = transform({ carts, customers }, ({ carts, customers }) => { + return { + is_draft_order: true, + status: OrderStatus.DRAFT, + sales_channel_id: carts[0].sales_channel_id || undefined, + email: customers[0].email || undefined, + customer_id: customers[0].id || undefined, + billing_address: carts[0].billing_address, + shipping_address: carts[0].shipping_address, + items: carts[0].items as CreateOrderLineItemDTO[] || [], + region_id: carts[0].region_id || undefined, + promo_codes: carts[0].promotions?.map((promo) => promo?.code), + currency_code: carts[0].currency_code, + shipping_methods: carts[0].shipping_methods || [], + } as CreateOrderWorkflowInput +}) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +const draftOrder = createOrderWorkflow.runAsStep({ + input: orderInput, +}) -*** +// TODO create order change +``` -## Product Category Events +You first prepare the order's details using `transform` from the Workflows SDK. Since Medusa creates an internal representation of the workflow's constructor before any data actually has a value, you can't manipulate data directly in the function. So, Medusa provides utilities like `transform` to manipulate data instead. You can learn more in the [transform variables](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation. -### Summary +Then, you create the draft order using the `createOrderWorkflow` workflow which you imported from `@medusajs/medusa/core-flows`. The workflow creates and returns the created order. -|Event|Description| -|---|---| -|product-category.created|Emitted when product categories are created.| -|product-category.updated|Emitted when product categories are updated.| -|product-category.deleted|Emitted when product categories are deleted.| +After that, you want to create an order change for the draft order. This will allow the admin later to make edits to the draft order, such as updating the prices or quantities of the items in the order. -### product-category.created +Replace the `TODO` with the following: -Emitted when product categories are created. +```ts title="src/workflows/create-request-for-quote.ts" +const orderEditInput = transform({ draftOrder }, ({ draftOrder }) => { + return { + order_id: draftOrder.id, + description: "", + internal_note: "", + metadata: {}, + } +}) -#### Payload +const changeOrder = beginOrderEditOrderWorkflow.runAsStep({ + input: orderEditInput, +}) -```ts -[{ - id, // The ID of the product category -}] +// TODO create quote ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. - -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) - -*** +You prepare the order change's details using `transform` and then create the order change using the `beginOrderEditOrderWorkflow` workflow which is provided by Medusa. -### product-category.updated +Finally, you want to create the quote for the customer and return it. Replace the last `TODO` with the following: -Emitted when product categories are updated. +```ts title="src/workflows/create-request-for-quote.ts" +const quoteData = transform({ + draftOrder, + carts, + customers, + changeOrder, +}, ({ draftOrder, carts, customers, changeOrder }) => { + return { + draft_order_id: draftOrder.id, + cart_id: carts[0].id, + customer_id: customers[0].id, + order_change_id: changeOrder.id, + } +}) -#### Payload +const quotes = createQuotesStep([ + quoteData, +]) -```ts -[{ - id, // The ID of the product category -}] +return new WorkflowResponse({ quote: quotes[0] }) ``` -#### Workflows Emitting this Event +Similar to before, you prepare the quote's details using `transform`. Then, you create the quote using the `createQuotesStep` you implemented earlier. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created quote in this case. -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) +In the next step, you'll learn how to execute the workflow when a customer requests a quote. *** -### product-category.deleted +## Step 5: Create Quote API Route -Emitted when product categories are deleted. +Now that you have the logic to create a quote for a customer, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). -#### Payload +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/quotes` that executes the workflow from the previous step. -```ts -[{ - id, // The ID of the product category -}] -``` +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). -#### Workflows Emitting this Event +### Implement API Route -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +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`. -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +By default, all routes starting with `/store/customers/me` require the customer to be authenticated. So, you'll be creating the API route at `/store/customers/me/quotes`. -*** +To create the API route, create the file `src/api/store/customers/me/quotes/route.ts` with the following content: -## Product Collection Events +![Directory structure after adding the store/quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741086995/Medusa%20Resources/quote-14_meo0yo.jpg) -### Summary +```ts title="src/api/store/customers/me/quotes/route.ts" highlights={createQuoteApiHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { + createRequestForQuoteWorkflow, +} from "../../../../../workflows/create-request-for-quote" -|Event|Description| -|---|---| -|product-collection.created|Emitted when product collections are created.| -|product-collection.updated|Emitted when product collections are updated.| -|product-collection.deleted|Emitted when product collections are deleted.| +type CreateQuoteType = { + cart_id: string; +} -### product-collection.created +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { + result: { quote: createdQuote }, + } = await createRequestForQuoteWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + customer_id: req.auth_context.actor_id, + }, + }) -Emitted when product collections are created. + const query = req.scope.resolve( + ContainerRegistrationKeys.QUERY + ) -#### Payload + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + fields: req.queryConfig.fields, + filters: { id: createdQuote.id }, + }, + { throwIfKeyNotFound: true } + ) -```ts -[{ - id, // The ID of the product collection -}] + return res.json({ quote }) +} ``` -#### Workflows Emitting this Event +Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/quotes`. The route handler function accepts two parameters: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +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. -- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +`AuthenticatedMedusaRequest` accepts the request body's type as a type argument. -*** +In the route handler function, you create the quote using the [createRequestForQuoteWorkflow](#createrequestforquoteworkflow) from the previous step. Then, you resolve Query from the Medusa container, which is available in the request object's `req.scope` property. -### product-collection.updated +You use Query to retrieve the Quote with its fields and linked records, which you'll learn how to specify soon. Finally, you send the quote as a response. -Emitted when product collections are updated. +### Add Validation Schema -#### Payload +The API route accepts the cart ID as a request body parameter. So, it's important to validate the body of a request before executing the route's handler. You can do this by specifying a validation schema in a middleware for the API route. -```ts -[{ - id, // The ID of the product collection -}] -``` +In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/validators.ts` with the following content: -#### Workflows Emitting this Event +![Directory structure after adding the validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089363/Medusa%20Resources/quote-15_iy6jem.jpg) -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +```ts title="src/api/store/validators.ts" +import { z } from "zod" -- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +export type CreateQuoteType = z.infer; +export const CreateQuote = z + .object({ + cart_id: z.string().min(1), + }) + .strict() +``` -*** +You define a `CreateQuote` schema using Zod that specifies the `cart_id` parameter as a required string. -### product-collection.deleted +You also export a type inferred from the schema. So, go back to `src/api/store/customers/me/quotes/route.ts` and replace the implementation of `CreateQuoteType` to import the type from the `validators.ts` file instead: -Emitted when product collections are deleted. +```ts title="src/api/store/customers/me/quotes/route.ts" +// other imports... +// add the following import +import { CreateQuoteType } from "../../../validators" -#### Payload +// remove CreateQuoteType definition -```ts -[{ - id, // The ID of the product collection -}] +export const POST = async ( + // keep type argument the same + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + // ... +} ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +### Apply Validation Schema Middleware -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +Now that you have the validation schema, you need to add the middleware that ensures the request body is validated before the route handler is executed. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler. -*** +Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). -## Product Option Events +Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content: -### Summary +![Directory structure after adding the store middlewares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089625/Medusa%20Resources/quote-16_oryolz.jpg) -|Event|Description| -|---|---| -|product-option.updated|Emitted when product options are updated.| -|product-option.created|Emitted when product options are created.| -|product-option.deleted|Emitted when product options are deleted.| +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { CreateQuote } from "./store/validators" -### product-option.updated +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + matcher: "/store/customers/me/quotes", + middlewares: [ + validateAndTransformBody(CreateQuote), + ], + }, + ], +}) +``` -Emitted when product options are updated. +To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties: -#### Payload +- `method`: The HTTP methods the middleware applies to, which is in this case `POST`. +- `matcher`: The path of the route the middleware applies to. +- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply the `validateAndTransformBody` middleware, which accepts a Zod schema as a parameter and validates that a request's body matches the schema. If not, it throws and returns an error. -```ts -[{ - id, // The ID of the product option -}] -``` +### Specify Quote Fields to Retrieve -#### Workflows Emitting this Event +In the route handler you just created, you specified what fields to retrieve in a quote using the `req.queryConfig.fields` property. The `req.queryConfig` field holds query configurations indicating the default fields to retrieve when using Query to return data in a request. This is useful to unify the returned data structure across different routes, or to allow clients to specify the fields they want to retrieve. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +To add the Query configurations, you'll first create a file that exports the default fields to retrieve for a quote, then apply them in a `validateAndTransformQuery` middleware. -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) +Learn more about configuring Query for requests in the [Request Query Configurations documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). -*** +Create the file `src/api/store/customers/me/quotes/query-config.ts` with the following content: -### product-option.created +![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741090067/Medusa%20Resources/quote-17_n6xsdb.jpg) -Emitted when product options are created. +```ts title="src/api/store/customers/me/quotes/query-config.ts" +export const quoteFields = [ + "id", + "status", + "*customer", + "cart.id", + "draft_order.id", + "draft_order.currency_code", + "draft_order.display_id", + "draft_order.region_id", + "draft_order.status", + "draft_order.version", + "draft_order.summary", + "draft_order.total", + "draft_order.subtotal", + "draft_order.tax_total", + "draft_order.order_change", + "draft_order.discount_total", + "draft_order.discount_tax_total", + "draft_order.original_total", + "draft_order.original_tax_total", + "draft_order.item_total", + "draft_order.item_subtotal", + "draft_order.item_tax_total", + "draft_order.original_item_total", + "draft_order.original_item_subtotal", + "draft_order.original_item_tax_total", + "draft_order.shipping_total", + "draft_order.shipping_subtotal", + "draft_order.shipping_tax_total", + "draft_order.original_shipping_tax_total", + "draft_order.original_shipping_subtotal", + "draft_order.original_shipping_total", + "draft_order.created_at", + "draft_order.updated_at", + "*draft_order.items", + "*draft_order.items.tax_lines", + "*draft_order.items.adjustments", + "*draft_order.items.variant", + "*draft_order.items.variant.product", + "*draft_order.items.detail", + "*draft_order.payment_collections", + "*order_change.actions", +] -#### Payload +export const retrieveStoreQuoteQueryConfig = { + defaults: quoteFields, + isList: false, +} -```ts -[{ - id, // The ID of the product option -}] +export const listStoreQuoteQueryConfig = { + defaults: quoteFields, + isList: true, +} ``` -#### Workflows Emitting this Event +You export two objects: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `retrieveStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a single quote. +- `listStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a list of quotes, which you'll use later. -- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) +Notice that in the fields retrieved, you specify linked records such as `customer` and `draft_order`. You can do this because you've defined links between the `Quote` data model and these data models previously. -*** +For simplicity, this guide will apply the `listStoreQuoteQueryConfig` to all routes starting with `/store/customers/me/quotes`. However, you should instead apply `retrieveStoreQuoteQueryConfig` to routes that retrieve a single quote, and `listStoreQuoteQueryConfig` to routes that retrieve a list of quotes. -### product-option.deleted +Next, you'll define a Zod schema that allows client applications to specify the fields they want to retrieve in a quote as a query parameter. In `src/api/store/validators.ts`, add the following schema: -Emitted when product options are deleted. +```ts title="src/api/store/validators.ts" +// other imports... +import { createFindParams } from "@medusajs/medusa/api/utils/validators" -#### Payload +// ... -```ts -[{ - id, // The ID of the product option -}] +export type GetQuoteParamsType = z.infer; +export const GetQuoteParams = createFindParams({ + limit: 15, + offset: 0, +}) ``` -#### Workflows Emitting this Event +You create a `GetQuoteParams` schema using the `createFindParams` utility from Medusa. This utility creates a schema that allows clients to specify query parameters such as: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `fields`: The fields to retrieve in a quote. +- `limit`: The maximum number of quotes to retrieve. This is useful for routes that return a list of quotes. +- `offset`: The number of quotes to skip before retrieving the next set of quotes. This is useful for routes that return a list of quotes. +- `order`: The fields to sort the quotes by either in ascending or descending order. This is useful for routes that return a list of quotes. -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +Finally, you'll apply these Query configurations in a middleware. So, add the following middleware in `src/api/middlewares.ts`: -*** +```ts title="src/api/store/middlewares.ts" +// other imports... +import { GetQuoteParams } from "./store/validators" +import { validateAndTransformQuery } from "@medusajs/framework/http" +import { listStoreQuoteQueryConfig } from "./store/customers/me/quotes/query-config" -## Product Tag Events +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/store/customers/me/quotes*", + middlewares: [ + validateAndTransformQuery( + GetQuoteParams, + listStoreQuoteQueryConfig + ), + ], + }, + ], +}) +``` -### Summary +You apply the `validateAndTransformQuery` middleware on all routes starting with `/store/customers/me/quotes`. The `validateAndTransformQuery` middleware that Medusa provides accepts two parameters: -|Event|Description| -|---|---| -|product-tag.updated|Emitted when product tags are updated.| -|product-tag.created|Emitted when product tags are created.| -|product-tag.deleted|Emitted when product tags are deleted.| +1. A Zod schema that specifies how to validate the query parameters of incoming requests. +2. A Query configuration object that specifies the default fields to retrieve in the response, which you defined in the `query-config.ts` file. -### product-tag.updated +The create quote route is now ready to be used by clients to create quotes for customers. -Emitted when product tags are updated. +### Test the API Route -#### Payload +To test out the API route, start the Medusa application: -```ts -[{ - id, // The ID of the product tag -}] +```bash npm2yarn +npm run dev ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. - -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. -*** +#### Retrieve Publishable API Key -### product-tag.created +All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront. -Emitted when product tags are created. +To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). -#### Payload +To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md). -```ts -[{ - id, // The ID of the product tag -}] -``` +#### Retrieve Customer Authentication Token -#### Workflows Emitting this Event +As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register): -- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) +```bash +curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "customer@gmail.com", + "password": "supersecret" +}' +``` -*** +Make sure to replace the email and password with the credentials you want. -### product-tag.deleted +Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers): -Emitted when product tags are deleted. +```bash +curl -X POST 'http://localhost:9000/store/customers' \ +-H 'Authorization: Bearer {token}' \ +-H 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +--data-raw '{ + "email": "customer@gmail.com" +}' +``` -#### Payload +Make sure to replace: -```ts -[{ - id, // The ID of the product tag -}] -``` +- `{token}` with the registration token you received from the previous request. +- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -#### Workflows Emitting this Event +Also, if you changed the email in the first request, make sure to change it here as well. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider): -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) +```bash +curl -X POST 'http://localhost:9000/auth/customer/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "customer@gmail.com", + "password": "supersecret" +}' +``` -*** +Copy the returned token to use it in the next requests. -## Product Type Events +#### Create Cart -### Summary +The customer needs a cart with an item before creating the quote. -|Event|Description| -|---|---| -|product-type.updated|Emitted when product types are updated.| -|product-type.created|Emitted when product types are created.| -|product-type.deleted|Emitted when product types are deleted.| +A cart requires a region ID. You can retrieve a region ID using the [List Regions API route](https://docs.medusajs.com/api/store#regions_getregions): -### product-type.updated +```bash +curl 'http://localhost:9000/store/regions' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' +``` -Emitted when product types are updated. +Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -#### Payload +Then, create a cart for the customer using the [Create Cart API route](https://docs.medusajs.com/api/store#carts_postcarts): -```ts -[{ - id, // The ID of the product type -}] +```bash +curl -X POST 'http://localhost:9000/store/carts' \ +-H 'Authorization: Bearer {token}' \ +-H 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +--data '{ + "region_id": "{region_id}" +}' ``` -#### Workflows Emitting this Event +Make sure to replace: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `{token}` with the authentication token you received from the previous request. +- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +- `{region_id}` with the region ID you retrieved from the previous request. -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) +This will create and return a cart. Copy its ID for the next request. -*** +You now need to add a product variant to the cart. You can retrieve a product variant ID using the [List Products API route](https://docs.medusajs.com/api/store#products_getproducts): -### product-type.created +```bash +curl 'http://localhost:9000/store/products' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' +``` -Emitted when product types are created. +Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -#### Payload +Copy the ID of a variant in a product from the response. -```ts -[{ - id, // The ID of the product type -}] +Finally, to add the product variant to the cart, use the [Add Item to Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems): + +```bash +curl -X POST 'http://localhost:9000/store/carts/{id}/line-items' \ +-H 'Authorization: Bearer {token}' \ +-H 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +--data-raw '{ + "variant_id": "{variant_id}", + "quantity": 1, +}' ``` -#### Workflows Emitting this Event +Make sure to replace: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `{id}` with the cart ID you retrieved previously. +- `{token}` with the authentication token you retrieved previously. +- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +- `{variant_id}` with the product variant ID you retrieved in the previous request. -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) +This adds the product variant to the cart. You can now use the cart to create a quote. -*** +For more accurate totals and processing of the quote's draft order, you should: -### product-type.deleted +- Add shipping and billing addresses by [updating the cart](https://docs.medusajs.com/api/store#carts_postcartsid). +- [Choose a shipping method](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods) for the cart. +- [Create a payment collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections) for the cart. +- [Initialize payment session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) in the payment collection. -Emitted when product types are deleted. +You can also learn how to build a checkout experience in a storefront by following [this storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md). It's not specific to quote management, so you'll need to change the last step to create a quote instead of an order. -#### Payload +#### Create Quote -```ts -[{ - id, // The ID of the product type -}] +To create a quote for the customer, send a request to the `/store/customers/me/quotes` route you created: + +```bash +curl -X POST 'http://localhost:9000/store/customers/me/quotes' \ +-H 'Authorization: Bearer {token}' \ +-H 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +--data-raw '{ + "cart_id": "{cart_id}" +}' ``` -#### Workflows Emitting this Event +Make sure to replace: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `{token}` with the authentication token you retrieved previously. +- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +- `{cart_id}` with the ID of the customer's cart. -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +This will create a quote for the customer and you'll receive its details in the response. *** -## Product Variant Events - -### Summary - -|Event|Description| -|---|---| -|product-variant.updated|Emitted when product variants are updated.| -|product-variant.created|Emitted when product variants are created.| -|product-variant.deleted|Emitted when product variants are deleted.| +## Step 6: List Quotes API Route -### product-variant.updated +After the customer creates a quote, the admin user needs to view these quotes to manage them. In this step, you'll create the API route to list quotes for the admin user. Then, in the next step, you'll customize the Medusa Admin dashboard to display these quotes. -Emitted when product variants are updated. +The process of creating this API route will be somewhat similar to the previous route you created. You'll create the route, define the query configurations, and apply them in a middleware. -#### Payload +### Implement API Route -```ts -[{ - id, // The ID of the product variant -}] -``` +To create the API route, create the file `src/api/admin/quotes/route.ts` with the following content: -#### Workflows Emitting this Event +![Directory structure after adding the admin quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741094735/Medusa%20Resources/quote-18_uvwqt6.jpg) -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +```ts title="src/api/admin/quotes/route.ts" highlights={listQuotesHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -*** + const { data: quotes, metadata } = await query.graph({ + entity: "quote", + ...req.queryConfig, + }) -### product-variant.created + res.json({ + quotes, + count: metadata!.count, + offset: metadata!.skip, + limit: metadata!.take, + }) +} +``` -Emitted when product variants are created. +You export a `GET` function in this file, which exposes a `GET` API route at `/admin/quotes`. -#### Payload +In the route handler function, you resolve Query from the Medusa container and use it to retrieve the list of quotes. Similar to before, you use `req.queryConfig` to specify the fields to retrieve in the response. -```ts -[{ - id, // The ID of the product variant -}] -``` +`req.queryConfig` also includes pagination parameters, such as `limit`, `offset`, and `count`, and they're returned in the `metadata` property of Query's result. You return the pagination details and the list of quotes in the response. -#### Workflows Emitting this Event +Learn more about paginating Query results in the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-pagination/index.html.md). -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +### Add Query Configurations -- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +Similar to before, you need to specify the default fields to retrieve in a quote and apply them in a middleware for this new route. -*** +Since this is an admin route, create the file `src/api/admin/quotes/query-config.ts` with the following content: -### product-variant.deleted +![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095492/Medusa%20Resources/quote-19_xca6aq.jpg) -Emitted when product variants are deleted. +```ts title="src/api/admin/quotes/query-config.ts" +export const quoteFields = [ + "id", + "status", + "created_at", + "updated_at", + "*customer", + "cart.id", + "draft_order.id", + "draft_order.currency_code", + "draft_order.display_id", + "draft_order.region_id", + "draft_order.status", + "draft_order.version", + "draft_order.summary", + "draft_order.total", + "draft_order.subtotal", + "draft_order.tax_total", + "draft_order.order_change", + "draft_order.discount_total", + "draft_order.discount_tax_total", + "draft_order.original_total", + "draft_order.original_tax_total", + "draft_order.item_total", + "draft_order.item_subtotal", + "draft_order.item_tax_total", + "draft_order.original_item_total", + "draft_order.original_item_subtotal", + "draft_order.original_item_tax_total", + "draft_order.shipping_total", + "draft_order.shipping_subtotal", + "draft_order.shipping_tax_total", + "draft_order.original_shipping_tax_total", + "draft_order.original_shipping_subtotal", + "draft_order.original_shipping_total", + "draft_order.created_at", + "draft_order.updated_at", + "*draft_order.items", + "*draft_order.items.tax_lines", + "*draft_order.items.adjustments", + "*draft_order.items.variant", + "*draft_order.items.variant.product", + "*draft_order.items.detail", + "*order_change.actions", +] -#### Payload +export const retrieveAdminQuoteQueryConfig = { + defaults: quoteFields, + isList: false, +} -```ts -[{ - id, // The ID of the product variant -}] +export const listAdminQuoteQueryConfig = { + defaults: quoteFields, + isList: true, +} ``` -#### Workflows Emitting this Event +You export two objects: `retrieveAdminQuoteQueryConfig` and `listAdminQuoteQueryConfig`, which specify the default fields to retrieve for a single quote and a list of quotes, respectively. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +For simplicity, this guide will apply the `listAdminQuoteQueryConfig` to all routes starting with `/admin/quotes`. However, you should instead apply `retrieveAdminQuoteQueryConfig` to routes that retrieve a single quote, and `listAdminQuoteQueryConfig` to routes that retrieve a list of quotes. -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +Next, you'll define a Zod schema that allows client applications to specify the fields to retrieve and pagination fields as a query parameter. Create the file `src/api/admin/validators.ts` with the following content: -*** +![Directory structure after adding the admin validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095771/Medusa%20Resources/quote-20_iygrip.jpg) -## Product Events +```ts title="src/api/admin/validators.ts" +import { + createFindParams, +} from "@medusajs/medusa/api/utils/validators" -### Summary +export const AdminGetQuoteParams = createFindParams({ + limit: 15, + offset: 0, +}) + .strict() +``` -|Event|Description| -|---|---| -|product.updated|Emitted when products are updated.| -|product.created|Emitted when products are created.| -|product.deleted|Emitted when products are deleted.| +You define the `AdminGetQuoteParams` schema using the `createFindParams` utility from Medusa. The schema allows clients to specify query parameters such as: -### product.updated +- `fields`: The fields to retrieve in a quote. +- `limit`: The maximum number of quotes to retrieve. +- `offset`: The number of quotes to skip before retrieving the next set of quotes. +- `order`: The fields to sort the quotes by either in ascending or descending order. -Emitted when products are updated. +Finally, you need to apply the `validateAndTransformQuery` middleware on this route. So, add the following to `src/api/middlewares.ts`: -#### Payload +```ts title="src/api/middlewares.ts" +// other imports... +import { AdminGetQuoteParams } from "./admin/quotes/validators" +import { listAdminQuoteQueryConfig } from "./admin/quotes/query-config" -```ts -[{ - id, // The ID of the product -}] +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/quotes*", + middlewares: [ + validateAndTransformQuery( + AdminGetQuoteParams, + listAdminQuoteQueryConfig + ), + ], + }, + ], +}) ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You add the `validateAndTransformQuery` middleware to all routes starting with `/admin/quotes`. It validates the query parameters and sets the Query configurations based on the defaults you defined and the passed query parameters. -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +Your API route is now ready for use. You'll test it in the next step by customizing the Medusa Admin dashboard to display the quotes. *** -### product.created - -Emitted when products are created. - -#### Payload +## Step 7: List Quotes Route in Medusa Admin -```ts -[{ - id, // The ID of the product -}] -``` +Now that you have the API route to retrieve the list of quotes, you want to show these quotes to the admin user in the Medusa Admin dashboard. The Medusa Admin is customizable, allowing you to add new pages as UI routes. -#### Workflows Emitting this Event +A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of quotes in the Medusa Admin. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +### Configure JS SDK -*** +Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations. -### product.deleted +The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content: -Emitted when products are deleted. +![Directory structure after adding the sdk file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098137/Medusa%20Resources/quote-23_plm90s.jpg) -#### Payload +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" -```ts -[{ - id, // The ID of the product -}] +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You create an instance of the JS SDK using the `Medusa` class from the `@medusajs/js-sdk` package. You pass it an object having the following properties: -- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +- `baseUrl`: The base URL of the Medusa server. +- `debug`: A boolean indicating whether to log debug information. +- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type. -*** +### Add Admin Types -## Region Events +In your development, you'll need types that represents the data you'll retrieve from the Medusa server. So, create the file `src/admin/types.ts` with the following content: -### Summary +![Directory structure after adding the admin type](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098478/Medusa%20Resources/quote-25_jr79pa.jpg) -|Event|Description| -|---|---| -|region.updated|Emitted when regions are updated.| -|region.created|Emitted when regions are created.| -|region.deleted|Emitted when regions are deleted.| +```ts title="src/admin/types.ts" +import { + AdminCustomer, + AdminOrder, + AdminUser, + FindParams, + PaginatedResponse, + StoreCart, +} from "@medusajs/framework/types" -### region.updated +export type AdminQuote = { + id: string; + status: string; + draft_order_id: string; + order_change_id: string; + cart_id: string; + customer_id: string; + created_at: string; + updated_at: string; + draft_order: AdminOrder; + cart: StoreCart; + customer: AdminCustomer +}; -Emitted when regions are updated. +export interface QuoteQueryParams extends FindParams {} -#### Payload +export type AdminQuotesResponse = PaginatedResponse<{ + quotes: AdminQuote[]; +}> -```ts -[{ - id, // The ID of the region -}] +export type AdminQuoteResponse = { + quote: AdminQuote; +}; ``` -#### Workflows Emitting this Event +You define the following types: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `AdminQuote`: Represents a quote. +- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving qoutes. +- `AdminQuotesResponse`: Represents the response when retrieving a list of quotes. +- `AdminQuoteResponse`: Represents the response when retrieving a single quote, which you'll implement later in this guide. -- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) +You'll use these types in the rest of the customizations. -*** +### Create useQuotes Hook -### region.created +When sending requests to the Medusa server from your admin customizations, it's recommended to use [Tanstack Query](https://tanstack.com/query/latest), allowing you to benefit from its caching and data fetching capabilities. -Emitted when regions are created. +So, you'll create a `useQuotes` hook that uses Tanstack Query and the JS SDK to fetch the list of quotes from the Medusa server. -#### Payload +Create the file `src/admin/hooks/quotes.tsx` with the following content: -```ts -[{ - id, // The ID of the region -}] -``` +![Directory structure after adding the hooks quotes file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098244/Medusa%20Resources/quote-24_apdpem.jpg) -#### Workflows Emitting this Event +```ts title="src/admin/hooks/quotes.tsx" +import { ClientHeaders, FetchError } from "@medusajs/js-sdk" +import { + QuoteQueryParams, + AdminQuotesResponse, +} from "../types" +import { + QueryKey, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" +import { sdk } from "../lib/sdk" -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +export const useQuotes = ( + query: QuoteQueryParams, + options?: UseQueryOptions< + AdminQuotesResponse, + FetchError, + AdminQuotesResponse, + QueryKey + > +) => { + const fetchQuotes = (query: QuoteQueryParams, headers?: ClientHeaders) => + sdk.client.fetch(`/admin/quotes`, { + query, + headers, + }) -- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) + const { data, ...rest } = useQuery({ + ...options, + queryFn: () => fetchQuotes(query)!, + queryKey: ["quote", "list"], + }) -*** + return { ...data, ...rest } +} +``` -### region.deleted +You define a `useQuotes` hook that accepts query parameters and optional options as a parameter. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes` route. -Emitted when regions are deleted. +You return the fetched data from the Medusa server. You'll use this hook in the UI route. -#### Payload +### Create Quotes UI Route -```ts -[{ - id, // The ID of the region -}] -``` +You can now create the UI route that will show a new page in the Medusa Admin with the list of quotes. -#### Workflows Emitting this Event +UI routes are created in a `page.tsx` file under the `src/admin/routes` directory. The path of the UI route is the file's path relative to `src/admin/routes`. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +So, to add the UI route at `/quotes` in the Medusa Admin, create the file `src/admin/routes/quotes/page.tsx` with the following content: -- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +![Directory structure after adding the Quotes UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099122/Medusa%20Resources/quote-26_qrqzut.jpg) -*** +```tsx title="src/admin/routes/quotes/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { DocumentText } from "@medusajs/icons" +import { + Container, createDataTableColumnHelper, DataTable, + DataTablePaginationState, Heading, Toaster, useDataTable, +} from "@medusajs/ui" +import { useNavigate } from "react-router-dom" +import { useQuotes } from "../../hooks/quotes" +import { AdminQuote } from "../../types" +import { useState } from "react" -## Sales Channel Events +const Quotes = () => { + // TODO implement page content +} -### Summary +export const config = defineRouteConfig({ + label: "Quotes", + icon: DocumentText, +}) -|Event|Description| -|---|---| -|sales-channel.created|Emitted when sales channels are created.| -|sales-channel.updated|Emitted when sales channels are updated.| -|sales-channel.deleted|Emitted when sales channels are deleted.| +export default Quotes +``` -### sales-channel.created +The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuation object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar. -Emitted when sales channels are created. +In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This componet requires you first define the columns of the table. -#### Payload +To define the table's columns, add in the same file and before the `Quotes` component the following: -```ts -[{ - id, // The ID of the sales channel -}] +```tsx title="src/admin/routes/quotes/page.tsx" +const StatusTitles: Record = { + accepted: "Accepted", + customer_rejected: "Customer Rejected", + merchant_rejected: "Merchant Rejected", + pending_merchant: "Pending Merchant", + pending_customer: "Pending Customer", +} + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("draft_order.display_id", { + header: "ID", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => StatusTitles[getValue()], + }), + columnHelper.accessor("customer.email", { + header: "Email", + }), + columnHelper.accessor("draft_order.customer.first_name", { + header: "First Name", + }), + columnHelper.accessor("draft_order.customer.company_name", { + header: "Company Name", + }), + columnHelper.accessor("draft_order.total", { + header: "Total", + cell: ({ getValue, row }) => + `${row.original.draft_order.currency_code.toUpperCase()} ${getValue()}`, + }), + columnHelper.accessor("created_at", { + header: "Created At", + cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(), + }), +] ``` -#### Workflows Emitting this Event +You use the `createDataTableColumnHelper` utility to create a function that allows you to define the columns of the table. Then, you create a `columns` array variable that defines the following columns: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +1. `ID`: The display ID of the quote's draft order. +2. `Status`: The status of the quote. Here, you use an object to map the status to a human-readable title. + - The `cell` property of the second object passed to the `columnHelper.accessor` function allows you to customize how the cell is rendered. +3. `Email`: The email of the customer. +4. `First Name`: The first name of the customer. +5. `Company Name`: The company name of the customer. +6. `Total`: The total amount of the quote's draft order. You format it to include the currency code. +7. `Created At`: The date the quote was created. -- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +Next, you'll use these columns to render the `DataTable` component in the `Quotes` component. -*** +Change the implementation of `Quotes` to the following: -### sales-channel.updated +```tsx title="src/admin/routes/quotes/page.tsx" +const Quotes = () => { + const navigate = useNavigate() + const [pagination, setPagination] = useState({ + pageSize: 15, + pageIndex: 0, + }) -Emitted when sales channels are updated. + const { + quotes = [], + count, + isPending, + } = useQuotes({ + limit: pagination.pageSize, + offset: pagination.pageIndex * pagination.pageSize, + fields: + "+draft_order.total,*draft_order.customer", + order: "-created_at", + }) -#### Payload + const table = useDataTable({ + columns, + data: quotes, + getRowId: (quote) => quote.id, + rowCount: count, + isLoading: isPending, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + onRowClick(event, row) { + navigate(`/quotes/${row.id}`) + }, + }) -```ts -[{ - id, // The ID of the sales channel -}] + + return ( + <> + + + Quotes + + + + + Products + + + + + + + + ) +} ``` -#### Workflows Emitting this Event +In the component, you use the `useQuotes` hook to fetch the quotes from the Medusa server. You pass the following query parameters in the request: -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +- `limit` and `offset`: Pagination fields to specify the current page and the number of quotes to retrieve. These are based on the `pagination` state variable, which will be managed by the `DataTable` component. +- `fields`: The fields to retrieve in the response. You specify the total amount of the draft order and the customer of the draft order. Since you prefix the fields with `+` and `*`, the fields are retrieved along with the default fields specified in the Query configurations. +- `order`: The order in which to retrieve the quotes. Here, you retrieve the quotes in descending order of their creation date. -- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) +Next, you use the `useDataTable` hook to create a table instance with the columns you defined. You pass the fetched quotes to the `DataTable` component, along with configurations related to pagination and loading. -*** +Notice that as part of the `useDataTable` configurations you naviagte to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step. -### sales-channel.deleted +Finally, you render the `DataTable` component to display the quotes in a table. -Emitted when sales channels are deleted. +### Test List Quotes UI Route -#### Payload +You can now test out the UI route and the route added in the previous section from the Medusa Admin. -```ts -[{ - id, // The ID of the sales channel -}] +First, start the Medusa application: + +```bash npm2yarn +npm run dev ``` -#### Workflows Emitting this Event +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You'll find a "Quotes" sidebar item. If you click on it, it will show you the table of quotes. -- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +![Medusa Admin dashboard displaying the Quotes management interface with a data table showing quote entries including columns for quote ID, customer information, status, and creation date](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099952/Medusa%20Resources/Screenshot_2025-03-04_at_4.52.17_PM_nqxyfq.png) *** -## User Events +## Step 8: Retrieve Quote API Route -### Summary +Next, you'll add an admin API route to retrieve a single quote. You'll use this route in the next step to add a UI route to view a quote's details. You'll later expand on that UI route to allow the admin to manage the quote. -|Event|Description| -|---|---| -|user.created|Emitted when users are created.| -|user.updated|Emitted when users are updated.| -|user.deleted|Emitted when users are deleted.| +To add the API route, create the file `src/api/admin/quotes/[id]/route.ts` with the following content: -### user.created +![Directory structure after adding the single quote route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741100686/Medusa%20Resources/quote-27_ugvhbb.jpg) -Emitted when users are created. +```ts title="src/api/admin/quotes/[id]/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -#### Payload +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { id } = req.params -```ts -[{ - id, // The ID of the user -}] + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) + + res.json({ quote }) +} ``` -#### Workflows Emitting this Event +You export a `GET` route handler, which will create a `GET` API route at `/admin/quotes/:id`. -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +In the route handler, you resolve Query and use it to retrieve the quote. You pass the ID in the path parameter as a filter in Query. You also pass the query configuration fields, which are the same as the ones you've configured before, to retrieve the default fields and the fields specified in the query parameter. -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well. + +You'll test this route in the next step as you create the UI route for a single quote. *** -### user.updated +## Step 9: Quote Details UI Route -Emitted when users are updated. +In the Quotes List UI route, you configured the data table to navigate to a quote's page when you click on it in the table. Now that you have the API route to retrieve a single quote, you'll create the UI route that shows a quote's details. -#### Payload +![Preview of the quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png) -```ts -[{ - id, // The ID of the user -}] -``` +Before you create the UI route, you need to create the hooks necessary to retrieve data from the Medusa server, and some components that will show the different elements of the page. -#### Workflows Emitting this Event +### Add Hooks -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +The first hook you'll add is a hook that will retrieve a quote using the API route you added in the previous step. -- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) +In `src/admin/hooks/quote.tsx`, add the following: -*** +```tsx title="src/admin/hooks/quote.tsx" +// other imports... +import { AdminQuoteResponse } from "../types" -### user.deleted +// ... -Emitted when users are deleted. +export const useQuote = ( + id: string, + query?: QuoteQueryParams, + options?: UseQueryOptions< + AdminQuoteResponse, + FetchError, + AdminQuoteResponse, + QueryKey + > +) => { + const fetchQuote = ( + id: string, + query?: QuoteQueryParams, + headers?: ClientHeaders + ) => + sdk.client.fetch(`/admin/quotes/${id}`, { + query, + headers, + }) -#### Payload + const { data, ...rest } = useQuery({ + queryFn: () => fetchQuote(id, query), + queryKey: ["quote", id], + ...options, + }) -```ts -[{ - id, // The ID of the user -}] + return { ...data, ...rest } +} ``` -#### Workflows Emitting this Event - -The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references. +You define a `useQuote` hook that accepts the quote's ID and optional query parameters and options as parameters. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes/:id` route. -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) -- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) +The hook returns the fetched data from the Medusa server. You'll use this hook later in the UI route. +In addition, you'll need a hook to retrieve a preview of the quote's draft order. An order preview includes changes or edits to be applied on an order's items, such as changes in prices and quantities. Medusa already provides a [Get Order Preview API route](https://docs.medusajs.com/api/admin#orders_getordersidpreview) that you can use to retrieve the preview. -# build Command - Medusa CLI Reference +To create the hook, create the file `src/admin/hooks/order-preview.tsx` with the following content: -Create a standalone build of the Medusa application. +![Directory structure after adding the order preview hook file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157692/Medusa%20Resources/quote-32_tb1tqw.jpg) -This creates a build that: +```tsx title="src/admin/hooks/order-preview.tsx" +import { HttpTypes } from "@medusajs/framework/types" +import { FetchError } from "@medusajs/js-sdk" +import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. +export const orderPreviewQueryKey = "custom_orders" -The build is outputted to a new `.medusa/server` directory. +export const useOrderPreview = ( + id: string, + query?: HttpTypes.AdminOrderFilters, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminOrderPreviewResponse, + FetchError, + HttpTypes.AdminOrderPreviewResponse, + QueryKey + >, + "queryFn" | "queryKey" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: async () => sdk.admin.order.retrievePreview(id, query), + queryKey: [orderPreviewQueryKey, id], + ...options, + }) -```bash -npx medusa build + return { ...data, ...rest } +} ``` -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 | - -*** +You add a `useOrderPreview` hook that accepts as parameters the order's ID, query parameters, and options. In the hook, you use the JS SDK's `admin.order.retrievePreview` method to retrieve the order preview and return it. -## Run Built Medusa Application +You'll use this hook later in the quote's details page. -After running the `build` command, use the following step to run the built Medusa application: +### Add formatAmount Utility -- Change to the `.medusa/server` directory and install the dependencies: +In the quote's details page, you'll display the amounts of the items in the quote. To format the amounts, you'll create a utility function that formats the amount based on the currency code. -```bash npm2yarn -cd .medusa/server && npm install -``` +Create the file `src/admin/utils/format-amount.ts` with the following content: -- 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. +![Directory structure after adding the format amount utility file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157986/Medusa%20Resources/quote-33_k5sa9q.jpg) -```bash npm2yarn -cp .env .medusa/server/.env.production +```ts title="src/admin/utils/format-amount.ts" +export const formatAmount = (amount: number, currency_code: string) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency_code, + }).format(amount) +} ``` -- In the system environment variables, set `NODE_ENV` to `production`: +You define a `formatAmount` function that accepts an amount and a currency code as parameters. The function uses the `Intl.NumberFormat` API to format the amount as a currency based on the currency code. -```bash -NODE_ENV=production -``` +You'll use this function in the UI route and its components. -- Use the `start` command to run the application: +### Create Amount Component -```bash npm2yarn -cd .medusa/server && npm run start -``` +In the quote's details page, you want to display changes in amounts for items and totals. This is useful as you later add the capability to edit the price and quantity of items. -*** +![Diagram showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183186/Medusa%20Resources/amount-highlighted_havznm.png) -## Build Medusa Admin +To display changes in an amount, you'll create an `Amount` component and re-use it where necessary. So, create the file `src/admin/components/amount.tsx` with the following content: -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. +![Directory structure after adding the amount component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741101819/Medusa%20Resources/quote-28_iwukg2.jpg) -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. +```tsx title="src/admin/components/amount.tsx" +import { clx } from "@medusajs/ui" +import { formatAmount } from "../utils/format-amount" +type AmountProps = { + currencyCode: string; + amount?: number | null; + originalAmount?: number | null; + align?: "left" | "right"; + className?: string; +}; -# db Commands - Medusa CLI Reference +export const Amount = ({ + currencyCode, + amount, + originalAmount, + align = "left", + className, +}: AmountProps) => { + if (typeof amount === "undefined" || amount === null) { + return ( +
+ - +
+ ) + } -Commands starting with `db:` perform actions on the database. + const formatted = formatAmount(amount, currencyCode) + const originalAmountPresent = typeof originalAmount === "number" + const originalAmountDiffers = originalAmount !== amount + const shouldShowAmountDiff = originalAmountPresent && originalAmountDiffers -## db:setup + return ( +
+ {shouldShowAmountDiff ? ( + <> + + {formatAmount(originalAmount!, currencyCode)} + + {formatted} + + ) : ( + <> + {formatted} + + )} +
+ ) +} +``` -Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. +In this component, you show the current amount of an item and, if it has been changed, you show previous amount as well. -It also updates your `.env` file with the database name. +You'll use this component in other components whenever you want to display any amount that can be changed. -```bash -npx medusa db:setup --db -``` +### Create QuoteItems Component -Use this command if you're setting up a Medusa project or database manually. +In the quote's UI route, you want to display the details of the items in the quote. You'll create a separate component that you'll use within the UI route. -### Options +![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183303/Medusa%20Resources/item-highlighted-cropped_ddyikt.png) -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \\`|The database name.|Yes|-| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +Create the file `src/admin/components/quote-items.tsx` with the following content: -*** +![Directory structure after adding the quote items component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741102170/Medusa%20Resources/quote-29_r5ljph.jpg) -## db:create +```tsx title="src/admin/components/quote-items.tsx" +import { + AdminOrder, + AdminOrderLineItem, + AdminOrderPreview, +} from "@medusajs/framework/types" +import { Badge, Text } from "@medusajs/ui" +import { useMemo } from "react" +import { Amount } from "./amount-cell" -Creates a database for the Medusa application with the specified name, if it doesn't exit. +export const QuoteItem = ({ + item, + originalItem, + currencyCode, +}: { + item: AdminOrderPreview["items"][0]; + originalItem?: AdminOrderLineItem; + currencyCode: string; +}) => { -It also updates your `.env` file with the database name. + const isItemUpdated = useMemo( + () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"), + [item] + ) -```bash -npx medusa db:create --db -``` + return ( +
+
+
+ + {item.title} + -Use this command if you want to only create a database. + {item.variant_sku && ( +
+ {item.variant_sku} +
+ )} + + {item.variant?.options?.map((o) => o.value).join(" · ")} + +
+
-### Options +
+
+ +
-|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \\`|The database name.|Yes|-| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +
+
+ + {item.quantity}x + +
-*** +
-## db:generate + {isItemUpdated && ( + + Modified + + )} +
-Generate a migration file for the latest changes in one or more modules. +
+
-```bash -npx medusa db:generate + +
+
+ ) +} ``` -### Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| - -*** +You first define the component for one quote item. In the component, you show the item's title, variant SKU, and quantity. You also use the `Amount` component to show the item's current and previous amounts. -## db:migrate +Next, add to the same file the `QuoteItems` component: -Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. +```tsx title="src/admin/components/quote-items.tsx" +export const QuoteItems = ({ + order, + preview, +}: { + order: AdminOrder; + preview: AdminOrderPreview; +}) => { + const itemsMap = useMemo(() => { + return new Map(order.items.map((item) => [item.id, item])) + }, [order]) -```bash -npx medusa db:migrate + return ( +
+ {preview.items?.map((item) => { + return ( + + ) + })} +
+ ) +} ``` -Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. - -### Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from -|No|Data migration scripts are run by default starting from -| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| - -*** - -## db:rollback +In this component, you loop over the order's items and show each of them using the `QuoteItem` component. -Revert the last migrations ran on one or more modules. +### Create TotalsBreakdown Component -```bash -npx medusa db:rollback -``` +Another component you'll need in the quote's UI route is a component that breaks down the totals of the quote's draft order, such as its discount or shipping totals. -### Arguments +![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183481/Medusa%20Resources/totals-highlighted_hpxier.png) -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| +Create the file `src/admin/components/totals-breakdown.tsx` with the following content: -*** +![Directory structure after adding the totals breakdown component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741155757/Medusa%20Resources/quote-30_de0kjq.jpg) -## db:sync-links +```tsx title="src/admin/components/totals-breakdown.tsx" +import { AdminOrder } from "@medusajs/framework/types" +import { Text } from "@medusajs/ui" +import { ReactNode } from "react" +import { formatAmount } from "../utils/format-amount" -Sync the database with the link definitions in your application, including the definitions in Medusa's modules. +export const Total = ({ + label, + value, + secondaryValue, + tooltip, +}: { + label: string; + value: string | number; + secondaryValue: string; + tooltip?: ReactNode; +}) => ( +
+ + {label} {tooltip} + +
+ + {secondaryValue} + +
-```bash -npx medusa db:sync-links +
+ + {value} + +
+
+) ``` -### Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--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 +You first define the `Total` component, which breaksdown a total item, such as discount. You'll use this component to breakdown the different totals in the `TotalsBreakdown` component. -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. +Add the `TotalsBreakdown` component after the `Total` component: -```bash -npx medusa develop +```tsx title="src/admin/components/totals-breakdown.tsx" +export const TotalsBreakdown = ({ order }: { order: AdminOrder }) => { + return ( +
+ 0 + ? `- ${formatAmount(order.discount_total, order.currency_code)}` + : "-" + } + /> + {(order.shipping_methods || []) + .sort((m1, m2) => + (m1.created_at as string).localeCompare(m2.created_at as string) + ) + .map((sm, i) => { + return ( +
+ +
+ ) + })} +
+ ) +} ``` -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# exec Command - Medusa CLI Reference +In this component, you show the different totals of the quote's draft order, such as discounts and shipping totals. You use the `Total` component to show each total item. -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). +### Create Quote Details UI Route -```bash -npx medusa exec [file] [args...] -``` +You can now create the UI route that will show a quote's details in the Medusa Admin. -## Arguments +Create the file `src/admin/routes/quote/[id]/page.tsx` with the following content: -|Argument|Description|Required| -|---|---|---|---|---| -|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| -|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| +![Diagram showcasing the directory structure after adding the quote details UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157385/Medusa%20Resources/quote-31_grwlon.jpg) +```tsx title="src/admin/routes/quote/[id]/page.tsx" highlights={quotesDetailsHighlights} +import { CheckCircleSolid } from "@medusajs/icons" +import { + Button, + Container, + Heading, + Text, + Toaster, +} from "@medusajs/ui" +import { Link, useNavigate, useParams } from "react-router-dom" +import { useOrderPreview } from "../../../hooks/order-preview" +import { + useQuote, +} from "../../../hooks/quotes" +import { QuoteItems } from "../../../components/quote-items" +import { TotalsBreakdown } from "../../../components/totals-breakdown" +import { formatAmount } from "../../../utils/format-amount" -# new Command - Medusa CLI Reference +const QuoteDetails = () => { + const { id } = useParams() + const navigate = useNavigate() + const { quote, isLoading } = useQuote(id!, { + fields: + "*draft_order.customer", + }) -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. + const { order: preview, isLoading: isPreviewLoading } = useOrderPreview( + quote?.draft_order_id!, + {}, + { enabled: !!quote?.draft_order_id } + ) -```bash -medusa new [ []] -``` + if (isLoading || !quote) { + return <> + } -## Arguments + if (isPreviewLoading) { + return <> + } -|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\`| + if (!isPreviewLoading && !preview) { + throw "preview not found" + } -## Options + // TODO render content +} -|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.| +export default QuoteDetails +``` +The `QuoteDetails` component will render the content of the quote's details page. So far, you retrieve the quote and its preview using the hooks you created earlier. You also render empty components or an error message if the data is still loading or not found. -# plugin Commands - Medusa CLI Reference +To add the rendered content, replace the `TODO` with the following: -Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. +```tsx title="src/admin/routes/quote/[id]/page.tsx" +return ( +
+
+
+ {quote.status === "accepted" && ( + +
+ + + Quote accepted by customer. Order is ready for processing. + -These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +
+
+ )} -## plugin:publish + +
+ Quote Summary +
+ + +
+
+ + Original Total + + + {formatAmount(quote.draft_order.total, quote.draft_order.currency_code)} + +
+ +
+ + Quote Total + + + {formatAmount(preview!.summary.current_order_total, quote.draft_order.currency_code)} + +
+
-Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. + {/* TODO add actions later */} +
-```bash -npx medusa plugin:publish -``` +
-*** +
+ +
+ Customer +
-## plugin:add +
+ + Email + -Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. + e.stopPropagation()} + > + {quote.draft_order?.customer?.email} + +
+
+
+
-```bash -npx medusa plugin:add [names...] + +
+) ``` -### Arguments +You first check if the quote has been accepted by the customer, and show a banner to view the created order if so. -|Argument|Description|Required| -|---|---|---|---|---| -|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| +Next, you use the `QuoteItems` and `TotalsBreakdown` components that you created to show the quote's items and totals. You also show the original and current totals of the quote, where the original total is the total of the draft order before any changes are made to its items. -*** +Finally, you show the customer's email and a link to view their details. -## plugin:develop +### Test Quote Details UI Route -Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. +To test the quote details UI route, start the Medusa application: -```bash -npx medusa plugin:develop +```bash npm2yarn +npm run dev ``` -*** +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. -## plugin:db:generate +Next, click on Quotes in the sidebar, which will open the list of quotes UI route you created earlier. Click on one of the quotes to view its details page. -Generate migrations for all modules in a plugin. +On the quote's details page, you can see the quote's items, its totals, and the customer's details. In the next steps, you'll add management features to the page. -```bash -npx medusa plugin:db:generate -``` +![Quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png) *** -## plugin:build - -Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. - -```bash -npx medusa plugin:build -``` - - -# start Command - Medusa CLI Reference +## Step 10: Add Merchant Reject Quote Feature -Start the Medusa application in production. +After the merchant or admin views the quote, they can choose to either reject it, send the quote back to the customer to review it, or make changes to the quote's prices and quantities. -```bash -npx medusa start -``` +In this step, you'll implement the functionality to reject a quote from the quote's details page. This will include: -## Options +1. Implementing the workflow to reject a quote. +2. Adding the API route to reject a quote that uses the workflow. +3. Add a hook in admin customizations that sends a request to the reject quote API route. +4. Add a button to reject the quote in the quote's details page. -|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.| +### Implement Merchant Reject Quote Workflow +To reject a quote, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps: -# telemetry Command - Medusa CLI Reference +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. +- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. +- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`merchant\_rejected\`. -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. +As mentioned before, the `useQueryGraphStep` is provided by Medusa's `@medusajs/medusa/core-flows` package. So, you'll only implement the remaining steps. -```bash -npx medusa telemetry -``` +#### validateQuoteNotAccepted -#### Options +The second step of the merchant rejection workflow ensures that a quote isn't already accepted, as it can't be rejected afterwards. -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +To create the step, create the file `src/workflows/steps/validate-quote-not-accepted.ts` with the following content: +![Diagram showcasing the directory structure after adding the validate quote rejection step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159537/Medusa%20Resources/quote-34_mtcxwa.jpg) -# user Command - Medusa CLI Reference +```ts title="src/workflows/steps/validate-quote-not-accepted.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Quote, QuoteStatus } from "../../modules/quote/models/quote" -Create a new admin user. +type StepInput = { + quote: InferTypeOf +} -```bash -npx medusa user --email [--password ] +export const validateQuoteNotAccepted = createStep( + "validate-quote-not-accepted", + async function ({ quote }: StepInput) { + if (quote.status === QuoteStatus.ACCEPTED) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Quote is already accepted by customer` + ) + } + } +) ``` -## Options +You create a step that accepts a quote as an input and throws an error if the quote's status is `accepted`, as you can't reject a quote that has been accepted by the customer. -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \\`|The user's email.|Yes|-| -|\`-p \\`|The user's password.|No|-| -|\`-i \\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +#### updateQuoteStatusStep +In the last step of the workflow, you'll change the workflow's status to `merchant_rejected`. So, you'll create a step that can be used to update a quote's status. -# Medusa CLI Reference +Create the file `src/workflows/steps/update-quotes.ts` with the following content: -The Medusa CLI tool provides commands that facilitate your development. +![Diagram showcasing the directory structure after adding the update quotes step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159754/Medusa%20Resources/quote-35_moaulz.jpg) -### Prerequisites +```ts title="src/workflows/steps/update-quotes.ts" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { QUOTE_MODULE } from "../../modules/quote" +import { QuoteStatus } from "../../modules/quote/models/quote" +import QuoteModuleService from "../../modules/quote/service" -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +type StepInput = { + id: string; + status?: QuoteStatus; +}[] -## Usage +export const updateQuotesStep = createStep( + "update-quotes", + async (data: StepInput, { container }) => { + const quoteModuleService: QuoteModuleService = container.resolve( + QUOTE_MODULE + ) -In your Medusa application's directory, you can use the Medusa CLI tool using NPX. + const dataBeforeUpdate = await quoteModuleService.listQuotes( + { id: data.map((d) => d.id) } + ) -For example: + const updatedQuotes = await quoteModuleService.updateQuotes(data) -```bash -npx medusa --help + return new StepResponse(updatedQuotes, { + dataBeforeUpdate, + }) + }, + async (revertInput, { container }) => { + if (!revertInput) { + return + } + + const quoteModuleService: QuoteModuleService = container.resolve( + QUOTE_MODULE + ) + + await quoteModuleService.updateQuotes( + revertInput.dataBeforeUpdate + ) + } +) ``` -*** +This step accepts an array of quotes to update their status. In the step function, you resolve the Quote Module's service. Then, you retrieve the quotes' original data so that you can pass them to the compensation function. Finally, you update the quotes' data and return the updated quotes. +In the compensation function, you resolve the Quote Module's service and update the quotes with their original data. -# build Command - Medusa CLI Reference +#### Implement Workflow -Create a standalone build of the Medusa application. +You can now implement the merchant-rejection workflow. Create the file `src/workflows/merchant-reject-quote.ts` with the following content: -This creates a build that: +![Diagram showcasing the directory structure after adding the merchant reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159969/Medusa%20Resources/quote-36_l1ffxm.jpg) -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. +```ts title="src/workflows/merchant-reject-quote.ts" highlights={merchantRejectionWorkflowHighlights} +import { useQueryGraphStep } from "@medusajs/core-flows" +import { createWorkflow } from "@medusajs/workflows-sdk" +import { QuoteStatus } from "../modules/quote/models/quote" +import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" +import { updateQuotesStep } from "./steps/update-quotes" -The build is outputted to a new `.medusa/server` directory. +type WorkflowInput = { + quote_id: string; +} -```bash -npx medusa build -``` +export const merchantRejectQuoteWorkflow = createWorkflow( + "merchant-reject-quote-workflow", + (input: WorkflowInput) => { + const { data: quotes } = useQueryGraphStep({ + entity: "quote", + fields: ["id", "status"], + filters: { id: input.quote_id }, + options: { + throwIfKeyNotFound: true, + }, + }) -Refer to [this section](#run-built-medusa-application) for next steps. + validateQuoteNotAccepted({ + // @ts-ignore + quote: quotes[0], + }) -## Options + updateQuotesStep([ + { + id: input.quote_id, + status: QuoteStatus.MERCHANT_REJECTED, + }, + ]) + } +) +``` -|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 | +You create a workflow that accepts the ID of a quote to reject. In the workflow, you: -*** +1. Use the `useQueryGraphStep` to retrieve the quote's details. +2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted`. +3. Update the quote's status to `merchant_rejected` using the `updateQuotesStep`. -## Run Built Medusa Application +You'll use this workflow next in an API route that allows a merchant to reject a quote. -After running the `build` command, use the following step to run the built Medusa application: +### Add Admin Reject Quote API Route -- Change to the `.medusa/server` directory and install the dependencies: +You'll now add the API route that allows a merchant to reject a quote. The route will use the `merchantRejectQuoteWorkflow` you created in the previous step. -```bash npm2yarn -cd .medusa/server && npm install -``` +Create the file `src/api/admin/quotes/[id]/reject/route.ts` with the following content: -- 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. +![Diagram showcasing the directory structure after adding the reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741160251/Medusa%20Resources/quote-37_jwlfcw.jpg) -```bash npm2yarn -cp .env .medusa/server/.env.production -``` +```ts title="src/api/admin/quotes/[id]/reject/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { merchantRejectQuoteWorkflow } from "../../../../../workflows/merchant-reject-quote" -- In the system environment variables, set `NODE_ENV` to `production`: +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { id } = req.params -```bash -NODE_ENV=production -``` + await merchantRejectQuoteWorkflow(req.scope).run({ + input: { + quote_id: id, + }, + }) -- Use the `start` command to run the application: + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) -```bash npm2yarn -cd .medusa/server && npm run start + res.json({ quote }) +} ``` -*** +You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/reject`. In the route handler, you run the `merchantRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. -## Build Medusa Admin +Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`. -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. +### Add Reject Quote Hook -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. +Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the route to reject a quote. +In `src/admin/hooks/quotes.tsx` add the following new hook: -# db Commands - Medusa CLI Reference +```tsx title="src/admin/hooks/quotes.tsx" +// other imports... +import { + useMutation, + UseMutationOptions, +} from "@tanstack/react-query" -Commands starting with `db:` perform actions on the database. +// ... -## db:setup +export const useRejectQuote = ( + id: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient() -Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. + const rejectQuote = async (id: string) => + sdk.client.fetch(`/admin/quotes/${id}/reject`, { + method: "POST", + }) -It also updates your `.env` file with the database name. + return useMutation({ + mutationFn: () => rejectQuote(id), + onSuccess: (data: AdminQuoteResponse, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [orderPreviewQueryKey, id], + }) -```bash -npx medusa db:setup --db -``` + queryClient.invalidateQueries({ + queryKey: ["quote", id], + }) -Use this command if you're setting up a Medusa project or database manually. + queryClient.invalidateQueries({ + queryKey: ["quote", "list"], + }) -### Options + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} +``` -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \\`|The database name.|Yes|-| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +You add a `useRejectQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the reject quote API route. -*** +When the mutation is invoked, the hook sends a request to the API route to reject the quote, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data. -## db:create +### Add Reject Quote Button -Creates a database for the Medusa application with the specified name, if it doesn't exit. +Finally, you can add a button to the quote's details page that allows a merchant to reject the quote. -It also updates your `.env` file with the database name. +In `src/admin/routes/quote/[id]/page.tsx`, add the following imports: -```bash -npx medusa db:create --db +```tsx title="src/admin/routes/quote/[id]/page.tsx" +import { + toast, + usePrompt, +} from "@medusajs/ui" +import { useEffect, useState } from "react" +import { + useRejectQuote, +} from "../../../hooks/quotes" ``` -Use this command if you want to only create a database. +Then, in the `QuoteDetails` component, add the following after the `useOrderPreview` hook usage: -### Options +```tsx title="src/admin/routes/quote/[id]/page.tsx" +const prompt = usePrompt() +const { mutateAsync: rejectQuote, isPending: isRejectingQuote } = + useRejectQuote(id!) +const [showRejectQuote, setShowRejectQuote] = useState(false) -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \\`|The database name.|Yes|-| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +useEffect(() => { + if ( + ["customer_rejected", "merchant_rejected", "accepted"].includes( + quote?.status! + ) + ) { + setShowRejectQuote(false) + } else { + setShowRejectQuote(true) + } +}, [quote]) -*** +const handleRejectQuote = async () => { + const res = await prompt({ + title: "Reject quote?", + description: + "You are about to reject this customer's quote. Do you want to continue?", + confirmText: "Continue", + cancelText: "Cancel", + variant: "confirmation", + }) -## db:generate + if (res) { + await rejectQuote(void 0, { + onSuccess: () => + toast.success("Successfully rejected customer's quote"), + onError: (e) => toast.error(e.message), + }) + } +} +``` -Generate a migration file for the latest changes in one or more modules. +First, you initialize the following variables: -```bash -npx medusa db:generate -``` +1. `prompt`: A function that you'll use to show a confirmation pop-up when the merchant tries to reject the quote. The `usePrompt` hook is available from the Medusa UI package. +2. `rejectQuote` and `isRejectingQuote`: both are returned by the `useRejectQuote` hook. The `rejectQuote` function invokes the mutation, rejecting the quote; `isRejectingQuote` is a boolean that indicates if the mutation is in progress. +3. `showRejectQuote`: A boolean that indicates whether the "Reject Quote" button should be shown. The button is shown if the quote's status is not `customer_rejected`, `merchant_rejected`, or `accepted`. This state variable is changed based on the quote's status in the `useEffect` hook. -### Arguments +You also define a `handleRejectQuote` function that will be called when the merchant clicks the reject quote button. The function shows a confirmation pop-up using the `prompt` function. If the user confirms the action, the function calls the `rejectQuote` function to reject the quote. -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| +Finally, find the `TODO` in the `return` statement and replace it with the following: -*** +```tsx title="src/admin/routes/quote/[id]/page.tsx" +
+ {showRejectQuote && ( + + )} +
+``` -## db:migrate +In this code snippet, you show the reject quote button if the `showRejectQuote` state is `true`. When the button is clicked, you call the `handleRejectQuote` function to reject the quote. -Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. +### Test Reject Quote Feature -```bash -npx medusa db:migrate +To test the reject quote feature, start the Medusa application: + +```bash npm2yarn +npm run dev ``` -Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. -### Options +Next, open a quote's details page. You'll find a new "Reject Quote" button. If you click on it and confirm rejecting the quote, the quote will be rejected, and a success message will be shown. -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from -|No|Data migration scripts are run by default starting from -| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +![Quote details page with reject quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741161544/Medusa%20Resources/Screenshot_2025-03-05_at_9.58.41_AM_xzdv6k.png) *** -## db:rollback +## Step 11: Add Merchant Send Quote Feature -Revert the last migrations ran on one or more modules. +Another action that a merchant can take on a quote is to send the quote back to the customer for review. The customer can then reject or accept the quote, which would convert it to an order. -```bash -npx medusa db:rollback -``` +In this step, you'll implement the functionality to send a quote back to the customer for review. This will include: -### Arguments +1. Implementing the workflow to send a quote back to the customer. +2. Adding the API route to send a quote back to the customer that uses the workflow. +3. Add a hook in admin customizations that sends a request to the send quote API route. +4. Add a button to send the quote back to the customer in the quote's details page. -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| +### Implement Merchant Send Quote Workflow -*** +You'll implement the logic of sending the quote in a workflow. The workflow has the following steps: -## db:sync-links +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. +- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. +- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`pending\_customer\`. -Sync the database with the link definitions in your application, including the definitions in Medusa's modules. +All the steps are available for use, so you can implement the workflow directly. -```bash -npx medusa db:sync-links -``` +Create the file `src/workflows/merchant-send-quote.ts` with the following content: -### Options +![Directory structure after adding the merchant send quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162342/Medusa%20Resources/quote-38_n4ksr0.jpg) -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +```ts title="src/workflows/merchant-send-quote.ts" highlights={sendQuoteHighlights} +import { useQueryGraphStep } from "@medusajs/core-flows" +import { createWorkflow } from "@medusajs/workflows-sdk" +import { QuoteStatus } from "../modules/quote/models/quote" +import { updateQuotesStep } from "./steps/update-quotes" +import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" +type WorkflowInput = { + quote_id: string; +} -# develop Command - Medusa CLI Reference +export const merchantSendQuoteWorkflow = createWorkflow( + "merchant-send-quote-workflow", + (input: WorkflowInput) => { + const { data: quotes } = useQueryGraphStep({ + entity: "quote", + fields: ["id", "status"], + filters: { id: input.quote_id }, + options: { + throwIfKeyNotFound: true, + }, + }) -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + validateQuoteNotAccepted({ + // @ts-ignore + quote: quotes[0], + }) -```bash -npx medusa develop + updateQuotesStep([ + { + id: input.quote_id, + status: QuoteStatus.PENDING_CUSTOMER, + }, + ]) + } +) ``` -## Options +You create a workflow that accepts the ID of a quote to send back to the customer. In the workflow, you: -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| +1. Use the `useQueryGraphStep` to retrieve the quote's details. +2. Validate that the quote can be sent back to the customer using the `validateQuoteNotAccepted` step. +3. Update the quote's status to `pending_customer` using the `updateQuotesStep`. +You'll use this workflow next in an API route that allows a merchant to send a quote back to the customer. -# exec Command - Medusa CLI Reference +### Add Send Quote API Route -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). +You'll now add the API route that allows a merchant to send a quote back to the customer. The route will use the `merchantSendQuoteWorkflow` you created in the previous step. -```bash -npx medusa exec [file] [args...] -``` +Create the file `src/api/admin/quotes/[id]/send/route.ts` with the following content: -## Arguments +![Directory structure after adding the send quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162497/Medusa%20Resources/quote-39_us1jbh.jpg) -|Argument|Description|Required| -|---|---|---|---|---| -|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| -|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| +```ts title="src/api/admin/quotes/[id]/send/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { + merchantSendQuoteWorkflow, +} from "../../../../../workflows/merchant-send-quote" +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { id } = req.params -# new Command - Medusa CLI Reference + await merchantSendQuoteWorkflow(req.scope).run({ + input: { + quote_id: id, + }, + }) -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. + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) -```bash -medusa new [ []] + res.json({ quote }) +} ``` -## Arguments +You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/send`. In the route handler, you run the `merchantSendQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. -|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\`| +Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`. -## Options +### Add Send Quote Hook -|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.| +Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the quote send API route. + +In `src/admin/hooks/quotes.tsx` add the new hook: + +```tsx title="src/admin/hooks/quotes.tsx" +export const useSendQuote = ( + id: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient() + + const sendQuote = async (id: string) => + sdk.client.fetch(`/admin/quotes/${id}/send`, { + method: "POST", + }) + + return useMutation({ + mutationFn: () => sendQuote(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [orderPreviewQueryKey, id], + }) + queryClient.invalidateQueries({ + queryKey: ["quote", id], + }) -# plugin Commands - Medusa CLI Reference + queryClient.invalidateQueries({ + queryKey: ["quote", "list"], + }) -Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} +``` -These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +You add a `useSendQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the send quote API route. -## plugin:publish +When the mutation is invoked, the hook sends a request to the send quote API route, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data. -Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. +### Add Send Quote Button -```bash -npx medusa plugin:publish -``` +Finally, you can add a button to the quote's details page that allows a merchant to send the quote back to the customer for review. -*** +First, add the following import to the `src/admin/routes/quote/[id]/page.tsx` file: -## plugin:add +```tsx title="src/admin/routes/quote/[id]/page.tsx" +import { + useSendQuote, +} from "../../../hooks/quotes" +``` -Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. +Then, after the `useRejectQuote` hook usage, add the following: -```bash -npx medusa plugin:add [names...] +```tsx title="src/admin/routes/quote/[id]/page.tsx" +const { mutateAsync: sendQuote, isPending: isSendingQuote } = useSendQuote( + id! +) +const [showSendQuote, setShowSendQuote] = useState(false) ``` -### Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| +You initialize the following variables: -*** +1. `sendQuote` and `isSendingQuote`: Data returned by the `useSendQuote` hook. The `sendQuote` function invokes the mutation, sending the quote back to the customer; `isSendingQuote` is a boolean that indicates if the mutation is in progress. +2. `showSendQuote`: A boolean that indicates whether the "Send Quote" button should be shown. -## plugin:develop +Next, update the existing `useEffect` hook to change `showSendQuote` based on the quote's status: -Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. +```tsx title="src/admin/routes/quote/[id]/page.tsx" +useEffect(() => { + if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) { + setShowSendQuote(true) + } else { + setShowSendQuote(false) + } -```bash -npx medusa plugin:develop + if ( + ["customer_rejected", "merchant_rejected", "accepted"].includes( + quote?.status! + ) + ) { + setShowRejectQuote(false) + } else { + setShowRejectQuote(true) + } +}, [quote]) ``` -*** +The `useEffect` hook now updates both the `showSendQuote` and `showRejectQuote` states based on the quote's status. The "Send Quote" button is hidden if the quote's status is not `pending_merchant` or `customer_rejected`. -## plugin:db:generate +Then, after the `handleRejectQuote` function, add the following `handleSendQuote` function: -Generate migrations for all modules in a plugin. +```tsx title="src/admin/routes/quote/[id]/page.tsx" +const handleSendQuote = async () => { + const res = await prompt({ + title: "Send quote?", + description: + "You are about to send this quote to the customer. Do you want to continue?", + confirmText: "Continue", + cancelText: "Cancel", + variant: "confirmation", + }) -```bash -npx medusa plugin:db:generate + if (res) { + await sendQuote( + void 0, + { + onSuccess: () => toast.success("Successfully sent quote to customer"), + onError: (e) => toast.error(e.message), + } + ) + } +} ``` -*** - -## plugin:build +You define a `handleSendQuote` function that will be called when the merchant clicks the "Send Quote" button. The function shows a confirmation pop-up using the `prompt` hook. If the user confirms the action, the function calls the `sendQuote` function to send the quote back to the customer. -Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. +Finally, add the following after the reject quote button in the `return` statement: -```bash -npx medusa plugin:build +```tsx title="src/admin/routes/quote/[id]/page.tsx" +{showSendQuote && ( + +)} ``` +In this code snippet, you show the "Send Quote" button if the `showSendQuote` state is `true`. When the button is clicked, you call the `handleSendQuote` function to send the quote back to the customer. -# start Command - Medusa CLI Reference +### Test Send Quote Feature -Start the Medusa application in production. +To test the send quote feature, start the Medusa application: -```bash -npx medusa start +```bash npm2yarn +npm run dev ``` -## Options +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. -|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.| +Next, open a quote's details page. You'll find a new "Send Quote" button. If you click on it and confirm sending the quote, the quote will be sent back to the customer, and a success message will be shown. +You'll later add the feature to update the quote item's details before sending the quote back to the customer. -# telemetry Command - Medusa CLI Reference +![Quote details page with send quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162950/Medusa%20Resources/Screenshot_2025-03-05_at_10.22.11_AM_sjuipg.png) -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. +*** -```bash -npx medusa telemetry -``` +## Step 12: Add Customer Preview Order API Route -#### Options +When the merchant sends back the quote to the customer, you want to show the customer the details of the quote and the order that would be created if they accept the quote. This helps the customer decide whether to accept or reject the quote (which you'll implement next). -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +In this step, you'll add the API route that allows a customer to preview a quote's order. +To create the API route, create the file `src/api/store/customers/me/quotes/[id]/preview/route.ts` with the following content: -# user Command - Medusa CLI Reference +![Directory structure after adding the customer preview order API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741163145/Medusa%20Resources/quote-40_lmcgve.jpg) -Create a new admin user. +```ts title="src/api/store/customers/me/quotes/[id]/preview/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" -```bash -npx medusa user --email [--password ] -``` +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const query = req.scope.resolve( + ContainerRegistrationKeys.QUERY + ) -## Options + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \\`|The user's email.|Yes|-| -|\`-p \\`|The user's password.|No|-| -|\`-i \\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| + const orderModuleService = req.scope.resolve( + Modules.ORDER + ) + + const preview = await orderModuleService.previewOrderChange( + quote.draft_order_id + ) + res.status(200).json({ + quote: { + ...quote, + order_preview: preview, + }, + }) +} +``` -# Medusa CLI Reference +You create a `GET` route handler, which will expose a `GET` API route at `/store/customers/me/quotes/:id/preview`. In the route handler, you retrieve the quote's details using Query, then preview the order that would be created from the quote using the `previewOrderChange` method from the Order Module's service. Finally, you return the quote and its order preview in the response. -The Medusa CLI tool provides commands that facilitate your development. +Notice that you're using the `req.queryConfig.fields` object in the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. -### Prerequisites +### Test Customer Preview Order API Route -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +To test the customer preview order API route, start the Medusa application: -## Usage +```bash npm2yarn +npm run dev +``` -In your Medusa application's directory, you can use the Medusa CLI tool using NPX. +Then, grab the ID of a quote placed by a customer that you have their [authentication token](#retrieve-customer-authentication-token). You can find the quote ID in the URL when viewing the quote's details page in the Medusa Admin dashboard. -For example: +Finally, send the following request to get a preview of the customer's quote and order: ```bash -npx medusa --help +curl 'http://localhost:9000/store/customers/me/quotes/{quote_id}/preview' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +-H 'Authorization: Bearer {token}' ``` -*** - +Make sure to replace: -# Authentication in JS SDK +- `{quote_id}` with the ID of the quote you want to preview. +- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). +- `{token}` with the customer's authentication token. -In this guide, you'll learn about the default authentication setup when using the JS SDK, how to customize it, and how to send authenticated requests to Medusa's APIs. +You'll receive in the response the quote's details with the order preview. You can show the customer these details in the storefront. -## Default Authentication Settings in JS SDK +*** -The JS SDK facilitates authentication by storing and managing the necessary authorization headers or sessions for you. +## Step 13: Add Customer Reject Quote Feature -There are three types of authentication: +After the customer previews the quote and its order, they can choose to reject the quote. When the customer rejects the quote, the quote's status is changed to `customer_rejected`. The merchant will still be able to update the quote and send it back to the customer for review. -|Method|Description|When to use| -|---|---|---| -|JWT token (default)|When you log in a user, the JS SDK stores the JWT for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK clears the stored JWT.|| -|Cookie session|When you log in a user, the JS SDK stores the session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK destroys the session cookie using Medusa's API.|| -|Secret API Key|Only available for admin users. You pass the API key in the JS SDK configurations, and it's always passed in the headers of all requests to the Medusa API.|| +In this step, you'll implement the functionality to reject a quote from the customer's perspective. This will include: -*** +1. Implementing the workflow to reject a quote as a customer. +2. Adding the API route to allow customers to reject a quote using the workflow. -## JS SDK Authentication Configurations +### Implement Customer Reject Quote Workflow -The JS SDK provides a set of configurations to customize the authentication method and storage. You can set these configurations when initializing the SDK. +To reject a quote from the customer's perspective, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps: -For a full list of JS SDK configurations and their possible values, check out the [JS SDK Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#js-sdk-configurations/index.html.md) documentation. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. +- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. +- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`customer\_rejected\`. -### Authentication Type +All the steps are available for use, so you can implement the workflow directly. -By default, the JS SDK uses JWT token (`jwt`) authentication. You can change the authentication method or type by setting the `auth.type` configuration to `session`. +Create the file `src/workflows/customer-reject-quote.ts` with the following content: -For example: +![Directory structure after adding the customer reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164371/Medusa%20Resources/quote-41_fgpqhz.jpg) -```ts -import Medusa from "@medusajs/js-sdk" +```ts title="src/workflows/customer-reject-quote.ts" highlights={customerRejectQuoteHighlights} +import { useQueryGraphStep } from "@medusajs/core-flows" +import { createWorkflow } from "@medusajs/workflows-sdk" +import { QuoteStatus } from "../modules/quote/models/quote" +import { updateQuotesStep } from "./steps/update-quotes" +import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" -export const sdk = new Medusa({ - // ... - auth: { - type: "session", - }, -}) -``` +type WorkflowInput = { + quote_id: string; + customer_id: string; +} -To use a secret API key instead, pass it in the `apiKey` configuration instead: +export const customerRejectQuoteWorkflow = createWorkflow( + "customer-reject-quote-workflow", + (input: WorkflowInput) => { + const { data: quotes } = useQueryGraphStep({ + entity: "quote", + fields: ["id", "status"], + filters: { id: input.quote_id, customer_id: input.customer_id }, + options: { + throwIfKeyNotFound: true, + }, + }) -```ts -import Medusa from "@medusajs/js-sdk" + validateQuoteNotAccepted({ + // @ts-ignore + quote: quotes[0], + }) -export const sdk = new Medusa({ - // ... - apiKey: "your-api-key", -}) + updateQuotesStep([ + { + id: input.quote_id, + status: QuoteStatus.CUSTOMER_REJECTED, + }, + ]) + } +) ``` -The provided API key will be passed in the headers of all requests to the Medusa API. - -### Change JWT Authentication Storage - -By default, the JS SDK stores the JWT token in the `localStorage` under the `medusa_auth_token` key. +You create a workflow that accepts the IDs of the quote to reject and the customer rejecting it. In the workflow, you: -Some environments or use cases may require a different storage method or `localStorage` may not be available. For example, if you're building a mobile app with React Native, you might want to use `AsyncStorage` instead of `localStorage`. +1. Use the `useQueryGraphStep` to retrieve the quote's details. Notice that you pass the IDs of the quote and the customer as filters to ensure that the quote belongs to the customer. +2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted` step. +3. Update the quote's status to `customer_rejected` using the `updateQuotesStep`. -You can change the storage method by setting the `auth.jwtTokenStorageMethod` configuration to one of the following values: +You'll use this workflow next in an API route that allows a customer to reject a quote. -|Value|Description| -|---|---| -|\`local\`|Uses | -|\`session\`|Uses | -|\`memory\`|Uses a memory storage method. This means the token will be cleared when the user refreshes the page or closes the browser tab or window. This is also useful when using the JS SDK in a server-side environment.| -|\`custom\`|Uses a custom storage method. This means you can provide your own implementation of the storage method. For example, you can use | -|\`nostore\`|Does not store the JWT token. This means you have to manually set the authorization header for each request. This is useful when you want to use a different authentication method or when you're using the JS SDK in a server-side environment.| +### Add Customer Reject Quote API Route -#### Custom Authentication Storage in JS SDK +You'll now add the API route that allows a customer to reject a quote. The route will use the `customerRejectQuoteWorkflow` you created in the previous step. -To use a custom storage method, you need to set the `auth.jwtTokenStorageMethod` configuration to `custom` and provide your own implementation of the storage method in the `auth.storage` configuration. +Create the file `src/api/store/customers/me/quotes/[id]/reject/route.ts` with the following content: -The object or class passed to `auth.storage` configuration must have the following methods: +![Directory structure after adding the customer reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164538/Medusa%20Resources/quote-42_bryo2z.jpg) -- `setItem`: A function that accepts a key and value to store the JWT token. -- `getItem`: A function that accepts a key to retrieve the JWT token. -- `removeItem`: A function that accepts a key to remove the JWT token from storage. +```ts title="src/api/store/customers/me/quotes/[id]/reject/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { + customerRejectQuoteWorkflow, +} from "../../../../../../../workflows/customer-reject-quote" -For example, to use `AsyncStorage` in React Native: +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const query = req.scope.resolve( + ContainerRegistrationKeys.QUERY + ) -```ts -import AsyncStorage from "@react-native-async-storage/async-storage" -import Medusa from "@medusajs/js-sdk" + await customerRejectQuoteWorkflow(req.scope).run({ + input: { + quote_id: id, + customer_id: req.auth_context.actor_id, + }, + }) -let MEDUSA_BACKEND_URL = "http://localhost:9000" + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) -if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { - MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL + return res.json({ quote }) } - -export const sdk = new Medusa({ - baseUrl: MEDUSA_BACKEND_URL, - debug: process.env.NODE_ENV === "development", - publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, - auth: { - type: "jwt", - jwtTokenStorageMethod: "custom", - storge: AsyncStorage, - }, -}) ``` -In this example, you specify the `jwtTokenStorageMethod` as `custom` and set the `storage` configuration to `AsyncStorage`. This way, the JS SDK will use `AsyncStorage` to store and manage the JWT token instead of `localStorage`. +You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/reject`. In the route handler, you run the `customerRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. -### Change Cookie Session Credentials Options +Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. -By default, if you set the `auth.type` configuration in the JS SDK to `session`, the JS SDK will pass the `credentials: include` option in the underlying [fetch requests](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials). +### Test Customer Reject Quote Feature -However, some platforms or environments may not support passing this option. For example, if you're using the JS SDK in a server-side environment or a mobile app, you might want to set the `credentials` option to `same-origin` or `omit`. +To test the customer reject quote feature, start the Medusa application: -You can change the `credentials` option by setting the `auth.fetchCredentials` configuration to one of the following values: +```bash npm2yarn +npm run dev +``` -|Value|Description| -|---|---| -|\`include\`|Passes the | -|\`same-origin\`|Passes the | -|\`omit\`|Passes the | +Then, send a request to reject a quote for the authenticated customer: -For example: +```bash +curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/reject' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +-H 'Authorization: Bearer {token}' +``` -```ts -import Medusa from "@medusajs/js-sdk" +Make sure to replace: -export const sdk = new Medusa({ - // ... - auth: { - type: "session", - fetchCredentials: "same-origin", - }, -}) -``` +- `{quote_id}` with the ID of the quote you want to reject. +- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). +- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token). -In this example, you set the `fetchCredentials` configuration to `same-origin`, which means the JS SDK will include cookies and authorization headers in the requests to the Medusa API only if the request is made to the same origin as the current page. +After sending the request, the quote will be rejected, and the updated quote will be returned in the response. You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. *** -## Sending Authenticated Requests in JS SDK +## Step 14: Add Customer Accept Quote Feature -If you're using an API key for authentication, you don't need to log in the user. +The customer alternatively can choose to accept a quote after previewing it. When the customer accepts a quote, the quote's draft order should become an order whose payment can be processed and items fulfilled. No further changes can be made on the quote after it's accepted. -The JS SDK has an `auth.login` method that allows you to login admin users, customers, or any [actor type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) with any [auth provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). +In this step, you'll implement the functionality to allow a customer to accept a quote. This will include: -Not only does this method log in the user, but it also stores the JWT token or session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. +1. Implementing the workflow to accept a quote as a customer. +2. Adding the API route to allow customers to accept a quote using the workflow. -For example: +### Implement Customer Accept Quote Workflow -### Admin User +You'll implement the quote acceptance logic in a workflow. The workflow has the following steps: -```ts -sdk.auth.login("user", "emailpass", { - email, - password, -}) -.then((data) => { - if (typeof data === "object" && data.location){ - // authentication requires more actions - } - // user is authenticated -}) -.catch((error) => { - // authentication failed -}) -``` +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. +- [validateQuoteCanAcceptStep](#validateQuoteCanAcceptStep): Validate that the quote can be accepted. +- [updateQuotesStep](#updateQuotesStep): Update the quote's status to \`accepted\`. +- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md): Confirm the changes made on the draft order, such as changes to item quantities and prices. +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md): Update the draft order to change its status and convert it into an order. -### Customer +You only need to implement the `validateQuoteCanAcceptStep` step before implementing the workflow, as the other steps are already available for use. -```ts -sdk.auth.login("customer", "emailpass", { - email, - password, -}) -.then((data) => { - if (typeof data === "object" && data.location){ - // authentication requires more actions - } - // customer is authenticated -}) -.catch((error) => { - // authentication failed -}) -``` +#### validateQuoteCanAcceptStep -### Custom +In the `validateQuoteCanAcceptStep`, you'll validate whether the customer can accept the quote. The customer can only accept a quote if the quote's status is `pending_customer`, meaning the merchant sent the quote back to the customer for review. -```ts -sdk.auth.login("manager", "emailpass", { - email, - password, -}) -.then((data) => { - if (typeof data === "object" && data.location){ - // authentication requires more actions +Create the file `src/workflows/steps/validate-quote-can-accept.ts` with the following content: + +![Directory structure after adding the validate quote can accept step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741165829/Medusa%20Resources/quote-43_cxc3qi.jpg) + +```ts title="src/workflows/steps/validate-quote-can-accept.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Quote, QuoteStatus } from "../../modules/quote/models/quote" + +type StepInput = { + quote: InferTypeOf +} + +export const validateQuoteCanAcceptStep = createStep( + "validate-quote-can-accept", + async function ({ quote }: StepInput) { + if (quote.status !== QuoteStatus.PENDING_CUSTOMER) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot accept quote when quote status is ${quote.status}` + ) + } } - // manager is authenticated -}) -.catch((error) => { - // authentication failed -}) +) ``` -In this example, you call the `sdk.auth.login` method passing it the actor type (for example, `user`), the provider (`emailpass`), and the credentials. - -If the authentication is successful, there are two types of returned data: - -- An object with a `location` property: This means the authentication requires more actions, which happens when using third-party authentication providers, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md). In that case, you need to redirect the customer to the location to complete their authentication. - - Refer to the [Third-Party Login in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md) guide for an example implementation. -- A string: This means the authentication was successful, and the user is logged in. The JS SDK automatically stores the JWT token or session cookie for you and includes it in the headers of all requests to the Medusa API. All requests you send afterwards will be authenticated with the stored token or session cookie. +You create a step that accepts a quote as input. In the step function, you throw an error if the quote's status is not `pending_customer`. -If the authentication fails, the `catch` block will be executed, and you can handle the error accordingly. +#### Implement Workflow -You can learn more about this method in the [auth.login reference](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md). +You can now implement the workflow that accepts a quote for a customer. Create the file `src/workflows/customer-accept-quote.ts` with the following content: -### Manually Set JWT Token +![Directory structure after adding the customer accept quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166025/Medusa%20Resources/quote-44_c09ts9.jpg) -If you need to set the JWT token manually, you can use the `sdk.client.setToken` method. All subsequent requests will be authenticated with the provided token. +```ts title="src/workflows/customer-accept-quote.ts" highlights={customerAcceptQuoteHighlights} +import { + confirmOrderEditRequestWorkflow, + updateOrderWorkflow, + useQueryGraphStep, +} from "@medusajs/core-flows" +import { OrderStatus } from "@medusajs/framework/utils" +import { createWorkflow } from "@medusajs/workflows-sdk" +import { validateQuoteCanAcceptStep } from "./steps/validate-quote-can-accept" +import { QuoteStatus } from "../modules/quote/models/quote" +import { updateQuotesStep } from "./steps/update-quotes" -For example: +type WorkflowInput = { + quote_id: string; + customer_id: string; +}; -```ts -sdk.client.setToken("your-jwt-token") +export const customerAcceptQuoteWorkflow = createWorkflow( + "customer-accept-quote-workflow", + (input: WorkflowInput) => { + const { data: quotes } = useQueryGraphStep({ + entity: "quote", + fields: ["id", "draft_order_id", "status"], + filters: { id: input.quote_id, customer_id: input.customer_id }, + options: { + throwIfKeyNotFound: true, + }, + }) -// all requests sent after this will be authenticated with the provided token -``` + validateQuoteCanAcceptStep({ + // @ts-ignore + quote: quotes[0], + }) -You can also clear the token manually as explained in the [Manually Clearing JWT Token](#manually-clearing-jwt-token) section. + updateQuotesStep([{ + id: input.quote_id, + status: QuoteStatus.ACCEPTED, + }]) -*** + confirmOrderEditRequestWorkflow.runAsStep({ + input: { + order_id: quotes[0].draft_order_id, + confirmed_by: input.customer_id, + }, + }) -## Logout in JS SDK + updateOrderWorkflow.runAsStep({ + input:{ + id: quotes[0].draft_order_id, + // @ts-ignore + status: OrderStatus.PENDING, + is_draft_order: false, + }, + }) + } +) +``` -If you're using an API key for authentication, you can't log out the user. You'll have to unset the API key in the JS SDK configurations. +You create a workflow that accepts the IDs of the quote to accept and the customer accepting it. In the workflow, you: -The JS SDK has an `auth.logout` method that allows you to log out the currently authenticated user. +1. Use the `useQueryGraphStep` to retrieve the quote's details. You pass the IDs of the quotes and the customer as filters to ensure that the quote belongs to the customer. +2. Validate that the quote can be accepted using the `validateQuoteCanAcceptStep`. +3. Update the quote's status to `accepted` using the `updateQuotesStep`. +4. Confirm the changes made on the draft order using the `confirmOrderEditRequestWorkflow` executed as a step. This is useful when you soon add the admin functionality to edit the quote items. Any changes that the admin has made will be applied on the draft order using this step. +5. Update the draft order to change its status and convert it into an order using the `updateOrderWorkflow` executed as a step. -If the JS SDK's authentication type is `jwt`, the method will only clear the stored JWT token from the local storage. If the authentication type is `session`, the method will destroy the session cookie using Medusa's `/auth/session` API route. +You'll use this workflow next in an API route that allows a customer to accept a quote. -Any request sent after logging out will not be authenticated, and you will need to log in again to authenticate the user. +### Add Customer Accept Quote API Route -For example: +You'll now add the API route that allows a customer to accept a quote. The route will use the `customerAcceptQuoteWorkflow` you created in the previous step. -```ts -sdk.auth.logout() -.then(() => { - // user is logged out -}) -``` +Create the file `src/api/store/customers/me/quotes/[id]/accept/route.ts` with the following content: -You can learn more about this method in the [auth.logout reference](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md). +![Directory structure after adding the customer accept quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166543/Medusa%20Resources/quote-45_y8zprn.jpg) -### Manually Clearing JWT Token +```ts title="src/api/store/customers/me/quotes/[id]/accept/route.ts" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { + customerAcceptQuoteWorkflow, +} from "../../../../../../../workflows/customer-accept-quote" -If you need to clear the JWT token manually, you can use the `sdk.client.clearToken` method. This will remove the token from the local storage and all subsequent requests will not be authenticated. +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const { id } = req.params -For example: + await customerAcceptQuoteWorkflow(req.scope).run({ + input: { + quote_id: id, + customer_id: req.auth_context.actor_id, + }, + }) -```ts -sdk.client.clearToken() + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + filters: { id }, + fields: req.queryConfig.fields, + }, + { throwIfKeyNotFound: true } + ) -// all requests sent after this will not be authenticated + return res.json({ quote }) +} ``` +You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/accept`. In the route handler, you run the `customerAcceptQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. -# Medusa JS SDK +Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. -In this documentation, you'll learn how to install and use Medusa's JS SDK. +### Test Customer Accept Quote Feature -## What is Medusa JS SDK? +To test the customer accept quote feature, start the Medusa application: -Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts. +```bash npm2yarn +npm run dev +``` -*** +Then, send a request to accept a quote for the authenticated customer: -## How to Install Medusa JS SDK? +```bash +curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/accept' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +-H 'Authorization: Bearer {token}' +``` -The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations. +Make sure to replace: -To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command: +- `{quote_id}` with the ID of the quote you want to accept. +- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). +- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token). -```bash npm2yarn -npm install @medusajs/js-sdk@latest @medusajs/types@latest -``` +After sending the request, the quote will be accepted, and the updated quote will be returned in the response. -You install two libraries: +You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. The quote will also have an order, which you can view in the Orders page or using the "View Order" button on the quote's details page. -- `@medusajs/js-sdk`: the Medusa JS SDK. -- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development. +![View order button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166844/Medusa%20Resources/Screenshot_2025-03-05_at_11.27.02_AM_s90rqh.png) *** -## Setup JS SDK - -In your project, create the following `config.ts` file: +## Step 15: Edit Quote Items UI Route -For admin customizations, create this file at `src/admin/lib/config.ts`. +The last feature you'll add is allowing merchants or admin users to make changes to the quote's items. This includes updating the item's quantity and price. -### Admin (Medusa project) +Since you're using an [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to manage edits to the quote's draft orders, you don't need to implement customizations on the server side, such as adding workflows or API routes. Instead, you'll only add a new UI route in the Medusa Admin that uses the [Order Edit API routes](https://docs.medusajs.com/api/admin#order-edits) to provide the functionality to edit the quote's items. -```ts title="src/admin/lib/config.ts" -import Medusa from "@medusajs/js-sdk" +Order changes also allow you to add or remove items from the quote. However, for simplicity, this guide only covers how to update the item's quantity and price. Refer to the [Order Change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) documentation to learn more. -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` +![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png) -### Admin (Medusa Plugin) +In this step, you'll add a new UI route to manage the quote's items. This will include: -```ts title="src/admin/lib/config.ts" -import Medusa from "@medusajs/js-sdk" +1. Adding hooks to send requests to [Medusa's Order Edits API routes](https://docs.medusajs.com/api/admin#order-edits). +2. Implement the components you'll use within the UI route. +3. Add the new UI route to the Medusa Admin. -export const sdk = new Medusa({ - baseUrl: __BACKEND_URL__ || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` +### Intermission: Order Editing Overview -### Storefront +Before you start implementing the customizations, here's a quick overview of how order editing works in Medusa. -```ts title="config.ts" -import Medusa from "@medusajs/js-sdk" +When the admin wants to edit an order's items, Medusa creates an order change. You've already implemented this part on quote creation. -let MEDUSA_BACKEND_URL = "http://localhost:9000" +Then, when the admin makes an edit to an item, Medusa saves that edit but without applying it to the order or finalizing the edit. This allows the admin to make multiple edits before finalizing the changes. -if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { - MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL -} +Once the admin is finished editing, they can confirm the order edit, which finalizes it to later be applied on the order. You've already implemented applying the order edit on the order when the customer accepts the quote. -export const sdk = new Medusa({ - baseUrl: MEDUSA_BACKEND_URL, - debug: process.env.NODE_ENV === "development", - publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, -}) -``` +So, you still need two implement two aspects: updating the quote items, and confirming the order edit. You'll implement these in the next steps. -In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md) chapter. +### Add Hooks -### JS SDK Configurations +To implement the edit quote items functionality, you'll need two hooks: -The `Medusa` initializer accepts as a parameter an object with the following properties: +1. A hook that updates a quote item's quantity and price using the Order Edits API routes. +2. A hook that confirms the edit of the items using the Order Edits API routes. -|Property|Description|Default| -|---|---|---|---|---| -|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-| -|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-| -|\`auth.type\`|A string that specifies the user authentication method to use.|-| -|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`| -|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`| -|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-| -|\`auth.fetchCredentials\`|By default, if |\`include\`| -|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-| -|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-| -|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`| -|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's | +#### Update Quote Item Hook -*** +The first hook updates an item's quantity and price using the Order Edits API routes. You'll use this whenever an admin updates an item's quantity or price. -## Manage Authentication in JS SDK +In `src/admin/hooks/quotes.tsx`, add the following hook: -The JS SDK supports different types of authentication methods and allow you to flexibly configure them. +```tsx title="src/admin/hooks/quotes.tsx" +// other imports... +import { HttpTypes } from "@medusajs/framework/types" -To learn more about configuring authentication in the JS SDK and sending authenticated requests, refer to the [Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/auth/overview/index.html.md) guide. +// ... -*** +export const useUpdateQuoteItem = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminOrderEditPreviewResponse, + FetchError, + UpdateQuoteItemParams + > +) => { + const queryClient = useQueryClient() -## Send Requests to Custom Routes + return useMutation({ + mutationFn: ({ + itemId, + ...payload + }: UpdateQuoteItemParams) => { + return sdk.admin.orderEdit.updateOriginalItem(id, itemId, payload) + }, + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [orderPreviewQueryKey, id], + }) -The sidebar shows the different methods that you can use to send requests to Medusa's API routes. + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} +``` -To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request. +You create a `useUpdateQuoteItem` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that updates an item's quantity and price using the `sdk.admin.orderEdit.updateOriginalItem` method. -For example, to send a request to a custom route at `http://localhost:9000/custom`: +When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data. -### GET +#### Confirm Order Edit Hook -```ts -sdk.client.fetch(`/custom`) -.then((data) => { - console.log(data) -}) -``` +Next, you'll add a hook that confirms the order edit. This hook will be used when the admin is done editing the quote's items. As mentioned earlier, confirming the order edit doesn't apply the changes to the order but finalizes the edit. -### POST +In `src/admin/hooks/quotes.tsx`, add the following hook: -```ts -sdk.client.fetch(`/custom`, { - method: "post", - body: { - id: "123", - }, -}).then((data) => { - console.log(data) -}) -``` +```tsx title="src/admin/hooks/quotes.tsx" +export const useConfirmQuote = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminOrderEditPreviewResponse, + FetchError, + void + > +) => { + const queryClient = useQueryClient() -### DELETE + return useMutation({ + mutationFn: () => sdk.admin.orderEdit.request(id), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [orderPreviewQueryKey, id], + }) -```ts -sdk.client.fetch(`/custom`, { - method: "delete", -}).then(() => { - console.log("success") -}) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} ``` -The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK. - -In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method. +You create a `useConfirmQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that confirms the order edit using the `sdk.admin.orderEdit.request` method. -The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned. +When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data. -*** +Now that you have the necessary hooks, you can use them in the UI route and its components. -## Handle Errors +### Add ManageItem Component -If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties: +The UI route will show the list of items to the admin user and allows them to update the item's quantity and price. So, you'll create a component that allows the admin to manage a single item's details. You'll later use this component for each item in the quote. -- `status`: The HTTP status code of the response. -- `statusText`: The error code. For example, `Unauthorized`. -- `message`: The error message. For example, `Invalid credentials`. +![Screenshot of the manage item component in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186495/Medusa%20Resources/manage-item-highlight_ouffnu.png) -You can use these properties to handle errors in your application. +Create the file `src/admin/components/manage-item.tsx` with the following content: -For example: +![Directory structure after adding the manage item component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168152/Medusa%20Resources/quote-46_yxanj7.jpg) -### Promise +```tsx +import { AdminOrder, AdminOrderPreview } from "@medusajs/framework/types" +import { + Badge, + CurrencyInput, + Hint, + Input, + Label, + Text, + toast, +} from "@medusajs/ui" +import { useMemo } from "react" +import { + useUpdateQuoteItem, +} from "../hooks/quotes" +import { Amount } from "./amount" -```ts -sdk.store.customer.listAddress() -.then(({ addresses, count, offset, limit }) => { - // no errors occurred - // do something with the data - console.log(addresses) -}) -.catch((error) => { - const fetchError = error as FetchError +type ManageItemProps = { + originalItem: AdminOrder["items"][0]; + item: AdminOrderPreview["items"][0]; + currencyCode: string; + orderId: string; +}; - if (fetchError.statusText === "Unauthorized") { - // redirect to login page - } else { - // handle other errors - } -}) -``` +export function ManageItem({ + originalItem, + item, + currencyCode, + orderId, +}: ManageItemProps) { + const { mutateAsync: updateItem } = useUpdateQuoteItem(orderId) -### Async/Await + const isItemUpdated = useMemo( + () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"), + [item] + ) -```ts -try { - const { - addresses, - count, - offset, - limit, - } = await sdk.store.customer.listAddress() - // no errors occurred - // do something with the data - console.log(addresses) -} catch (error) { - const fetchError = error as FetchError + const onUpdate = async ({ + quantity, + unit_price, + }: { + quantity?: number; + unit_price?: number; + }) => { + if ( + typeof quantity === "number" && + quantity <= item.detail.fulfilled_quantity + ) { + toast.error("Quantity should be greater than the fulfilled quantity") + return + } - if (fetchError.statusText === "Unauthorized") { - // redirect to login page - } else { - // handle other errors + try { + await updateItem({ + quantity, + unit_price, + itemId: item.id, + }) + } catch (e) { + toast.error((e as any).message) + } } + + // TODO render the item's details and input fields } ``` -In the example above, you handle errors in two ways: - -- Since the JS SDK's methods return a Promise, you can use the `catch` method to handle errors. -- You can use the `try...catch` statement to handle errors when using `async/await`. This is useful when you're executing the methods as part of a larger function. - -In the `catch` method or statement, you have access to the error object of type `FetchError`. +You define a `ManageItem` component that accepts the following props: -An example of handling the error is to check if the error's `statusText` is `Unauthorized`. If so, you can redirect the customer to the login page. Otherwise, you can handle other errors by showing an alert, for example. +- `originalItem`: The original item details from the quote. This is the item's details before any edits. +- `item`: The item's details from the quote's order preview. This is the item's details which may have been edited. +- `currencyCode`: The currency code of the quote's draft order. +- `orderId`: The ID of the quote's draft order. -*** +In the component, you define the following variables: -## Pass Headers in Requests +- `updateItem`: The `mutateAsync` function returned by the `useUpdateQuoteItem` hook. This function updates the item's quantity and price using Medusa's Order Edits API routes. +- `isItemUpdated`: A boolean that indicates whether the item has been updated. -There are two ways to pass custom headers in requests when using the JS SDK: +You also define an `onUpdate` function that will be called when the admin updates the item's quantity or price. The function sends a request to update the item's quantity and price using the `updateItem` function. If the quantity is less than or equal to the fulfilled quantity, you show an error message. -1. Using the `globalHeaders` configuration: This is useful when you want to pass the same headers in all requests. For example, if you want to pass a custom header for tracking purposes: +Next, you'll add a return statement to show the item's details and allow the admin to update the item's quantity and price. Replace the `TODO` with the following: -```ts -const sdk = new Medusa({ - // ... - globalHeaders: { - "x-tracking-id": "123456789", - }, -}) -``` +```tsx title="src/admin/components/manage-item.tsx" +return ( +
+
+
+
-2. Using the headers parameter of a specific method. Every method has as a last parameter a headers parameter, which is an object of headers to pass in the request. This is useful when you want to pass a custom header in specific requests. For example, to disable HTTP compression for specific requests: +
+
+ + {item.title}{" "} + -```ts -sdk.store.product.list({ - limit, - offset, -}, { - "x-no-compression": "false", -}) -``` + {item.variant_sku && ({item.variant_sku})} +
+ + {item.product_title} + +
+
-In the example above, you pass the `x-no-compression` header in the request to disable HTTP compression. You pass it as the last parameter of the `sdk.store.product.list` method. + {isItemUpdated && ( + + Modified + + )} +
-The JS SDK appends request-specific headers to authentication headers and headers configured in the `globalHeaders` configuration. So, in the example above, the `x-no-compression` header is passed in the request along with the authentication headers and any headers configured in the `globalHeaders` configuration. +
+
+ { + const val = e.target.value + const quantity = val === "" ? null : Number(val) -*** + if (quantity) { + onUpdate({ quantity }) + } + }} + /> + + Quantity + +
-## Medusa JS SDK Tips +
+ +
+
+
-### Use Tanstack (React) Query in Admin Customizations +
+
+ + + Override the unit price of this product + +
-In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes. +
+
+ { + onUpdate({ + unit_price: parseFloat(e.target.value), + quantity: item.quantity, + }) + }} + className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover" + /> +
+
+
+
+) +``` -Tanstack Query is installed by default in your Medusa application. +You show the item's title, product title, and variant SKU. If the item has been updated, you show a "Modified" badge. -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. +You also show input fields for the quantity and price of the item, allowing the admin to update the item's quantity and price. Once the admin updates the quantity or price, the `onUpdate` function is called to send a request to update the item's details. -Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests. +### Add ManageQuoteForm Component -For example: +Next, you'll add the form component that shows the list of items in the quote and allows the admin to manage each item. You'll use the `ManageItem` component you created in the previous step for each item in the quote. -### Query +![Screenshot of the manage quote form in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186643/Medusa%20Resources/manage-quote-form-highlight_pfyee5.png) -```tsx title="src/admin/widgets/product-widget.ts" -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" +Create the file `src/admin/components/manage-quote-form.tsx` with the following content: -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} +![Directory structure after adding the manage quote form component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168581/Medusa%20Resources/quote-47_f5kamq.jpg) -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) +```tsx title="src/admin/components/manage-quote-form.tsx" +import { AdminOrder } from "@medusajs/framework/types" +import { Button, Heading, toast } from "@medusajs/ui" +import { useConfirmQuote } from "../hooks/quotes" +import { formatAmount } from "../utils/format-amount" +import { useOrderPreview } from "../hooks/order-preview" +import { useNavigate, useParams } from "react-router-dom" +import { useMemo } from "react" +import { ManageItem } from "./manage-item" -export default ProductWidget -``` +type ReturnCreateFormProps = { + order: AdminOrder; +}; -### Mutation +export const ManageQuoteForm = ({ order }: ReturnCreateFormProps) => { + const { order: preview } = useOrderPreview(order.id) + const navigate = useNavigate() + const { id: quoteId } = useParams() -```tsx title="src/admin/widgets/product-widget.ts" -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 { mutateAsync: confirmQuote, isPending: isRequesting } = + useConfirmQuote(order.id) -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + try { + await confirmQuote() + navigate(`/quotes/${quoteId}`) - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) + toast.success("Successfully updated quote") + } catch (e) { + toast.error("Error", { + description: (e as any).message, + }) + } } - return ( - - - - ) -} + const originalItemsMap = useMemo(() => { + return new Map(order.items.map((item) => [item.id, item])) + }, [order]) -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) + if (!preview) { + return <> + } -export default ProductWidget + // TODO render form +} ``` -Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation). +You define a `ManageQuoteForm` component that accepts the quote's draft order as a prop. In the component, you retrieve the preview of that order. The preview holds any edits made on the order's items. -### Cache in Next.js Projects +You also define the `confirmQuote` function using the `useConfirmQuote` hook. This function confirms the order edit, finalizing the changes made on the order's items. -Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request. +Then, you define the `handleSubmit` function that will be called when the admin submits the form. The function confirms the order edit using the `confirmQuote` function and navigates the admin back to the quote's details page. -In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching. +Next, you'll add a return statement to show the edit form for the quote's items. Replace the `TODO` with the following: -For example: +```tsx title="src/admin/components/manage-quote-form.tsx" +return ( +
+
+
+ Items +
-```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]} -sdk.store.product.list({}, { - next: { - tags: ["products"], - }, -}) -``` + {preview.items.map((item) => ( + + ))} +
-The `tags` property accepts an array of tags that the data is cached under. +
+
+ + Current Total + -Then, to purge the cache later, use Next.js's `revalidateTag` utility: + + {formatAmount(order.total, order.currency_code)} + +
-```ts -import { revalidateTag } from "next/cache" +
+ + New Total + -// ... + + {formatAmount(preview.total, order.currency_code)} + +
+
-revalidateTag("products") +
+
+ +
+
+
+) ``` -Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag). +You use the `ManageItem` component to show each item in the quote and allow the admin to update the item's quantity and price. You also show the updated total amount of the quote and a button to confirm the order edit. +You'll use this component next in the UI route that allows the admin to edit the quote's items. -# Implement Custom Line Item Pricing in Medusa +### Implement UI Route -In this guide, you'll learn how to add line items with custom prices to a cart in Medusa. +Finally, you'll add the UI route that allows the admin to edit the quote's items. The route will use the `ManageQuoteForm` component you created in the previous step. -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 managing carts and adding line items to them. +Create the file `src/admin/routes/quotes/[id]/manage/page.tsx` with the following content: -By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products. +![Directory structure after adding the edit quote items UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168993/Medusa%20Resources/quote-48_roangs.jpg) -To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing. +```tsx title="src/admin/routes/quotes/[id]/manage/page.tsx" +import { useParams } from "react-router-dom" +import { useQuote } from "../../../../hooks/quotes" +import { Container, Heading, Toaster } from "@medusajs/ui" +import { ManageQuoteForm } from "../../../../components/manage-quote-form" -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. +const QuoteManage = () => { + const { id } = useParams() + const { quote, isLoading } = useQuote(id!, { + fields: + "*draft_order.customer", + }) -### Summary + if (isLoading) { + return <> + } -This guide will teach you how to: + if (!quote) { + throw "quote not found" + } -- Install and set up Medusa. -- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver. -- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service. + return ( + <> + + + Manage Quote + -![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg) + + + + + ) +} -- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.yaml): Import this OpenApi Specs file into tools like Postman. +export default QuoteManage +``` -*** +You define a `QuoteManage` component that will show the form to manage the quote's items in the Medusa Admin dashboard. -## Step 1: Install a Medusa Application +In the component, you first retrieve the quote's details using the `useQuote` hook. Then, you show the `ManageQuoteForm` component, passing the quote's draft order as a prop. -### Prerequisites +### Add Manage Button to Quote Details Page -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +To allow the admin to access the manage page you just added, you'll add a new button on the quote's details page that links to the manage page. -Start by installing the Medusa application on your machine with the following command: +In `src/admin/routes/quotes/[id]/page.tsx`, add the following variable definition after the `showSendQuote` variable: -```bash -npx create-medusa-app@latest +```tsx title="src/admin/routes/quotes/[id]/page.tsx" +const [showManageQuote, setShowManageQuote] = useState(false) ``` -You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). +This variable will be used to show or hide the manage quote button. -Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. +Then, update the existing `useEffect` hook to the following: -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). +```tsx title="src/admin/routes/quotes/[id]/page.tsx" +useEffect(() => { + if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) { + setShowSendQuote(true) + } else { + setShowSendQuote(false) + } -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. Afterwards, you can log in with the new user and explore the dashboard. + if ( + ["customer_rejected", "merchant_rejected", "accepted"].includes( + quote?.status! + ) + ) { + setShowRejectQuote(false) + } else { + setShowRejectQuote(true) + } -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. + if (![ + "pending_merchant", + "customer_rejected", + "merchant_rejected", + ].includes(quote?.status!)) { + setShowManageQuote(false) + } else { + setShowManageQuote(true) + } +}, [quote]) +``` -*** +The `showManageQuote` variable is now updated based on the quote's status, where you only show it if the quote is pending the merchant's action, or if it has been rejected by either the customer or merchant. -## Step 2: Integrate GoldAPI.io +Finally, add the following button component after the `Send Quote` button: -### Prerequisites +```tsx title="src/admin/routes/quotes/[id]/page.tsx" +{showManageQuote && ( + +)} +``` -- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io) +The Manage Quote button is now shown if the `showManageQuote` variable is `true`. When clicked, it navigates the admin to the manage quote page. -To integrate third-party services into Medusa, you create a custom module. 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. +### Test Edit Quote Items UI Route -In this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price. +To test the edit quote items UI route, start the Medusa application: -Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). +```bash npm2yarn +npm run dev +``` -### Create Module Directory +Then, open the Medusa Admin dashboard at `http://localhost:9000/admin`. Open a quote's details page whose status is either `pending_merchant`, `merchant_rejected` or `customer_rejected`. You'll find a new "Manage Quote" button. -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/metal-prices`. +![Manage Quote button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169567/Medusa%20Resources/Screenshot_2025-03-05_at_12.12.21_PM_c5fhsp.png) -![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.jpg) +Click on the button, and you'll be taken to the manage quote page where you can update the quote's items. Try to update the items' quantities or price. Then, once you're done, click the "Confirm Edit" button to finalize the changes. -### Create Module's Service +![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png) -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. +The changes can now be previewed from the quote's details page. The customer can also see these changes using the preview API route you created earlier. Once the customer accepts the quote, the changes will be applied to the order. -In this section, you'll create the Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals. +*** -Start by creating the file `src/modules/metal-prices/service.ts` with the following content: +## Next Steps -![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg) +You've now implemented quote management features in Medusa. There's still more that you can implement to enhance the quote management experience: -```ts title="src/modules/metal-prices/service.ts" -type Options = { - accessToken: string - sandbox?: boolean -} +- Refer to the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) for more quote-management related features, including how to add or remove items from a quote, and how to allow messages between the customer and the merchant. +- To build a storefront, refer to the [Storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md). You can also add to the storefront features related to quote-management using the APIs you implemented in this guide. -export default class MetalPricesModuleService { - protected options_: Options +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. - constructor({}, options: Options) { - this.options_ = options - } -} -``` +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). -A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options: -- `accessToken`: The access token for the GoldAPI.io service. -- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment. +# Medusa Examples -The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property. +This documentation page has examples of customizations useful for your custom development in the Medusa application. -A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). +Each section links to the associated documentation page to learn more about it. -#### Add Method to Retrieve Metal Prices +## API Routes -Next, you'll add the method to retrieve the metal prices from the third-party service. +An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. -First, add the following types at the beginning of `src/modules/metal-prices/service.ts`: +### Create API Route -```ts title="src/modules/metal-prices/service.ts" -export enum MetalSymbols { - Gold = "XAU", - Silver = "XAG", - Platinum = "XPT", - Palladium = "XPD" -} +Create the file `src/api/hello-world/route.ts` with the following content: -export type PriceResponse = { - metal: MetalSymbols - currency: string - exchange: string - symbol: string - price: number - [key: string]: unknown -} +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} ``` -The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint. +This creates a `GET` API route at `/hello-world`. -Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). -```ts title="src/modules/metal-prices/service.ts" -import { MedusaError } from "@medusajs/framework/utils" +### Resolve Resources in API Route -// ... +To resolve resources from the Medusa container in an API route: -export default class MetalPricesModuleService { - // ... - async getMetalPrice( - symbol: MetalSymbols, - currency: string - ): Promise { - const upperCaseSymbol = symbol.toUpperCase() - const upperCaseCurrency = currency.toUpperCase() +```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" - return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, { - headers: { - "x-access-token": this.options_.accessToken, - "Content-Type": "application/json", - }, - redirect: "follow", - }).then((response) => response.json()) - .then((response) => { - if (response.error) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - response.error - ) - } +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const productModuleService = req.scope.resolve( + Modules.PRODUCT + ) - return response - }) - } + const [, count] = await productModuleService + .listAndCountProducts() + + res.json({ + count, + }) } ``` -The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers. +This resolves the Product Module's main service. -If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -#### Add Helper Methods +### Use Path Parameters -You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings: +API routes can accept path parameters. -```ts title="src/modules/metal-prices/service.ts" -export default class MetalPricesModuleService { - // ... - async getMetalSymbols(): Promise { - return Object.values(MetalSymbols) - } -} -``` +To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content: -The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol: +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```ts title="src/modules/metal-prices/service.ts" -export default class MetalPricesModuleService { - // ... - async getMetalSymbol(name: string): Promise { - const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() - return MetalSymbols[formattedName as keyof typeof MetalSymbols] - } +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) } ``` -You'll use these methods in later steps. +Learn more about path parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#path-parameters/index.html.md). -### Export Module Definition +### Use Query Parameters -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. +API routes can accept query parameters: -So, create the file `src/modules/metal-prices/index.ts` with the following content: +```ts highlights={queryHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `Hello ${req.query.name}`, + }) +} +``` -```ts title="src/modules/metal-prices/index.ts" -import { Module } from "@medusajs/framework/utils" -import MetalPricesModuleService from "./service" +Learn more about query parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#query-parameters/index.html.md). -export const METAL_PRICES_MODULE = "metal-prices" +### Use Body Parameters -export default Module(METAL_PRICES_MODULE, { - service: MetalPricesModuleService, -}) -``` +API routes can accept request body parameters: -You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: +```ts highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -1. The module's name, which is `metal-prices`. -2. An object with a required property `service` indicating the module's service. +type HelloWorldReq = { + name: string +} -### Add Module to Medusa's Configurations +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} +``` -Once you finish building the module, add it to Medusa's configurations to start using it. +Learn more about request body parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#request-body-parameters/index.html.md). -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: +### Set Response Code -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/metal-prices", - options: { - accessToken: process.env.GOLD_API_TOKEN, - sandbox: process.env.GOLD_API_SANDBOX === "true", - }, - }, - ], -}) +You can change the response code of an API route: + +```ts highlights={[["7", "status", "Change the response's status."]]} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) +} ``` -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. +Learn more about setting the response code in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#set-response-status-code/index.html.md). -The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables. +### Execute a Workflow in an API Route -You'll find the access token at the top of your GoldAPI.io dashboard. +To execute a workflow in an API route: -![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png) +```ts +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../workflows/hello-world" -Set the access token as an environment variable in `.env`: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run({ + input: { + name: req.query.name as string, + }, + }) -```bash -GOLD_API_TOKEN= + res.send(result) +} ``` -You'll start using the module in the next steps. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md). -*** +### Change Response Content Type -## Step 3: Add Custom Item to Cart Workflow +By default, an API route's response has the content type `application/json`. -In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in a workflow. +To change it to another content type, use the `writeHead` method of `MedusaResponse`: -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. +```ts highlights={responseContentTypeHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) -The workflow you'll implement in this section has the following steps: + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) -- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query. -- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query -- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service. -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart. -- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query. + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` -`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`. +This changes the response type to return an event stream. -### getVariantMetalPricesStep +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#change-response-content-type/index.html.md). -The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input. +### Create Middleware -To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content: +A middleware is a function executed when a request is sent to an API Route. -![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg) +Create the file `src/api/middlewares.ts` with the following content: -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { ProductVariantDTO } from "@medusajs/framework/types" -import { METAL_PRICES_MODULE } from "../../modules/metal-prices" -import MetalPricesModuleService from "../../modules/metal-prices/service" +```ts title="src/api/middlewares.ts" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -export type GetVariantMetalPricesStepInput = { - variant: ProductVariantDTO & { - calculated_price?: { - calculated_amount: number - } - } - currencyCode: string - quantity?: number -} +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") -export const getVariantMetalPricesStep = createStep( - "get-variant-metal-prices", - async ({ - variant, - currencyCode, - quantity = 1, - }: GetVariantMetalPricesStepInput, { container }) => { - const metalPricesModuleService: MetalPricesModuleService = - container.resolve(METAL_PRICES_MODULE) + next() + }, + ], + }, + { + matcher: "/custom/:id", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("With Path Parameter") - // TODO - } -) + next() + }, + ], + }, + ], +}) ``` -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: +Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). -1. The step's unique name, which is `get-variant-metal-prices`. -2. An async function that receives two parameters: - - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees. - - 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. +### Restrict HTTP Methods in Middleware -In the step function, so far you only resolve the Metal Prices Module's service from the Medusa container. +To restrict a middleware to an HTTP method: -Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file: +```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights} +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -import { MedusaError } from "@medusajs/framework/utils" +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + method: ["POST", "PUT"], + middlewares: [ + // ... + ], + }, + ], +}) ``` -And replace the `TODO` in the step function with the following: - -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -const variantMetal = variant.options.find( - (option) => option.option?.title === "Metal" -)?.value -const metalSymbol = await metalPricesModuleService - .getMetalSymbol(variantMetal || "") +### Add Validation for Custom Routes -if (!metalSymbol) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol." - ) -} +1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`: -if (!variant.weight) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Variant doesn't have weight. Make sure the variant has weight to calculate its price." - ) -} +```ts title="src/api/custom/validators.ts" +import { z } from "zod" -// TODO retrieve custom price +export const PostStoreCustomSchema = z.object({ + a: z.number(), + b: z.number(), +}) ``` -In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service. - -If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight. - -Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following: +2. Add a validation middleware to the custom route in `src/api/middlewares.ts`: -```ts title="src/workflows/steps/get-variant-metal-prices.ts" -let price = variant.calculated_price?.calculated_amount || 0 -const weight = variant.weight -const { price: metalPrice } = await metalPricesModuleService.getMetalPrice( - metalSymbol as MetalSymbols, currencyCode -) -price += (metalPrice * weight * quantity) +```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]} +import { + validateAndTransformBody, + defineMiddlewares, +} from "@medusajs/framework/http" +import { PostStoreCustomSchema } from "./custom/validators" -return new StepResponse(price) +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: "POST", + middlewares: [ + validateAndTransformBody(PostStoreCustomSchema), + ], + }, + ], +}) ``` -In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service. +3. Use the validated body in the `/custom` API route: -Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it. +```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { z } from "zod" +import { PostStoreCustomSchema } from "./validators" -Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price. +type PostStoreCustomSchemaType = z.infer< + typeof PostStoreCustomSchema +> -### Create addCustomToCartWorkflow +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + sum: req.validatedBody.a + req.validatedBody.b, + }) +} +``` -Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart. +Learn more about request body validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). -Create the file `src/workflows/add-custom-to-cart.ts` with the following content: +### Pass Additional Data to API Route -![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg) +In this example, you'll pass additional data to the Create Product API route, then consume its hook: -```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights} -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { QueryContext } from "@medusajs/framework/utils" +Find this example in details in [this documentation](https://docs.medusajs.com/docs/learn/customization/extend-features/extend-create-product/index.html.md). -type AddCustomToCartWorkflowInput = { - cart_id: string - item: { - variant_id: string - quantity: number - metadata?: Record - } -} +1. Create the file `src/api/middlewares.ts` with the following content: -export const addCustomToCartWorkflow = createWorkflow( - "add-custom-to-cart", - ({ cart_id, item }: AddCustomToCartWorkflowInput) => { - const { data: carts } = useQueryGraphStep({ - entity: "cart", - filters: { id: cart_id }, - fields: ["id", "currency_code"], - }) +```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]} +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" - const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "*", - "options.*", - "options.option.*", - "calculated_price.*", - ], - filters: { - id: item.variant_id, - }, - options: { - throwIfKeyNotFound: true, - }, - context: { - calculated_price: QueryContext({ - currency_code: carts[0].currency_code, - }), +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), }, - }).config({ name: "retrieve-variant" }) - - // TODO add more steps - } -) + }, + ], +}) ``` -You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters: - -1. The workflow's unique name, which is `add-custom-to-cart`. -2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata. - -In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code. - -You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context. +Learn more about additional data in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/additional-data/index.html.md). -Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import: +2. Create the file `src/workflows/hooks/created-product.ts` with the following content: -```ts title="src/workflows/add-custom-to-cart.ts" -import { - getVariantMetalPricesStep, - GetVariantMetalPricesStepInput, -} from "./steps/get-variant-metal-prices" -``` +```ts +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" -Then, replace the `TODO` in the workflow with the following: +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data.brand_id) { + return new StepResponse([], []) + } -```ts title="src/workflows/add-custom-to-cart.ts" -const price = getVariantMetalPricesStep({ - variant: variants[0], - currencyCode: carts[0].currency_code, - quantity: item.quantity, -} as unknown as GetVariantMetalPricesStepInput) + // TODO perform custom action + }), + (async (links, { container }) => { + // TODO undo the action in the compensation + }) -// TODO add item with custom price to cart +) ``` -You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price. +Learn more about workflow hooks in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). -Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file: +### Restrict an API Route to Admin Users -```ts title="src/workflows/add-custom-to-cart.ts" -import { transform } from "@medusajs/framework/workflows-sdk" -import { addToCartWorkflow } from "@medusajs/medusa/core-flows" -``` +You can protect API routes by restricting access to authenticated admin users only. -Then, replace the `TODO` in the workflow with the following: +Add the following middleware in `src/api/middlewares.ts`: -```ts title="src/workflows/add-custom-to-cart.ts" -const itemToAdd = transform({ - item, - price, -}, (data) => { - return [{ - ...data.item, - unit_price: data.price, - }] -}) +```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -addToCartWorkflow.runAsStep({ - input: { - items: itemToAdd, - cart_id, - }, +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [ + authenticate( + "user", + ["session", "bearer", "api-key"] + ), + ], + }, + ], }) - -// TODO retrieve and return cart ``` -You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart. - -A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). -Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file: +### Restrict an API Route to Logged-In Customers -```ts title="src/workflows/add-custom-to-cart.ts" -import { WorkflowResponse } from "@medusajs/framework/workflows-sdk" -``` +You can protect API routes by restricting access to authenticated customers only. -And replace the last `TODO` in the workflow with the following: +Add the following middleware in `src/api/middlewares.ts`: -```ts title="src/workflows/add-custom-to-cart.ts" -const { data: updatedCarts } = useQueryGraphStep({ - entity: "cart", - filters: { id: cart_id }, - fields: ["id", "items.*"], -}).config({ name: "refetch-cart" }) +```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -return new WorkflowResponse({ - cart: updatedCarts[0], +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/customer*", + middlewares: [ + authenticate("customer", ["session", "bearer"]), + ], + }, + ], }) ``` -In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). -In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart. +### Retrieve Logged-In Admin User -*** +To retrieve the currently logged-in user in an API route: -## Step 4: Create Add Custom Item to Cart API Route +Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users). -Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route. +```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" -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/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart. +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const userModuleService = req.scope.resolve( + Modules.USER + ) -Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + const user = await userModuleService.retrieveUser( + req.auth_context.actor_id + ) -### Create API Route + // ... +} +``` -An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details/index.html.md). -The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content: +### Retrieve Logged-In Customer -![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg) +To retrieve the currently logged-in customer in an API route: -```ts title="src/api/store/carts/[id]/line-items-metals/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { HttpTypes } from "@medusajs/framework/types" -import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart" +Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers). -export const POST = async ( - req: MedusaRequest, +```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" + +export const GET = async ( + req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const { id } = req.params - const item = req.validatedBody + if (req.auth_context?.actor_id) { + // retrieve customer + const customerModuleService = req.scope.resolve( + Modules.CUSTOMER + ) - const { result } = await addCustomToCartWorkflow(req.scope) - .run({ - input: { - cart_id: id, - item, - }, - }) + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id + ) + } - res.status(200).json({ cart: result.cart }) + // ... } ``` -Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details/index.html.md). -1. A request object with details and context on the request, such as path and body parameters. -2. A response object to manipulate and send the response. +### Throw Errors in API Route -In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). +To throw errors in an API route, use `MedusaError` from the Medusa Framework: -Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart. +```ts highlights={[["9", "MedusaError"]]} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" -Finally, you return a response with the updated cart's details. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + if (!req.query.q) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The `q` query parameter is required." + ) + } -### Add Request Body Validation Middleware + // ... +} +``` -To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors/index.html.md). -A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory. +### Override Error Handler of API Routes -Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). +To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content: -To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content: +```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]} +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" -![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg) +export default defineMiddlewares({ + errorHandler: ( + error: MedusaError | any, + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + res.status(400).json({ + error: "Something happened.", + }) + }, +}) +``` + +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors#override-error-handler/index.html.md), + +### Setting up CORS for Custom API Routes + +By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`. + +To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" -import { +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, defineMiddlewares, - validateAndTransformBody, } from "@medusajs/framework/http" -import { - StoreAddCartLineItem, -} from "@medusajs/medusa/api/store/carts/validators" +import { ConfigModule } from "@medusajs/framework/types" +import { parseCorsOrigins } from "@medusajs/framework/utils" +import cors from "cors" export default defineMiddlewares({ routes: [ { - matcher: "/store/carts/:id/line-items-metals", - method: "POST", + matcher: "/custom*", middlewares: [ - validateAndTransformBody( - StoreAddCartLineItem - ), + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + const configModule: ConfigModule = + req.scope.resolve("configModule") + + return cors({ + origin: parseCorsOrigins( + configModule.projectConfig.http.storeCors + ), + credentials: true, + })(req, res, next) + }, ], }, ], }) ``` -In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes. +### Parse Webhook Body -You pass in the `routes` array an object having the following properties: +By default, the Medusa application parses a request's body using JSON. -- `matcher`: The route to apply the middleware on. -- `method`: The HTTP method to apply the middleware on for the specified API route. -- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems). +To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content: -Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters. +```ts title="src/api/middlewares.ts" highlights={[["9"]]} +import { + defineMiddlewares, +} from "@medusajs/framework/http" -Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). +export default defineMiddlewares({ + routes: [ + { + matcher: "/webhooks/*", + bodyParser: { preserveRawBody: true }, + method: ["POST"], + }, + ], +}) +``` -### Prepare to Test API Route +To access the raw body data in your route, use the `req.rawBody` property: -Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart. +```ts title="src/api/webhooks/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -#### Create Product with Metal Variant +export const POST = ( + req: MedusaRequest, + res: MedusaResponse +) => { + console.log(req.rawBody) +} +``` -You'll first create a product that has a `Metal` option, and variant(s) with values for this option. +*** -Start the Medusa application with the following command: +## Modules -```bash npm2yarn -npm run dev -``` +A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database. -Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step. +### Create Module -Once you log in, click on Products in the sidebar, then click the Create button at the top right. +Find this example explained in details in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). -![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png) +1. Create the directory `src/modules/blog`. +2. Create the file `src/modules/blog/models/post.ts` with the following data model: -Then, in the Create Product form: +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" -1. Enter a name for the product, and optionally enter other details like description. -2. Enable the "Yes, this is a product with variants" toggle. -3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values. +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) -Once you're done, click the Continue button. +export default Post +``` -![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png) +3. Create the file `src/modules/blog/service.ts` with the following service: -You can skip the next two steps by clicking the Continue button again, then the Publish button. +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that: +class BlogModuleService extends MedusaService({ + Post, +}){ +} -- Scroll to the Variants section and find the Gold variant. -- Click on the three-dots icon at its right. -- Choose "Edit" from the dropdown. +export default BlogModuleService +``` -![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png) +4. Create the file `src/modules/blog/index.ts` that exports the module definition: -In the side window that opens, find the Weight field, enter the weight, and click the Save button. +```ts title="src/modules/blog/index.ts" +import BlogModuleService from "./service" +import { Module } from "@medusajs/framework/utils" -![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png) +export const BLOG_MODULE = "blog" -Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that: +export default Module(BLOG_MODULE, { + service: BlogModuleService, +}) +``` -1. Click on the three-dots icon at the top right of the Variants section. -2. Choose "Edit Prices" from the dropdown. +5. Add the module to the configurations in `medusa-config.ts`: -![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png) +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + }, + modules: [ + { + resolve: "./modules/blog", + }, + ], +}) +``` -For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button. +6. Generate and run migrations: -![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png) +```bash +npx medusa db:generate blog +npx medusa db:migrate +``` -You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data. +7. Use the module's main service in an API route: -![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png) +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import BlogModuleService from "../../modules/blog/service" +import { BLOG_MODULE } from "../../modules/blog" -#### Retrieve Publishable API Key +export async function GET( + req: MedusaRequest, + res: MedusaResponse +): Promise { + const blogModuleService: BlogModuleService = req.scope.resolve( + BLOG_MODULE + ) -All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it. + const post = await blogModuleService.createPosts({ + title: "test", + }) -To retrieve the publishable API key, on the Medusa Admin: + res.json({ + post, + }) +} +``` -1. Click on Settings in the sidebar at the bottom left. -2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list. +### Module with Multiple Services -![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png) +To add services in your module other than the main one, create them in the `services` directory of the module. -3. Click on the publishable API key to copy it. +For example, create the file `src/modules/blog/services/category.ts` with the following content: -![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png) +```ts title="src/modules/blog/services/category.ts" +export class CategoryService { + // TODO add methods +} +``` -You'll use this key when you test the API route. +Then, export the service in the file `src/modules/blog/services/index.ts`: -### Test API Route +```ts title="src/modules/blog/services/index.ts" +export * from "./category" +``` -To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route: +Finally, resolve the service in your module's main service or loader: -```bash -curl 'localhost:9000/store/regions' \ --H 'x-publishable-api-key: {api_key}' +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" +import { CategoryService } from "./services" + +type InjectedDependencies = { + categoryService: CategoryService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + private categoryService: CategoryService + + constructor({ categoryService }: InjectedDependencies) { + super(...arguments) + + this.categoryService = categoryService + } +} + +export default BlogModuleService ``` -Make sure to replace `{api_key}` with the publishable API key you copied earlier. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/multiple-services/index.html.md). -This will return a list of regions. Copy the ID of one of the regions. +### Accept Module Options -Then, send a `POST` request to the `/store/carts` API route to create a cart: +A module can accept options for configurations and secrets. -```bash -curl -X POST 'localhost:9000/store/carts' \ --H 'x-publishable-api-key: {api_key}' \ --H 'Content-Type: application/json' \ ---data '{ - "region_id": "{region_id}" -}' +To accept options in your module: + +1. Pass options to the module in `medusa-config.ts`: + +```ts title="medusa-config.ts" highlights={[["6", "options"]]} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./modules/blog", + options: { + apiKey: true, + }, + }, + ], +}) ``` -Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request. +2. Access the options in the module's main service: -This will return the created cart. Copy the ID of the cart to use it next. +```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route: +// recommended to define type in another file +type ModuleOptions = { + apiKey?: boolean +} -```bash -curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \ --H 'x-publishable-api-key: {api_key}' \ --H 'Content-Type: application/json' \ ---data '{ - "variant_id": "{variant_id}", - "quantity": 1 -}' +export default class BlogModuleService extends MedusaService({ + Post, +}){ + protected options_: ModuleOptions + + constructor({}, options?: ModuleOptions) { + super(...arguments) + + this.options_ = options || { + apiKey: false, + } + } + + // ... +} ``` -Make sure to replace: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md). -- `{api_key}` with the publishable API key you copied earlier. -- `{cart_id}` with the ID of the cart you created. -- `{variant_id}` with the ID of the Gold variant you created. +### Integrate Third-Party System in Module -This will return the cart's details, where you can see in its `items` array the item with the custom price: +An example of integrating a dummy third-party system in a module's service: -```json title="Example Response" -{ - "cart": { - "items": [ - { - "variant_id": "{variant_id}", - "quantity": 1, - "is_custom_price": true, - // example custom price - "unit_price": 2000 - } - ] +```ts title="src/modules/blog/service.ts" +import { Logger } from "@medusajs/framework/types" +import { BLOG_MODULE } from ".." + +export type ModuleOptions = { + apiKey: string +} + +type InjectedDependencies = { + logger: Logger +} + +export class BlogClient { + private options_: ModuleOptions + private logger_: Logger + + constructor( + { logger }: InjectedDependencies, + options: ModuleOptions + ) { + this.logger_ = logger + this.options_ = options + } + + private async sendRequest(url: string, method: string, data?: any) { + this.logger_.info(`Sending a ${ + method + } request to ${url}. data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`Client Options: ${ + JSON.stringify(this.options_, null, 2) + }`) } } ``` -The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart. - -This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item. +Find a longer example of integrating a third-party service in [this documentation](https://docs.medusajs.com/docs/learn/customization/integrate-systems/service/index.html.md). *** -## Next Steps - -You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart. - -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 Quote Management in Medusa - -In this guide, you'll learn how to implement quote management in Medusa. - -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. - -By default, the Medusa application provides standard commerce features for orders and carts. However, Medusa's customization capabilities facilitate extending existing features to implement quote-management features. +## Data Models -By building quote management features, you allow customers to request a quote for a set of products and, once the merchant and customer reach an agreement, you create an order for that quote. Quote management is useful in many use cases, including B2B stores. +A data model represents a table in the database. Medusa provides a data model language to intuitively create data models. -This guide is based on the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) explaining how to implement some of its quote management features. You can refer to the B2B starter for other features not covered in this guide. +### Create Data Model -## Summary +To create a data model in a module: -By following this guide, you'll add the following features to Medusa: +This assumes you already have a module. If not, follow [this example](#create-module). -1. Customers can request a quote for a set of products. -2. Merchants can manage quotes in the Medusa Admin dashboard. They can reject a quote or send a counter-offer, and they can make edits to item prices and quantities. -3. Customers can accept or reject a quote once it's been sent by the merchant. -4. Once the customer accepts a quote, it's converted to an order in Medusa. +1. Create the file `src/modules/blog/models/post.ts` with the following data model: -![Diagram showcasing the features summary](https://res.cloudinary.com/dza7lstvk/image/upload/v1741173690/Medusa%20Resources/quote-management-summary_xd319j.jpg) +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" -To implement these features, you'll be customizing the Medusa server and the Medusa Admin dashboard. +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. +export default Post +``` -- [Quote Management Repository](https://github.com/medusajs/examples/tree/main/quote-management): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741171875/OpenApi/quote-management_tbk552.yml): Import this OpenApi Specs file into tools like Postman. +2. Generate and run migrations: -*** +```bash +npx medusa db:generate blog +npx medusa db:migrate +``` -## Step 1: Install a Medusa Application +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md). -### Prerequisites +### Data Model Property Types -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +A data model can have properties of the following types: -Start by installing the Medusa application on your machine with the following command: +1. ID property: -```bash -npx create-medusa-app@latest +```ts +const Post = model.define("post", { + id: model.id(), + // ... +}) ``` -You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). - -Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. +2. Text property: -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). +```ts +const Post = model.define("post", { + title: model.text(), + // ... +}) +``` -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. Afterwards, you can log in with the new user and explore the dashboard. +3. Number property: -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. +```ts +const Post = model.define("post", { + views: model.number(), + // ... +}) +``` -*** +4. Big Number property: -## Step 2: Add Quote Module +```ts +const Post = model.define("post", { + price: model.bigNumber(), + // ... +}) +``` -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. +5. Boolean property: -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 and link its data models to other modules' data models, such as orders and carts. +```ts +const Post = model.define("post", { + isPublished: model.boolean(), + // ... +}) +``` -In this step, you'll build a Quote Module that defines the necessary data model to store quotes. +6. Enum property: -Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). +```ts +const Post = model.define("post", { + status: model.enum(["draft", "published"]), + // ... +}) +``` -### Create Module Directory +7. Date-Time property: -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/quote`. +```ts +const Post = model.define("post", { + publishedAt: model.dateTime(), + // ... +}) +``` -![Diagram showcasing the directory structure after adding the Quote Module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074268/Medusa%20Resources/quote-1_lxgyyg.jpg) +8. JSON property: -### Create Data Models +```ts +const Post = model.define("post", { + metadata: model.json(), + // ... +}) +``` -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. +9. Array property: -Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md). +```ts +const Post = model.define("post", { + tags: model.array(), + // ... +}) +``` -For the Quote Module, you need to define a `Quote` data model that represents a quote requested by a customer. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md). -So, start by creating the `Quote` data model. Create the file `src/modules/quote/models/quote.ts` with the following content: +### Set Primary Key -![Diagram showcasing the directory structure after adding the quote model](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074453/Medusa%20Resources/quote-2_lh012l.jpg) +To set an `id` property as the primary key of a data model: -```ts title="src/modules/quote/models/quote.ts" highlights={quoteModelHighlights} +```ts highlights={[["4", "primaryKey"]]} import { model } from "@medusajs/framework/utils" -export enum QuoteStatus { - PENDING_MERCHANT = "pending_merchant", - PENDING_CUSTOMER = "pending_customer", - ACCEPTED = "accepted", - CUSTOMER_REJECTED = "customer_rejected", - MERCHANT_REJECTED = "merchant_rejected", -} - -export const Quote = model.define("quote", { +const Post = model.define("post", { id: model.id().primaryKey(), - status: model - .enum(Object.values(QuoteStatus)) - .default(QuoteStatus.PENDING_MERCHANT), - customer_id: model.text(), - draft_order_id: model.text(), - order_change_id: model.text(), - cart_id: model.text(), + // ... }) -``` - -You define the `Quote` 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. - -`Quote` has the following properties: - -- `id`: A unique identifier for the quote. -- `status`: The status of the quote, which can be one of the following: - - `pending_merchant`: The quote is pending the merchant's approval or rejection. - - `pending_customer`: The quote is pending the customer's acceptance or rejection. - - `accepted`: The quote has been accepted by the customer and converted to an order. - - `customer_rejected`: The customer has rejected the quote. - - `merchant_rejected`: The merchant has rejected the quote. -- `customer_id`: The ID of the customer who requested the quote. You'll later learn how to link this to a customer record. -- `draft_order_id`: The ID of the draft order created for the quote. You'll later learn how to link this to an order record. -- `order_change_id`: The ID of the order change created for the quote. An [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) is a record of changes made to an order, such as price or quantity updates of the order's items. These changes are later applied to the order. You'll later learn how to link this to an order change record. -- `cart_id`: The ID of the cart that the quote was created from. The cart will hold the items that the customer wants a quote for. You'll later learn how to link this to a cart record. - -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 +export default Post +``` -You now have the necessary data model in the Quote Module, but you need to define the logic to manage it. You do this by creating a service in the module. +To set a `text` property as the primary key: -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. +```ts highlights={[["4", "primaryKey"]]} +import { model } from "@medusajs/framework/utils" -Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md). +const Post = model.define("post", { + title: model.text().primaryKey(), + // ... +}) -To create the Quote Module's service, create the file `src/modules/quote/service.ts` with the following content: +export default Post +``` -![Directory structure after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1741075946/Medusa%20Resources/quote-4_hg4bnr.jpg) +To set a `number` property as the primary key: -```ts title="src/modules/quote/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import { Quote } from "./models/quote" +```ts highlights={[["4", "primaryKey"]]} +import { model } from "@medusajs/framework/utils" -class QuoteModuleService extends MedusaService({ - Quote, -}) {} +const Post = model.define("post", { + views: model.number().primaryKey(), + // ... +}) -export default QuoteModuleService +export default Post ``` -The `QuoteModuleService` 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. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#set-primary-key-property/index.html.md). -So, the `QuoteModuleService` class now has methods like `createQuotes` and `retrieveQuote`. +### Default Property Value -Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md). +To set the default value of a property: -You'll use this service later when you implement custom flows for quote management. +```ts highlights={[["6"], ["9"]]} +import { model } from "@medusajs/framework/utils" -### Export Module Definition +const Post = model.define("post", { + status: model + .enum(["draft", "published"]) + .default("draft"), + views: model + .number() + .default(0), + // ... +}) -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. +export default Post +``` -So, create the file `src/modules/quote/index.ts` with the following content: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#property-default-value/index.html.md). -![Directory structure after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076106/Medusa%20Resources/quote-5_ngitn1.jpg) +### Nullable Property -```ts title="src/modules/quote/index.ts" -import { Module } from "@medusajs/framework/utils" -import QuoteModuleService from "./service" +To allow `null` values for a property: -export const QUOTE_MODULE = "quote" +```ts highlights={[["4", "nullable"]]} +import { model } from "@medusajs/framework/utils" -export default Module(QUOTE_MODULE, { - service: QuoteModuleService, +const Post = model.define("post", { + price: model.bigNumber().nullable(), + // ... }) -``` - -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 `quote`. -2. An object with a required property `service` indicating the module's service. +export default Post +``` -You also export the module's name as `QUOTE_MODULE` so you can reference it later. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). -### Add Module to Medusa's Configurations +### Unique Property -Once you finish building the module, add it to Medusa's configurations to start using it. +To create a unique index on a property: -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: +```ts highlights={[["4", "unique"]]} +import { model } from "@medusajs/framework/utils" -```ts title="medusa-config.ts" -module.exports = defineConfig({ +const Post = model.define("post", { + title: model.text().unique(), // ... - modules: [ - { - resolve: "./src/modules/quote", - }, - ], }) + +export default Post ``` -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. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#unique-property/index.html.md). -### Generate Migrations +### Define Database Index on Property -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. +To define a database index on a property: -Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md). +```ts highlights={[["5", "index"]]} +import { model } from "@medusajs/framework/utils" -Medusa's CLI tool generates the migrations for you. To generate a migration for the Quote Module, run the following command in your Medusa application's directory: +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + title: model.text().index( + "IDX_POST_TITLE" + ), +}) -```bash -npx medusa db:generate quote +export default MyCustom ``` -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/quote` that holds the generated migration. - -![The directory structure of the Quote Module after generating the migration](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076301/Medusa%20Resources/quote-6_adzf76.jpg) - -Then, to reflect these migrations on the database, run the following command: - -```bash -npx medusa db:migrate -``` +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md). -The table for the `Quote` data model is now created in the database. +### Define Composite Index on Data Model -*** +To define a composite index on a data model: -## Step 3: Define Links to Other Modules +```ts highlights={[["7", "indexes"]]} +import { model } from "@medusajs/framework/utils" -When you defined the `Quote` data model, you added properties that store the ID of records managed by other modules. For example, the `customer_id` property stores the ID of the customer that requested the quote, but customers are managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, + }, + }, +]) -Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules. +export default MyCustom +``` -Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md). -To learn more about module isolation, refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +### Make a Property Searchable -In this step, you'll define the following links between the Quote Module's data model and data models in other modules: +To make a property searchable using terms or keywords: -1. `Quote` \<> `Cart` data model of the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md): link quotes to the carts they were created from. -2. `Quote` \<> `Customer` data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md): link quotes to the customers who requested them. -3. `Quote` \<> `OrderChange` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to the order changes that record adjustments made to the quote's draft order. -4. `Quote` \<> `Order` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to their draft orders that are later converted to orders. +```ts highlights={[["4", "searchable"]]} +import { model } from "@medusajs/framework/utils" -### Define Quote \<> Cart Link +const Post = model.define("post", { + title: model.text().searchable(), + // ... +}) -You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, to define the link between the `Quote` and `Cart` data models, create the file `src/links/quote-cart.ts` with the following content: +export default Post +``` -![Directory structure after adding the quote-cart link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741077395/Medusa%20Resources/quote-7_xrvodi.jpg) +Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service: -```ts title="src/links/quote-cart.ts" highlights={quoteCartHighlights} -import { defineLink } from "@medusajs/framework/utils" -import QuoteModule from "../modules/quote" -import CartModule from "@medusajs/medusa/cart" +`blogModuleService` is the main service that manages the `Post` data model. -export default defineLink( - { - linkable: QuoteModule.linkable.quote.id, - field: "cart_id", - }, - CartModule.linkable.cart, - { - readOnly: true, - } -) +```ts +const posts = await blogModuleService.listPosts({ + q: "John", +}) ``` -You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters: - -1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Quote` data model from the `QuoteModule` module, specifying that its `cart_id` property holds the ID of the linked record. -2. An object indicating the second data model part of the link. You pass the link configurations for the `Cart` data model from the `CartModule` module. -3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`. - -You'll now be able to retrieve the cart that a quote was created from, as you'll see in later steps. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#searchable-property/index.html.md). -### Define Quote \<> Customer Link +### Create One-to-One Relationship -Next, you'll define the link between the `Quote` and `Customer` data model of the Customer Module. So, create the file `src/links/quote-customer.ts` with the following content: +The following creates a one-to-one relationship between the `User` and `Email` data models: -![Directory structure after adding the quote-customer link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078047/Medusa%20Resources/quote-8_bbngmh.jpg) +```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]} +import { model } from "@medusajs/framework/utils" -```ts title="src/links/quote-customer.ts" -import { defineLink } from "@medusajs/framework/utils" -import QuoteModule from "../modules/quote" -import CustomerModule from "@medusajs/medusa/customer" +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "user", + }), +}) -export default defineLink( - { - linkable: QuoteModule.linkable.quote.id, - field: "customer_id", - }, - CustomerModule.linkable.customer, - { - readOnly: true, - } -) +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) ``` -You define the link between the `Quote` and `Customer` data models in the same way as the `Quote` and `Cart` link. In the first object parameter of `defineLink`, you pass the linkable configurations of the `Quote` data model, specifying the `customer_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `Customer` data model from the Customer Module. You also configure the link to be read-only. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md). -### Define Quote \<> OrderChange Link +### Create One-to-Many Relationship -Next, you'll define the link between the `Quote` and `OrderChange` data model of the Order Module. So, create the file `src/links/quote-order-change.ts` with the following content: +The following creates a one-to-many relationship between the `Store` and `Product` data models: -![Directory structure after adding the quote-order-change link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078511/Medusa%20Resources/quote-11_faac5m.jpg) +```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]} +import { model } from "@medusajs/framework/utils" -```ts title="src/links/quote-order-change.ts" -import { defineLink } from "@medusajs/framework/utils" -import QuoteModule from "../modules/quote" -import OrderModule from "@medusajs/medusa/order" +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), +}) -export default defineLink( - { - linkable: QuoteModule.linkable.quote.id, - field: "order_change_id", - }, - OrderModule.linkable.orderChange, - { - readOnly: true, - } -) +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) ``` -You define the link between the `Quote` and `OrderChange` data models in the same way as the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `order_change_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `OrderChange` data model from the Order Module. You also configure the link to be read-only. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md). -### Define Quote \<> Order Link +### Create Many-to-Many Relationship -Finally, you'll define the link between the `Quote` and `Order` data model of the Order Module. So, create the file `src/links/quote-order.ts` with the following content: +The following creates a many-to-many relationship between the `Order` and `Product` data models: -![Directory structure after adding the quote-order link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078607/Medusa%20Resources/quote-12_ixr2f7.jpg) +```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]} +import { model } from "@medusajs/framework/utils" -```ts title="src/links/quote-order.ts" -import { defineLink } from "@medusajs/framework/utils" -import QuoteModule from "../modules/quote" -import OrderModule from "@medusajs/medusa/order" +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + }), +}) -export default defineLink( - { - linkable: QuoteModule.linkable.quote.id, - field: "draft_order_id", - }, - { - linkable: OrderModule.linkable.order.id, - alias: "draft_order", - }, - { - readOnly: true, - } -) +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) ``` -You define the link between the `Quote` and `Order` data models similar to the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `draft_order_id` property as the link field. - -In the second object parameter, you pass the linkable configurations of the `Order` data model from the Order Module. You also set an `alias` property to `draft_order`. This allows you later to retrieve the draft order of a quote with the `draft_order` alias rather than the default `order` alias. Finally, you configure the link to be read-only. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md). -You've finished creating the links that allow you to retrieve data related to quotes. You'll see how to use these links in later steps. +### Configure Cascades of Data Model -*** +To configure cascade on a data model: -## Step 4: Implement Create Quote Workflow +```ts highlights={[["10", "cascades"]]} +import { model } from "@medusajs/framework/utils" +import Product from "./product" -You're now ready to start implementing quote-management features. The first one you'll implement is the ability for customers to request a quote for a set of items in their cart. +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product, { + mappedBy: "store", + }), +}) +.cascades({ + delete: ["products"], +}) +``` -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. +This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted. -So, in this section, you'll learn how to create a workflow that creates a quote for a customer. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#cascades/index.html.md). -Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +### Manage One-to-One Relationship -The workflow will have the following steps: +Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart that the customer wants a quote for. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the customer requesting the quote. -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md): Create the draft order for the quote. -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md): Create the order change for the draft order. -- [createQuotesStep](#createQuotesStep): Create the quote for the customer. +To set the ID of the user that an email belongs to: -The first four steps are provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createQuotesStep` step. +`blogModuleService` is the main service that manages the `Email` and `User` data models. -### createQuotesStep +```ts +// when creating an email +const email = await blogModuleService.createEmails({ + // other properties... + user: "123", +}) -In the last step of the workflow, you'll create a quote for the customer using the Quote Module's service. +// when updating an email +const email = await blogModuleService.updateEmails({ + id: "321", + // other properties... + user: "123", +}) +``` -To create a step, create the file `src/workflows/steps/create-quotes.ts` with the following content: +And to set the ID of a user's email when creating or updating it: -![Directory structure after adding the create-quotes step](https://res.cloudinary.com/dza7lstvk/image/upload/v1741085446/Medusa%20Resources/quote-13_tv9i23.jpg) +```ts +// when creating a user +const user = await blogModuleService.createUsers({ + // other properties... + email: "123", +}) -```ts title="src/workflows/steps/create-quotes.ts" highlights={createQuotesStepHighlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { QUOTE_MODULE } from "../../modules/quote" -import QueryModuleService from "../../modules/quote/service" +// when updating a user +const user = await blogModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` -type StepInput = { - draft_order_id: string; - order_change_id: string; - cart_id: string; - customer_id: string; -}[] +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship/index.html.md). -export const createQuotesStep = createStep( - "create-quotes", - async (input: StepInput, { container }) => { - const quoteModuleService: QueryModuleService = container.resolve( - QUOTE_MODULE - ) +### Manage One-to-Many Relationship - const quotes = await quoteModuleService.createQuotes(input) +Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products. - return new StepResponse( - quotes, - quotes.map((quote) => quote.id) - ) - } -) -``` +To set the ID of the store that a product belongs to: -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: +`blogModuleService` is the main service that manages the `Product` and `Store` data models. -1. The step's unique name, which is `create-quotes`. -2. An async function that receives two parameters: - - The step's input, which is in this case an array of quotes to create. - - 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. +```ts +// when creating a product +const product = await blogModuleService.createProducts({ + // other properties... + store_id: "123", +}) -In the step function, you resolve the Quote Module's service from the Medusa container using the `resolve` method of the container, passing it the module's name as a parameter. +// when updating a product +const product = await blogModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) +``` -Then, you create the quotes using the `createQuotes` method. As you remember, the Quote Module's service extends the `MedusaService` which generates data-management methods for you. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship/index.html.md) -A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: +### Manage Many-to-Many Relationship -1. The step's output, which is the quotes created. -2. Data to pass to the step's compensation function, which you'll add next. +Consider you have a many-to-many relationship between `Order` and `Product` data models. -#### Add Compensation to Step +To set the orders a product has when creating it: -A step can have a compensation function that 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. +`blogModuleService` is the main service that manages the `Product` and `Order` data models. -To add a compensation function to a step, pass it as a third-parameter to `createStep`: +```ts +const product = await blogModuleService.createProducts({ + // other properties... + orders: ["123", "321"], +}) +``` -```ts title="src/workflows/steps/create-quotes.ts" -export const createQuotesStep = createStep( - // ... - async (quoteIds, { container }) => { - if (!quoteIds) { - return - } - - const quoteModuleService: QueryModuleService = container.resolve( - QUOTE_MODULE - ) +To add new orders to a product without removing the previous associations: - await quoteModuleService.deleteQuotes(quoteIds) +```ts +const product = await blogModuleService.retrieveProduct( + "123", + { + relations: ["orders"], } ) -``` - -The compensation function accepts two parameters: - -1. The data passed from the step in the second parameter of `StepResponse`, which in this case is an array of quote IDs. -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 Quote Module's service from the Medusa container and call the `deleteQuotes` method to delete the quotes created in the step. - -### createRequestForQuoteWorkflow - -You can now create the workflow using the steps provided by Medusa and your custom step. -To create the workflow, create the file `src/workflows/create-request-for-quote.ts` with the following content: +const updatedProduct = await blogModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], +}) +``` -```ts title="src/workflows/create-request-for-quote.ts" highlights={createRequestForQuoteHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports" -import { - beginOrderEditOrderWorkflow, - createOrderWorkflow, - CreateOrderWorkflowInput, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { OrderStatus } from "@medusajs/framework/utils" -import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/workflows-sdk" -import { CreateOrderLineItemDTO } from "@medusajs/framework/types" -import { createQuotesStep } from "./steps/create-quotes" +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship/index.html.md). -type WorkflowInput = { - cart_id: string; - customer_id: string; -}; +### Retrieve Related Records -export const createRequestForQuoteWorkflow = createWorkflow( - "create-request-for-quote", - (input: WorkflowInput) => { - const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "id", - "sales_channel_id", - "currency_code", - "region_id", - "customer.id", - "customer.email", - "shipping_address.*", - "billing_address.*", - "items.*", - "shipping_methods.*", - "promotions.code", - ], - filters: { id: input.cart_id }, - options: { - throwIfKeyNotFound: true, - }, - }) +To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods: - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id", "customer"], - filters: { id: input.customer_id }, - options: { - throwIfKeyNotFound: true, - }, - }).config({ name: "customer-query" }) +`blogModuleService` is the main service that manages the `Product` and `Order` data models. - // TODO create order +```ts highlights={[["4", "relations"]]} +const product = await blogModuleService.retrieveProducts( + "123", + { + relations: ["orders"], } ) ``` -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation/index.html.md). -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 having the ID of the customer requesting the quote, and the ID of their cart. +*** -In the workflow's constructor function, you use `useQueryGraphStep` to retrieve the cart and customer details using the IDs passed as an input to the workflow. +## Services -`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), whic allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array. +A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems. -Next, you want to create the draft order for the quote. Replace the `TODO` in the workflow with the following: +### Extend Service Factory -```ts title="src/workflows/create-request-for-quote.ts" -const orderInput = transform({ carts, customers }, ({ carts, customers }) => { - return { - is_draft_order: true, - status: OrderStatus.DRAFT, - sales_channel_id: carts[0].sales_channel_id || undefined, - email: customers[0].email || undefined, - customer_id: customers[0].id || undefined, - billing_address: carts[0].billing_address, - shipping_address: carts[0].shipping_address, - items: carts[0].items as CreateOrderLineItemDTO[] || [], - region_id: carts[0].region_id || undefined, - promo_codes: carts[0].promotions?.map((promo) => promo?.code), - currency_code: carts[0].currency_code, - shipping_methods: carts[0].shipping_methods || [], - } as CreateOrderWorkflowInput -}) +The service factory `MedusaService` generates data-management methods for your data models. -const draftOrder = createOrderWorkflow.runAsStep({ - input: orderInput, -}) +To extend the service factory in your module's service: -// TODO create order change -``` +```ts highlights={[["4", "MedusaService"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -You first prepare the order's details using `transform` from the Workflows SDK. Since Medusa creates an internal representation of the workflow's constructor before any data actually has a value, you can't manipulate data directly in the function. So, Medusa provides utilities like `transform` to manipulate data instead. You can learn more in the [transform variables](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation. +class BlogModuleService extends MedusaService({ + Post, +}){ + // TODO implement custom methods +} -Then, you create the draft order using the `createOrderWorkflow` workflow which you imported from `@medusajs/medusa/core-flows`. The workflow creates and returns the created order. +export default BlogModuleService +``` -After that, you want to create an order change for the draft order. This will allow the admin later to make edits to the draft order, such as updating the prices or quantities of the items in the order. +The `BlogModuleService` will now have data-management methods for `Post`. -Replace the `TODO` with the following: +Refer to [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) for details on the generated methods. -```ts title="src/workflows/create-request-for-quote.ts" -const orderEditInput = transform({ draftOrder }, ({ draftOrder }) => { - return { - order_id: draftOrder.id, - description: "", - internal_note: "", - metadata: {}, - } -}) +Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md). -const changeOrder = beginOrderEditOrderWorkflow.runAsStep({ - input: orderEditInput, -}) +### Resolve Resources in the Service -// TODO create quote -``` +To resolve resources from the module's container in a service: -You prepare the order change's details using `transform` and then create the order change using the `beginOrderEditOrderWorkflow` workflow which is provided by Medusa. +### With Service Factory -Finally, you want to create the quote for the customer and return it. Replace the last `TODO` with the following: +```ts highlights={[["14"]]} +import { Logger } from "@medusajs/framework/types" +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -```ts title="src/workflows/create-request-for-quote.ts" -const quoteData = transform({ - draftOrder, - carts, - customers, - changeOrder, -}, ({ draftOrder, carts, customers, changeOrder }) => { - return { - draft_order_id: draftOrder.id, - cart_id: carts[0].id, - customer_id: customers[0].id, - order_change_id: changeOrder.id, - } -}) +type InjectedDependencies = { + logger: Logger +} -const quotes = createQuotesStep([ - quoteData, -]) +class BlogModuleService extends MedusaService({ + Post, +}){ + protected logger_: Logger -return new WorkflowResponse({ quote: quotes[0] }) -``` + constructor({ logger }: InjectedDependencies) { + super(...arguments) + this.logger_ = logger -Similar to before, you prepare the quote's details using `transform`. Then, you create the quote using the `createQuotesStep` you implemented earlier. + this.logger_.info("[BlogModuleService]: Hello World!") + } -A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created quote in this case. + // ... +} -In the next step, you'll learn how to execute the workflow when a customer requests a quote. +export default BlogModuleService +``` -*** +### Without Service Factory -## Step 5: Create Quote API Route +```ts highlights={[["10"]]} +import { Logger } from "@medusajs/framework/types" -Now that you have the logic to create a quote for a customer, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). +type InjectedDependencies = { + logger: Logger +} -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/quotes` that executes the workflow from the previous step. +export default class BlogModuleService { + protected logger_: Logger -Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger -### Implement API Route + this.logger_.info("[BlogModuleService]: Hello World!") + } -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`. + // ... +} +``` -By default, all routes starting with `/store/customers/me` require the customer to be authenticated. So, you'll be creating the API route at `/store/customers/me/quotes`. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). -To create the API route, create the file `src/api/store/customers/me/quotes/route.ts` with the following content: +### Access Module Options in Service -![Directory structure after adding the store/quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741086995/Medusa%20Resources/quote-14_meo0yo.jpg) +To access options passed to a module in its service: -```ts title="src/api/store/customers/me/quotes/route.ts" highlights={createQuoteApiHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -import { - createRequestForQuoteWorkflow, -} from "../../../../../workflows/create-request-for-quote" +```ts highlights={[["14", "options"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -type CreateQuoteType = { - cart_id: string; +// recommended to define type in another file +type ModuleOptions = { + apiKey?: boolean } -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const { - result: { quote: createdQuote }, - } = await createRequestForQuoteWorkflow(req.scope).run({ - input: { - ...req.validatedBody, - customer_id: req.auth_context.actor_id, - }, - }) +export default class BlogModuleService extends MedusaService({ + Post, +}){ + protected options_: ModuleOptions - const query = req.scope.resolve( - ContainerRegistrationKeys.QUERY - ) + constructor({}, options?: ModuleOptions) { + super(...arguments) - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - fields: req.queryConfig.fields, - filters: { id: createdQuote.id }, - }, - { throwIfKeyNotFound: true } - ) + this.options_ = options || { + apiKey: "", + } + } - return res.json({ quote }) + // ... } ``` -Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/quotes`. 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. - -`AuthenticatedMedusaRequest` accepts the request body's type as a type argument. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md). -In the route handler function, you create the quote using the [createRequestForQuoteWorkflow](#createrequestforquoteworkflow) from the previous step. Then, you resolve Query from the Medusa container, which is available in the request object's `req.scope` property. +### Run Database Query in Service -You use Query to retrieve the Quote with its fields and linked records, which you'll learn how to specify soon. Finally, you send the quote as a response. +To run database query in your service: -### Add Validation Schema +```ts highlights={[["14", "count"], ["21", "execute"]]} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" -The API route accepts the cart ID as a request body parameter. So, it's important to validate the body of a request before executing the route's handler. You can do this by specifying a validation schema in a middleware for the API route. +class BlogModuleService { + // ... -In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/validators.ts` with the following content: + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context + ): Promise { + return await sharedContext.manager.count("post") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context + ): Promise { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM post" + ) + + return parseInt(data[0].num) + } +} +``` -![Directory structure after adding the validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089363/Medusa%20Resources/quote-15_iy6jem.jpg) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#run-queries/index.html.md) -```ts title="src/api/store/validators.ts" -import { z } from "zod" +### Execute Database Operations in Transactions -export type CreateQuoteType = z.infer; -export const CreateQuote = z - .object({ - cart_id: z.string().min(1), - }) - .strict() -``` +To execute database operations within a transaction in your service: -You define a `CreateQuote` schema using Zod that specifies the `cart_id` parameter as a required string. +```ts +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -You also export a type inferred from the schema. So, go back to `src/api/store/customers/me/quotes/route.ts` and replace the implementation of `CreateQuoteType` to import the type from the `validators.ts` file instead: +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "post", + { + id: input.id, + }, + { + name: input.name, + } + ) -```ts title="src/api/store/customers/me/quotes/route.ts" -// other imports... -// add the following import -import { CreateQuoteType } from "../../../validators" + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM post WHERE id = '${input.id}'` + ) -// remove CreateQuoteType definition + return updatedRecord + } -export const POST = async ( - // keep type argument the same - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - // ... + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } } ``` -### Apply Validation Schema Middleware +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#execute-operations-in-transactions/index.html.md). -Now that you have the validation schema, you need to add the middleware that ensures the request body is validated before the route handler is executed. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler. +*** -Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). +## Module Links -Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content: +A module link forms an association between two data models of different modules, while maintaining module isolation. -![Directory structure after adding the store middlewares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089625/Medusa%20Resources/quote-16_oryolz.jpg) +### Define a Link -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { CreateQuote } from "./store/validators" +To define a link between your custom module and a Commerce Module, such as the Product Module: -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - matcher: "/store/customers/me/quotes", - middlewares: [ - validateAndTransformBody(CreateQuote), - ], - }, - ], -}) +1. Create the file `src/links/blog-product.ts` with the following content: + +```ts title="src/links/blog-product.ts" +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.post +) ``` -To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties: +2. Run the following command to sync the links: -- `method`: The HTTP methods the middleware applies to, which is in this case `POST`. -- `matcher`: The path of the route the middleware applies to. -- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply the `validateAndTransformBody` middleware, which accepts a Zod schema as a parameter and validates that a request's body matches the schema. If not, it throws and returns an error. +```bash +npx medusa db:migrate +``` -### Specify Quote Fields to Retrieve +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). -In the route handler you just created, you specified what fields to retrieve in a quote using the `req.queryConfig.fields` property. The `req.queryConfig` field holds query configurations indicating the default fields to retrieve when using Query to return data in a request. This is useful to unify the returned data structure across different routes, or to allow clients to specify the fields they want to retrieve. +### Define a List Link -To add the Query configurations, you'll first create a file that exports the default fields to retrieve for a quote, then apply them in a `validateAndTransformQuery` middleware. +To define a list link, where multiple records of a model can be linked to a record in another: -Learn more about configuring Query for requests in the [Request Query Configurations documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). +```ts highlights={[["9", "isList"]]} +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -Create the file `src/api/store/customers/me/quotes/query-config.ts` with the following content: +export default defineLink( + ProductModule.linkable.product, + { + linkable: BlogModule.linkable.post, + isList: true, + } +) +``` -![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741090067/Medusa%20Resources/quote-17_n6xsdb.jpg) +Learn more about list links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md). -```ts title="src/api/store/customers/me/quotes/query-config.ts" -export const quoteFields = [ - "id", - "status", - "*customer", - "cart.id", - "draft_order.id", - "draft_order.currency_code", - "draft_order.display_id", - "draft_order.region_id", - "draft_order.status", - "draft_order.version", - "draft_order.summary", - "draft_order.total", - "draft_order.subtotal", - "draft_order.tax_total", - "draft_order.order_change", - "draft_order.discount_total", - "draft_order.discount_tax_total", - "draft_order.original_total", - "draft_order.original_tax_total", - "draft_order.item_total", - "draft_order.item_subtotal", - "draft_order.item_tax_total", - "draft_order.original_item_total", - "draft_order.original_item_subtotal", - "draft_order.original_item_tax_total", - "draft_order.shipping_total", - "draft_order.shipping_subtotal", - "draft_order.shipping_tax_total", - "draft_order.original_shipping_tax_total", - "draft_order.original_shipping_subtotal", - "draft_order.original_shipping_total", - "draft_order.created_at", - "draft_order.updated_at", - "*draft_order.items", - "*draft_order.items.tax_lines", - "*draft_order.items.adjustments", - "*draft_order.items.variant", - "*draft_order.items.variant.product", - "*draft_order.items.detail", - "*draft_order.payment_collections", - "*order_change.actions", -] +### Set Delete Cascade on Link Definition -export const retrieveStoreQuoteQueryConfig = { - defaults: quoteFields, - isList: false, -} +To ensure a model's records linked to another model are deleted when the linked model is deleted: -export const listStoreQuoteQueryConfig = { - defaults: quoteFields, - isList: true, -} -``` +```ts highlights={[["9", "deleteCascades"]]} +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -You export two objects: +export default defineLink( + ProductModule.linkable.product, + { + linkable: BlogModule.linkable.post, + deleteCascades: true, + } +) +``` -- `retrieveStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a single quote. -- `listStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a list of quotes, which you'll use later. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md). -Notice that in the fields retrieved, you specify linked records such as `customer` and `draft_order`. You can do this because you've defined links between the `Quote` data model and these data models previously. +### Add Custom Columns to Module Link -For simplicity, this guide will apply the `listStoreQuoteQueryConfig` to all routes starting with `/store/customers/me/quotes`. However, you should instead apply `retrieveStoreQuoteQueryConfig` to routes that retrieve a single quote, and `listStoreQuoteQueryConfig` to routes that retrieve a list of quotes. +To add a custom column to the table that stores the linked records of two data models: -Next, you'll define a Zod schema that allows client applications to specify the fields they want to retrieve in a quote as a query parameter. In `src/api/store/validators.ts`, add the following schema: +```ts highlights={[["9", "database"]]} +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -```ts title="src/api/store/validators.ts" -// other imports... -import { createFindParams } from "@medusajs/medusa/api/utils/validators" +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.post, + { + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) +``` -// ... +Then, to set the custom column when creating or updating a link between records: -export type GetQuoteParamsType = z.infer; -export const GetQuoteParams = createFindParams({ - limit: 15, - offset: 0, +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: true, + }, + }, }) ``` -You create a `GetQuoteParams` schema using the `createFindParams` utility from Medusa. This utility creates a schema that allows clients to specify query parameters such as: - -- `fields`: The fields to retrieve in a quote. -- `limit`: The maximum number of quotes to retrieve. This is useful for routes that return a list of quotes. -- `offset`: The number of quotes to skip before retrieving the next set of quotes. This is useful for routes that return a list of quotes. -- `order`: The fields to sort the quotes by either in ascending or descending order. This is useful for routes that return a list of quotes. +To retrieve the custom column when retrieving linked records using Query: -Finally, you'll apply these Query configurations in a middleware. So, add the following middleware in `src/api/middlewares.ts`: +```ts +import productBlogLink from "../links/product-blog" -```ts title="src/api/store/middlewares.ts" -// other imports... -import { GetQuoteParams } from "./store/validators" -import { validateAndTransformQuery } from "@medusajs/framework/http" -import { listStoreQuoteQueryConfig } from "./store/customers/me/quotes/query-config" +// ... -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/store/customers/me/quotes*", - middlewares: [ - validateAndTransformQuery( - GetQuoteParams, - listStoreQuoteQueryConfig - ), - ], - }, - ], +const { data } = await query.graph({ + entity: productBlogLink.entryPoint, + fields: ["metadata", "product.*", "post.*"], + filters: { + product_id: "prod_123", + }, }) ``` -You apply the `validateAndTransformQuery` middleware on all routes starting with `/store/customers/me/quotes`. The `validateAndTransformQuery` middleware that Medusa provides accepts two parameters: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/custom-columns/index.html.md). -1. A Zod schema that specifies how to validate the query parameters of incoming requests. -2. A Query configuration object that specifies the default fields to retrieve in the response, which you defined in the `query-config.ts` file. +### Create Link Between Records -The create quote route is now ready to be used by clients to create quotes for customers. +To create a link between two records using Link: -### Test the API Route +```ts +import { Modules } from "@medusajs/framework/utils" +import { BLOG_MODULE } from "../../modules/blog" -To test out the API route, start the Medusa application: +// ... -```bash npm2yarn -npm run dev +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [HELLO_MODULE]: { + my_custom_id: "mc_123", + }, +}) ``` -Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. - -#### Retrieve Publishable API Key - -All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront. - -To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#create-link/index.html.md). -To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md). +### Dismiss Link Between Records -#### Retrieve Customer Authentication Token +To dismiss links between records using Link: -As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request. +```ts +import { Modules } from "@medusajs/framework/utils" +import { BLOG_MODULE } from "../../modules/blog" -Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register): +// ... -```bash -curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "customer@gmail.com", - "password": "supersecret" -}' +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [BLOG_MODULE]: { + post_id: "mc_123", + }, +}) ``` -Make sure to replace the email and password with the credentials you want. - -Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers): +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#dismiss-link/index.html.md). -```bash -curl -X POST 'http://localhost:9000/store/customers' \ --H 'Authorization: Bearer {token}' \ --H 'Content-Type: application/json' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ ---data-raw '{ - "email": "customer@gmail.com" -}' -``` +### Cascade Delete Linked Records -Make sure to replace: +To cascade delete records linked to a deleted record: -- `{token}` with the registration token you received from the previous request. -- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +```ts +import { Modules } from "@medusajs/framework/utils" -Also, if you changed the email in the first request, make sure to change it here as well. +// ... -The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider): +await productModuleService.deleteVariants([variant.id]) -```bash -curl -X POST 'http://localhost:9000/auth/customer/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "customer@gmail.com", - "password": "supersecret" -}' +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) ``` -Copy the returned token to use it in the next requests. - -#### Create Cart +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#cascade-delete-linked-records/index.html.md). -The customer needs a cart with an item before creating the quote. +### Restore Linked Records -A cart requires a region ID. You can retrieve a region ID using the [List Regions API route](https://docs.medusajs.com/api/store#regions_getregions): +To restore records that were soft-deleted because they were linked to a soft-deleted record: -```bash -curl 'http://localhost:9000/store/regions' \ --H 'x-publishable-api-key: {your_publishable_api_key}' -``` +```ts +import { Modules } from "@medusajs/framework/utils" -Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +// ... -Then, create a cart for the customer using the [Create Cart API route](https://docs.medusajs.com/api/store#carts_postcarts): +await productModuleService.restoreProducts(["prod_123"]) -```bash -curl -X POST 'http://localhost:9000/store/carts' \ --H 'Authorization: Bearer {token}' \ --H 'Content-Type: application/json' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ ---data '{ - "region_id": "{region_id}" -}' +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) ``` -Make sure to replace: - -- `{token}` with the authentication token you received from the previous request. -- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -- `{region_id}` with the region ID you retrieved from the previous request. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#restore-linked-records/index.html.md). -This will create and return a cart. Copy its ID for the next request. +*** -You now need to add a product variant to the cart. You can retrieve a product variant ID using the [List Products API route](https://docs.medusajs.com/api/store#products_getproducts): +## Query -```bash -curl 'http://localhost:9000/store/products' \ --H 'x-publishable-api-key: {your_publishable_api_key}' -``` +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +### Retrieve Records of Data Model -Copy the ID of a variant in a product from the response. +To retrieve records using Query in an API route: -Finally, to add the product variant to the cart, use the [Add Item to Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems): +```ts highlights={[["15", "graph"]]} +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -```bash -curl -X POST 'http://localhost:9000/store/carts/{id}/line-items' \ --H 'Authorization: Bearer {token}' \ --H 'Content-Type: application/json' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ ---data-raw '{ - "variant_id": "{variant_id}", - "quantity": 1, -}' -``` +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -Make sure to replace: + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) -- `{id}` with the cart ID you retrieved previously. -- `{token}` with the authentication token you retrieved previously. -- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -- `{variant_id}` with the product variant ID you retrieved in the previous request. + res.json({ my_customs: myCustoms }) +} +``` -This adds the product variant to the cart. You can now use the cart to create a quote. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -For more accurate totals and processing of the quote's draft order, you should: +### Retrieve Linked Records of Data Model -- Add shipping and billing addresses by [updating the cart](https://docs.medusajs.com/api/store#carts_postcartsid). -- [Choose a shipping method](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods) for the cart. -- [Create a payment collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections) for the cart. -- [Initialize payment session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) in the payment collection. +To retrieve records linked to a data model: -You can also learn how to build a checkout experience in a storefront by following [this storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md). It's not specific to quote management, so you'll need to change the last step to create a quote instead of an order. +```ts highlights={[["20"]]} +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -#### Create Quote +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -To create a quote for the customer, send a request to the `/store/customers/me/quotes` route you created: + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], + }) -```bash -curl -X POST 'http://localhost:9000/store/customers/me/quotes' \ --H 'Authorization: Bearer {token}' \ --H 'Content-Type: application/json' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ ---data-raw '{ - "cart_id": "{cart_id}" -}' + res.json({ my_customs: myCustoms }) +} ``` -Make sure to replace: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#retrieve-linked-records/index.html.md). -- `{token}` with the authentication token you retrieved previously. -- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. -- `{cart_id}` with the ID of the customer's cart. +### Apply Filters to Retrieved Records -This will create a quote for the customer and you'll receive its details in the response. +To filter the retrieved records: -*** +```ts highlights={[["18", "filters"]]} +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -## Step 6: List Quotes API Route +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -After the customer creates a quote, the admin user needs to view these quotes to manage them. In this step, you'll create the API route to list quotes for the admin user. Then, in the next step, you'll customize the Medusa Admin dashboard to display these quotes. + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, + }) -The process of creating this API route will be somewhat similar to the previous route you created. You'll create the route, define the query configurations, and apply them in a middleware. + res.json({ my_customs: myCustoms }) +} +``` -### Implement API Route +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-filters/index.html.md). -To create the API route, create the file `src/api/admin/quotes/route.ts` with the following content: +### Apply Pagination and Sort Records -![Directory structure after adding the admin quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741094735/Medusa%20Resources/quote-18_uvwqt6.jpg) +To paginate and sort retrieved records: -```ts title="src/api/admin/quotes/route.ts" highlights={listQuotesHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts highlights={[["21", "pagination"]]} +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" export const GET = async ( req: MedusaRequest, @@ -40063,12304 +50729,13157 @@ export const GET = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { data: quotes, metadata } = await query.graph({ - entity: "quote", - ...req.queryConfig, + const { + data: myCustoms, + metadata: { count, take, skip } = {}, + } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + skip: 0, + take: 10, + order: { + name: "DESC", + }, + }, }) - res.json({ - quotes, - count: metadata!.count, - offset: metadata!.skip, - limit: metadata!.take, + res.json({ + my_customs: myCustoms, + count, + take, + skip, }) } ``` -You export a `GET` function in this file, which exposes a `GET` API route at `/admin/quotes`. - -In the route handler function, you resolve Query from the Medusa container and use it to retrieve the list of quotes. Similar to before, you use `req.queryConfig` to specify the fields to retrieve in the response. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#sort-records/index.html.md). -`req.queryConfig` also includes pagination parameters, such as `limit`, `offset`, and `count`, and they're returned in the `metadata` property of Query's result. You return the pagination details and the list of quotes in the response. +*** -Learn more about paginating Query results in the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-pagination/index.html.md). +## Workflows -### Add Query Configurations +A workflow is a series of queries and actions that complete a task. -Similar to before, you need to specify the default fields to retrieve in a quote and apply them in a middleware for this new route. +A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more. -Since this is an admin route, create the file `src/api/admin/quotes/query-config.ts` with the following content: +### Create a Workflow -![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095492/Medusa%20Resources/quote-19_xca6aq.jpg) +To create a workflow: -```ts title="src/api/admin/quotes/query-config.ts" -export const quoteFields = [ - "id", - "status", - "created_at", - "updated_at", - "*customer", - "cart.id", - "draft_order.id", - "draft_order.currency_code", - "draft_order.display_id", - "draft_order.region_id", - "draft_order.status", - "draft_order.version", - "draft_order.summary", - "draft_order.total", - "draft_order.subtotal", - "draft_order.tax_total", - "draft_order.order_change", - "draft_order.discount_total", - "draft_order.discount_tax_total", - "draft_order.original_total", - "draft_order.original_tax_total", - "draft_order.item_total", - "draft_order.item_subtotal", - "draft_order.item_tax_total", - "draft_order.original_item_total", - "draft_order.original_item_subtotal", - "draft_order.original_item_tax_total", - "draft_order.shipping_total", - "draft_order.shipping_subtotal", - "draft_order.shipping_tax_total", - "draft_order.original_shipping_tax_total", - "draft_order.original_shipping_subtotal", - "draft_order.original_shipping_total", - "draft_order.created_at", - "draft_order.updated_at", - "*draft_order.items", - "*draft_order.items.tax_lines", - "*draft_order.items.adjustments", - "*draft_order.items.variant", - "*draft_order.items.variant.product", - "*draft_order.items.detail", - "*order_change.actions", -] +1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content: -export const retrieveAdminQuoteQueryConfig = { - defaults: quoteFields, - isList: false, -} +```ts title="src/workflows/hello-world/steps/step-1.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -export const listAdminQuoteQueryConfig = { - defaults: quoteFields, - isList: true, -} +export const step1 = createStep("step-1", async () => { + return new StepResponse(`Hello from step one!`) +}) ``` -You export two objects: `retrieveAdminQuoteQueryConfig` and `listAdminQuoteQueryConfig`, which specify the default fields to retrieve for a single quote and a list of quotes, respectively. - -For simplicity, this guide will apply the `listAdminQuoteQueryConfig` to all routes starting with `/admin/quotes`. However, you should instead apply `retrieveAdminQuoteQueryConfig` to routes that retrieve a single quote, and `listAdminQuoteQueryConfig` to routes that retrieve a list of quotes. - -Next, you'll define a Zod schema that allows client applications to specify the fields to retrieve and pagination fields as a query parameter. Create the file `src/api/admin/validators.ts` with the following content: +2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content: -![Directory structure after adding the admin validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095771/Medusa%20Resources/quote-20_iygrip.jpg) +```ts title="src/workflows/hello-world/steps/step-2.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -```ts title="src/api/admin/validators.ts" -import { - createFindParams, -} from "@medusajs/medusa/api/utils/validators" +type StepInput = { + name: string +} -export const AdminGetQuoteParams = createFindParams({ - limit: 15, - offset: 0, -}) - .strict() +export const step2 = createStep( + "step-2", + async ({ name }: StepInput) => { + return new StepResponse(`Hello ${name} from step two!`) + } +) ``` -You define the `AdminGetQuoteParams` schema using the `createFindParams` utility from Medusa. The schema allows clients to specify query parameters such as: +3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content: -- `fields`: The fields to retrieve in a quote. -- `limit`: The maximum number of quotes to retrieve. -- `offset`: The number of quotes to skip before retrieving the next set of quotes. -- `order`: The fields to sort the quotes by either in ascending or descending order. +```ts title="src/workflows/hello-world/index.ts" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { step1 } from "./steps/step-1" +import { step2 } from "./steps/step-2" -Finally, you need to apply the `validateAndTransformQuery` middleware on this route. So, add the following to `src/api/middlewares.ts`: +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1() + // to pass input + const str2 = step2(input) -```ts title="src/api/middlewares.ts" -// other imports... -import { AdminGetQuoteParams } from "./admin/quotes/validators" -import { listAdminQuoteQueryConfig } from "./admin/quotes/query-config" + return new WorkflowResponse({ + message: str1, + }) + } +) -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/quotes*", - middlewares: [ - validateAndTransformQuery( - AdminGetQuoteParams, - listAdminQuoteQueryConfig - ), - ], - }, - ], -}) +export default myWorkflow ``` -You add the `validateAndTransformQuery` middleware to all routes starting with `/admin/quotes`. It validates the query parameters and sets the Query configurations based on the defaults you defined and the passed query parameters. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -Your API route is now ready for use. You'll test it in the next step by customizing the Medusa Admin dashboard to display the quotes. +### Execute a Workflow -*** +### API Route -## Step 7: List Quotes Route in Medusa Admin +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../workflows/hello-world" -Now that you have the API route to retrieve the list of quotes, you want to show these quotes to the admin user in the Medusa Admin dashboard. The Medusa Admin is customizable, allowing you to add new pages as UI routes. +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run({ + input: { + name: req.query.name as string, + }, + }) -A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of quotes in the Medusa Admin. + res.send(result) +} +``` -Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). +### Subscriber -### Configure JS SDK +```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import myWorkflow from "../workflows/hello-world" +import { Modules } from "@medusajs/framework/utils" +import { IUserModuleService } from "@medusajs/framework/types" -Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations. +export default async function handleCustomerCreate({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const userId = data.id + const userModuleService: IUserModuleService = container.resolve( + Modules.USER + ) -The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content: + const user = await userModuleService.retrieveUser(userId) -![Directory structure after adding the sdk file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098137/Medusa%20Resources/quote-23_plm90s.jpg) + const { result } = await myWorkflow(container) + .run({ + input: { + name: user.first_name, + }, + }) -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" + console.log(result) +} -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) +export const config: SubscriberConfig = { + event: "user.created", +} ``` -You create an instance of the JS SDK using the `Medusa` class from the `@medusajs/js-sdk` package. You pass it an object having the following properties: +### Scheduled Job -- `baseUrl`: The base URL of the Medusa server. -- `debug`: A boolean indicating whether to log debug information. -- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type. +```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import myWorkflow from "../workflows/hello-world" -### Add Admin Types +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) -In your development, you'll need types that represents the data you'll retrieve from the Medusa server. So, create the file `src/admin/types.ts` with the following content: + console.log(result.message) +} -![Directory structure after adding the admin type](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098478/Medusa%20Resources/quote-25_jr79pa.jpg) +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +}; +``` -```ts title="src/admin/types.ts" -import { - AdminCustomer, - AdminOrder, - AdminUser, - FindParams, - PaginatedResponse, - StoreCart, -} from "@medusajs/framework/types" +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md). -export type AdminQuote = { - id: string; - status: string; - draft_order_id: string; - order_change_id: string; - cart_id: string; - customer_id: string; - created_at: string; - updated_at: string; - draft_order: AdminOrder; - cart: StoreCart; - customer: AdminCustomer -}; +### Step with a Compensation Function -export interface QuoteQueryParams extends FindParams {} +Pass a compensation function that undoes what a step did as a second parameter to `createStep`: -export type AdminQuotesResponse = PaginatedResponse<{ - quotes: AdminQuote[]; -}> +```ts highlights={[["15"]]} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -export type AdminQuoteResponse = { - quote: AdminQuote; -}; -``` +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` -You define the following types: + console.log(message) -- `AdminQuote`: Represents a quote. -- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving qoutes. -- `AdminQuotesResponse`: Represents the response when retrieving a list of quotes. -- `AdminQuoteResponse`: Represents the response when retrieving a single quote, which you'll implement later in this guide. + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") + } +) +``` -You'll use these types in the rest of the customizations. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md). -### Create useQuotes Hook +### Manipulate Variables in Workflow -When sending requests to the Medusa server from your admin customizations, it's recommended to use [Tanstack Query](https://tanstack.com/query/latest), allowing you to benefit from its caching and data fetching capabilities. +To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK: -So, you'll create a `useQuotes` hook that uses Tanstack Query and the JS SDK to fetch the list of quotes from the Medusa server. +```ts highlights={[["14", "transform"]]} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... -Create the file `src/admin/hooks/quotes.tsx` with the following content: +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) -![Directory structure after adding the hooks quotes file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098244/Medusa%20Resources/quote-24_apdpem.jpg) + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) -```ts title="src/admin/hooks/quotes.tsx" -import { ClientHeaders, FetchError } from "@medusajs/js-sdk" -import { - QuoteQueryParams, - AdminQuotesResponse, -} from "../types" -import { - QueryKey, - useQuery, - UseQueryOptions, -} from "@tanstack/react-query" -import { sdk } from "../lib/sdk" + return new WorkflowResponse(str3) + } +) +``` -export const useQuotes = ( - query: QuoteQueryParams, - options?: UseQueryOptions< - AdminQuotesResponse, - FetchError, - AdminQuotesResponse, - QueryKey - > -) => { - const fetchQuotes = (query: QuoteQueryParams, headers?: ClientHeaders) => - sdk.client.fetch(`/admin/quotes`, { - query, - headers, - }) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) - const { data, ...rest } = useQuery({ - ...options, - queryFn: () => fetchQuotes(query)!, - queryKey: ["quote", "list"], - }) +### Using Conditions in Workflow - return { ...data, ...rest } -} -``` +To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK: -You define a `useQuotes` hook that accepts query parameters and optional options as a parameter. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes` route. +```ts highlights={[["14", "when"]]} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... -You return the fetched data from the Medusa server. You'll use this hook in the UI route. +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -### Create Quotes UI Route + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) -You can now create the UI route that will show a new page in the Medusa Admin with the list of quotes. + // executed without condition + const anotherStepResult = anotherStep(result) -UI routes are created in a `page.tsx` file under the `src/admin/routes` directory. The path of the UI route is the file's path relative to `src/admin/routes`. + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` -So, to add the UI route at `/quotes` in the Medusa Admin, create the file `src/admin/routes/quotes/page.tsx` with the following content: +### Run Workflow in Another -![Directory structure after adding the Quotes UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099122/Medusa%20Resources/quote-26_qrqzut.jpg) +To run a workflow in another, use the workflow's `runAsStep` special method: -```tsx title="src/admin/routes/quotes/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { DocumentText } from "@medusajs/icons" +```ts highlights={[["11", "runAsStep"]]} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" import { - Container, createDataTableColumnHelper, DataTable, - DataTablePaginationState, Heading, Toaster, useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -import { useQuotes } from "../../hooks/quotes" -import { AdminQuote } from "../../types" -import { useState } from "react" - -const Quotes = () => { - // TODO implement page content -} + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -export const config = defineRouteConfig({ - label: "Quotes", - icon: DocumentText, -}) +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) -export default Quotes + // ... + } +) ``` -The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuation object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar. - -In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This componet requires you first define the columns of the table. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/execute-another-workflow/index.html.md). -To define the table's columns, add in the same file and before the `Quotes` component the following: +### Consume a Workflow Hook -```tsx title="src/admin/routes/quotes/page.tsx" -const StatusTitles: Record = { - accepted: "Accepted", - customer_rejected: "Customer Rejected", - merchant_rejected: "Merchant Rejected", - pending_merchant: "Pending Merchant", - pending_customer: "Pending Customer", -} +To consume a workflow hook, create a file under `src/workflows/hooks`: -const columnHelper = createDataTableColumnHelper() +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -const columns = [ - columnHelper.accessor("draft_order.display_id", { - header: "ID", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => StatusTitles[getValue()], - }), - columnHelper.accessor("customer.email", { - header: "Email", - }), - columnHelper.accessor("draft_order.customer.first_name", { - header: "First Name", - }), - columnHelper.accessor("draft_order.customer.company_name", { - header: "Company Name", - }), - columnHelper.accessor("draft_order.total", { - header: "Total", - cell: ({ getValue, row }) => - `${row.original.draft_order.currency_code.toUpperCase()} ${getValue()}`, - }), - columnHelper.accessor("created_at", { - header: "Created At", - cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(), - }), -] +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action + }, + async (dataFromStep, { container }) => { + // undo the performed action + } +) ``` -You use the `createDataTableColumnHelper` utility to create a function that allows you to define the columns of the table. Then, you create a `columns` array variable that defines the following columns: +This executes a custom step at the hook's designated point in the workflow. + +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). + +### Expose a Hook -1. `ID`: The display ID of the quote's draft order. -2. `Status`: The status of the quote. Here, you use an object to map the status to a human-readable title. - - The `cell` property of the second object passed to the `columnHelper.accessor` function allows you to customize how the cell is rendered. -3. `Email`: The email of the customer. -4. `First Name`: The first name of the customer. -5. `Company Name`: The company name of the customer. -6. `Total`: The total amount of the quote's draft order. You format it to include the currency code. -7. `Created At`: The date the quote was created. +To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`: -Next, you'll use these columns to render the `DataTable` component in the `Quotes` component. +```ts highlights={[["19", "hooks"]]} +import { + createStep, + createHook, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createProductStep } from "./steps/create-product" -Change the implementation of `Quotes` to the following: +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } + ) -```tsx title="src/admin/routes/quotes/page.tsx" -const Quotes = () => { - const navigate = useNavigate() - const [pagination, setPagination] = useState({ - pageSize: 15, - pageIndex: 0, - }) + return new WorkflowResponse(product, { + hooks: [productCreatedHook], + }) + } +) +``` - const { - quotes = [], - count, - isPending, - } = useQuotes({ - limit: pagination.pageSize, - offset: pagination.pageIndex * pagination.pageSize, - fields: - "+draft_order.total,*draft_order.customer", - order: "-created_at", - }) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/add-workflow-hook/index.html.md). - const table = useDataTable({ - columns, - data: quotes, - getRowId: (quote) => quote.id, - rowCount: count, - isLoading: isPending, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - onRowClick(event, row) { - navigate(`/quotes/${row.id}`) - }, - }) +### Retry Steps +To configure steps to retry in case of errors, pass the `maxRetries` step option: - return ( - <> - - - Quotes - +```ts highlights={[["10"]]} +import { + createStep, +} from "@medusajs/framework/workflows-sdk" - - - Products - - - - - - - - ) -} +export const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") + + throw new Error("Oops! Something happened.") + } +) ``` -In the component, you use the `useQuotes` hook to fetch the quotes from the Medusa server. You pass the following query parameters in the request: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/retry-failed-steps/index.html.md). -- `limit` and `offset`: Pagination fields to specify the current page and the number of quotes to retrieve. These are based on the `pagination` state variable, which will be managed by the `DataTable` component. -- `fields`: The fields to retrieve in the response. You specify the total amount of the draft order and the customer of the draft order. Since you prefix the fields with `+` and `*`, the fields are retrieved along with the default fields specified in the Query configurations. -- `order`: The order in which to retrieve the quotes. Here, you retrieve the quotes in descending order of their creation date. +### Run Steps in Parallel -Next, you use the `useDataTable` hook to create a table instance with the columns you defined. You pass the fetched quotes to the `DataTable` component, along with configurations related to pagination and loading. +If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK: -Notice that as part of the `useDataTable` configurations you naviagte to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step. +```ts highlights={[["22", "parallelize"]]} +import { + createWorkflow, + WorkflowResponse, + parallelize, +} from "@medusajs/framework/workflows-sdk" +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" -Finally, you render the `DataTable` component to display the quotes in a table. +interface WorkflowInput { + title: string +} -### Test List Quotes UI Route +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) -You can now test out the UI route and the route added in the previous section from the Medusa Admin. + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) -First, start the Medusa application: + const id = product.id + const refetchedProduct = getProductStep(product.id) -```bash npm2yarn -npm run dev + return new WorkflowResponse(refetchedProduct) + } +) ``` -Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. - -You'll find a "Quotes" sidebar item. If you click on it, it will show you the table of quotes. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/parallel-steps/index.html.md). -![Quotes table in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099952/Medusa%20Resources/Screenshot_2025-03-04_at_4.52.17_PM_nqxyfq.png) +### Configure Workflow Timeout -*** +To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration: -## Step 8: Retrieve Quote API Route +```ts highlights={[["10"]]} +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +// step import... -Next, you'll add an admin API route to retrieve a single quote. You'll use this route in the next step to add a UI route to view a quote's details. You'll later expand on that UI route to allow the admin to manage the quote. +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() -To add the API route, create the file `src/api/admin/quotes/[id]/route.ts` with the following content: + return new WorkflowResponse({ + message: str1, + }) +}) -![Directory structure after adding the single quote route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741100686/Medusa%20Resources/quote-27_ugvhbb.jpg) +export default myWorkflow +``` -```ts title="src/api/admin/quotes/[id]/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout/index.html.md). -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { id } = req.params +### Configure Step Timeout - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, - }, - { throwIfKeyNotFound: true } - ) +To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property: - res.json({ quote }) -} +```ts highlights={[["4"]]} +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) ``` -You export a `GET` route handler, which will create a `GET` API route at `/admin/quotes/:id`. - -In the route handler, you resolve Query and use it to retrieve the quote. You pass the ID in the path parameter as a filter in Query. You also pass the query configuration fields, which are the same as the ones you've configured before, to retrieve the default fields and the fields specified in the query parameter. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout/index.html.md). -Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well. +### Long-Running Workflow -You'll test this route in the next step as you create the UI route for a single quote. +A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs. -*** +To create a long-running workflow, configure any of its steps to be `async` without returning any data: -## Step 9: Quote Details UI Route +```ts highlights={[["4"]]} +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") + } +) +``` -In the Quotes List UI route, you configured the data table to navigate to a quote's page when you click on it in the table. Now that you have the API route to retrieve a single quote, you'll create the UI route that shows a quote's details. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md). -![Preview of the quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png) +### Change Step Status in Long-Running Workflow -Before you create the UI route, you need to create the hooks necessary to retrieve data from the Medusa server, and some components that will show the different elements of the page. +To change a step's status: -### Add Hooks +1. Grab the workflow's transaction ID when you run it: -The first hook you'll add is a hook that will retrieve a quote using the API route you added in the previous step. +```ts +const { transaction } = await myLongRunningWorkflow(req.scope) + .run() +``` -In `src/admin/hooks/quote.tsx`, add the following: +2. In an API route, workflow, or other resource, change a step's status to successful using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md): -```tsx title="src/admin/hooks/quote.tsx" -// other imports... -import { AdminQuoteResponse } from "../types" +```ts highlights={stepSuccessHighlights} +const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE +) -// ... +await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, +}) +``` -export const useQuote = ( - id: string, - query?: QuoteQueryParams, - options?: UseQueryOptions< - AdminQuoteResponse, - FetchError, - AdminQuoteResponse, - QueryKey - > -) => { - const fetchQuote = ( - id: string, - query?: QuoteQueryParams, - headers?: ClientHeaders - ) => - sdk.client.fetch(`/admin/quotes/${id}`, { - query, - headers, - }) +3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md): - const { data, ...rest } = useQuery({ - queryFn: () => fetchQuote(id, query), - queryKey: ["quote", id], - ...options, - }) +```ts highlights={stepFailureHighlights} +const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE +) - return { ...data, ...rest } -} +await workflowEngineService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, +}) ``` -You define a `useQuote` hook that accepts the quote's ID and optional query parameters and options as parameters. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes/:id` route. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md). -The hook returns the fetched data from the Medusa server. You'll use this hook later in the UI route. +### Access Long-Running Workflow's Result -In addition, you'll need a hook to retrieve a preview of the quote's draft order. An order preview includes changes or edits to be applied on an order's items, such as changes in prices and quantities. Medusa already provides a [Get Order Preview API route](https://docs.medusajs.com/api/admin#orders_getordersidpreview) that you can use to retrieve the preview. +Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow. -To create the hook, create the file `src/admin/hooks/order-preview.tsx` with the following content: +For example, in an API route: -![Directory structure after adding the order preview hook file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157692/Medusa%20Resources/quote-32_tb1tqw.jpg) +```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" +import { Modules } from "@medusajs/framework/utils" -```tsx title="src/admin/hooks/order-preview.tsx" -import { HttpTypes } from "@medusajs/framework/types" -import { FetchError } from "@medusajs/js-sdk" -import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query" -import { sdk } from "../lib/sdk" +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { transaction, result } = await myWorkflow(req.scope).run() -export const orderPreviewQueryKey = "custom_orders" + const workflowEngineService = req.scope.resolve( + Modules.WORKFLOW_ENGINE + ) -export const useOrderPreview = ( - id: string, - query?: HttpTypes.AdminOrderFilters, - options?: Omit< - UseQueryOptions< - HttpTypes.AdminOrderPreviewResponse, - FetchError, - HttpTypes.AdminOrderPreviewResponse, - QueryKey - >, - "queryFn" | "queryKey" - > -) => { - const { data, ...rest } = useQuery({ - queryFn: async () => sdk.admin.order.retrievePreview(id, query), - queryKey: [orderPreviewQueryKey, id], - ...options, + const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", + } + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, }) - return { ...data, ...rest } + res.send(result) } ``` -You add a `useOrderPreview` hook that accepts as parameters the order's ID, query parameters, and options. In the hook, you use the JS SDK's `admin.order.retrievePreview` method to retrieve the order preview and return it. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). -You'll use this hook later in the quote's details page. +*** -### Add formatAmount Utility +## Subscribers -In the quote's details page, you'll display the amounts of the items in the quote. To format the amounts, you'll create a utility function that formats the amount based on the currency code. +A subscriber is a function executed whenever the event it listens to is emitted. -Create the file `src/admin/utils/format-amount.ts` with the following content: +### Create a Subscriber -![Directory structure after adding the format amount utility file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157986/Medusa%20Resources/quote-33_k5sa9q.jpg) +To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content: -```ts title="src/admin/utils/format-amount.ts" -export const formatAmount = (amount: number, currency_code: string) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: currency_code, - }).format(amount) +```ts title="src/subscribers/product-created.ts" +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", } ``` -You define a `formatAmount` function that accepts an amount and a currency code as parameters. The function uses the `Intl.NumberFormat` API to format the amount as a currency based on the currency code. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). -You'll use this function in the UI route and its components. +### Resolve Resources in Subscriber -### Create Amount Component +To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter: -In the quote's details page, you want to display changes in amounts for items and totals. This is useful as you later add the capability to edit the price and quantity of items. +```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]} +import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" -![Diagram showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183186/Medusa%20Resources/amount-highlighted_havznm.png) +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const productModuleService = container.resolve(Modules.PRODUCT) -To display changes in an amount, you'll create an `Amount` component and re-use it where necessary. So, create the file `src/admin/components/amount.tsx` with the following content: + const productId = data.id -![Directory structure after adding the amount component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741101819/Medusa%20Resources/quote-28_iwukg2.jpg) + const product = await productModuleService.retrieveProduct( + productId + ) -```tsx title="src/admin/components/amount.tsx" -import { clx } from "@medusajs/ui" -import { formatAmount } from "../utils/format-amount" + console.log(`The product ${product.title} was created`) +} -type AmountProps = { - currencyCode: string; - amount?: number | null; - originalAmount?: number | null; - align?: "left" | "right"; - className?: string; -}; +export const config: SubscriberConfig = { + event: `product.created`, +} +``` -export const Amount = ({ - currencyCode, - amount, - originalAmount, - align = "left", - className, -}: AmountProps) => { - if (typeof amount === "undefined" || amount === null) { - return ( -
- - -
- ) - } +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers#resolve-resources/index.html.md). - const formatted = formatAmount(amount, currencyCode) - const originalAmountPresent = typeof originalAmount === "number" - const originalAmountDiffers = originalAmount !== amount - const shouldShowAmountDiff = originalAmountPresent && originalAmountDiffers +### Send a Notification to Reset Password - return ( -
- {shouldShowAmountDiff ? ( - <> - - {formatAmount(originalAmount!, currencyCode)} - - {formatted} - - ) : ( - <> - {formatted} - - )} -
+To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content: + +```ts title="src/subscribers/handle-reset.ts" +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" + + 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}`, + }, + }) } -``` -In this component, you show the current amount of an item and, if it has been changed, you show previous amount as well. +export const config: SubscriberConfig = { + event: "auth.password_reset", +} +``` -You'll use this component in other components whenever you want to display any amount that can be changed. +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md). -### Create QuoteItems Component +### Execute a Workflow in a Subscriber -In the quote's UI route, you want to display the details of the items in the quote. You'll create a separate component that you'll use within the UI route. +To execute a workflow in a subscriber: -![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183303/Medusa%20Resources/item-highlighted-cropped_ddyikt.png) +```ts +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import myWorkflow from "../workflows/hello-world" +import { Modules } from "@medusajs/framework/utils" -Create the file `src/admin/components/quote-items.tsx` with the following content: +export default async function handleCustomerCreate({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const userId = data.id + const userModuleService = container.resolve( + Modules.USER + ) -![Directory structure after adding the quote items component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741102170/Medusa%20Resources/quote-29_r5ljph.jpg) + const user = await userModuleService.retrieveUser(userId) -```tsx title="src/admin/components/quote-items.tsx" -import { - AdminOrder, - AdminOrderLineItem, - AdminOrderPreview, -} from "@medusajs/framework/types" -import { Badge, Text } from "@medusajs/ui" -import { useMemo } from "react" -import { Amount } from "./amount-cell" + const { result } = await myWorkflow(container) + .run({ + input: { + name: user.first_name, + }, + }) -export const QuoteItem = ({ - item, - originalItem, - currencyCode, -}: { - item: AdminOrderPreview["items"][0]; - originalItem?: AdminOrderLineItem; - currencyCode: string; -}) => { + console.log(result) +} - const isItemUpdated = useMemo( - () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"), - [item] - ) +export const config: SubscriberConfig = { + event: "user.created", +} +``` - return ( -
-
-
- - {item.title} - +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) - {item.variant_sku && ( -
- {item.variant_sku} -
- )} - - {item.variant?.options?.map((o) => o.value).join(" · ")} - -
-
+*** -
-
- -
+## Scheduled Jobs -
-
- - {item.quantity}x - -
+A scheduled job is a function executed at a specified interval of time in the background of your Medusa application. -
+### Create a Scheduled Job - {isItemUpdated && ( - - Modified - - )} -
+To create a scheduled job, create the file `src/jobs/hello-world.ts` with the following content: -
-
+```ts title="src/jobs/hello-world.ts" +// the scheduled-job function +export default function () { + console.log("Time to say hello world!") +} - -
-
- ) +// the job's configurations +export const config = { + name: "every-minute-message", + // execute every minute + schedule: "* * * * *", } ``` -You first define the component for one quote item. In the component, you show the item's title, variant SKU, and quantity. You also use the `Amount` component to show the item's current and previous amounts. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). -Next, add to the same file the `QuoteItems` component: +### Resolve Resources in Scheduled Job -```tsx title="src/admin/components/quote-items.tsx" -export const QuoteItems = ({ - order, - preview, -}: { - order: AdminOrder; - preview: AdminOrderPreview; -}) => { - const itemsMap = useMemo(() => { - return new Map(order.items.map((item) => [item.id, item])) - }, [order]) +To resolve resources in a scheduled job, use the `container` accepted as a first parameter: - return ( -
- {preview.items?.map((item) => { - return ( - - ) - })} -
+```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const productModuleService = container.resolve(Modules.PRODUCT) + + const [, count] = await productModuleService.listAndCountProducts() + + console.log( + `Time to check products! You have ${count} product(s)` ) } + +export const config = { + name: "every-minute-message", + // execute every minute + schedule: "* * * * *", +} ``` -In this component, you loop over the order's items and show each of them using the `QuoteItem` component. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs#resolve-resources/index.html.md) -### Create TotalsBreakdown Component +### Specify a Job's Execution Number -Another component you'll need in the quote's UI route is a component that breaks down the totals of the quote's draft order, such as its discount or shipping totals. +To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration: -![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183481/Medusa%20Resources/totals-highlighted_hpxier.png) +```ts highlights={[["9", "numberOfExecutions"]]} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} -Create the file `src/admin/components/totals-breakdown.tsx` with the following content: +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` -![Directory structure after adding the totals breakdown component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741155757/Medusa%20Resources/quote-30_de0kjq.jpg) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/execution-number/index.html.md). -```tsx title="src/admin/components/totals-breakdown.tsx" -import { AdminOrder } from "@medusajs/framework/types" -import { Text } from "@medusajs/ui" -import { ReactNode } from "react" -import { formatAmount } from "../utils/format-amount" +### Execute a Workflow in a Scheduled Job -export const Total = ({ - label, - value, - secondaryValue, - tooltip, -}: { - label: string; - value: string | number; - secondaryValue: string; - tooltip?: ReactNode; -}) => ( -
- - {label} {tooltip} - -
- - {secondaryValue} - -
+To execute a workflow in a scheduled job: -
- - {value} - -
-
-) -``` +```ts +import { MedusaContainer } from "@medusajs/framework/types" +import myWorkflow from "../workflows/hello-world" -You first define the `Total` component, which breaksdown a total item, such as discount. You'll use this component to breakdown the different totals in the `TotalsBreakdown` component. +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) -Add the `TotalsBreakdown` component after the `Total` component: + console.log(result.message) +} -```tsx title="src/admin/components/totals-breakdown.tsx" -export const TotalsBreakdown = ({ order }: { order: AdminOrder }) => { - return ( -
- 0 - ? `- ${formatAmount(order.discount_total, order.currency_code)}` - : "-" - } - /> - {(order.shipping_methods || []) - .sort((m1, m2) => - (m1.created_at as string).localeCompare(m2.created_at as string) - ) - .map((sm, i) => { - return ( -
- -
- ) - })} -
- ) +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, } ``` -In this component, you show the different totals of the quote's draft order, such as discounts and shipping totals. You use the `Total` component to show each total item. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) -### Create Quote Details UI Route +*** -You can now create the UI route that will show a quote's details in the Medusa Admin. +## Loaders -Create the file `src/admin/routes/quote/[id]/page.tsx` with the following content: +A loader is a function defined in a module that's executed when the Medusa application starts. -![Diagram showcasing the directory structure after adding the quote details UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157385/Medusa%20Resources/quote-31_grwlon.jpg) +### Create a Loader -```tsx title="src/admin/routes/quote/[id]/page.tsx" highlights={quotesDetailsHighlights} -import { CheckCircleSolid } from "@medusajs/icons" -import { - Button, - Container, - Heading, - Text, - Toaster, -} from "@medusajs/ui" -import { Link, useNavigate, useParams } from "react-router-dom" -import { useOrderPreview } from "../../../hooks/order-preview" -import { - useQuote, -} from "../../../hooks/quotes" -import { QuoteItems } from "../../../components/quote-items" -import { TotalsBreakdown } from "../../../components/totals-breakdown" -import { formatAmount } from "../../../utils/format-amount" +To create a loader, add it to a module's `loaders` directory. -const QuoteDetails = () => { - const { id } = useParams() - const navigate = useNavigate() - const { quote, isLoading } = useQuote(id!, { - fields: - "*draft_order.customer", - }) +For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content: - const { order: preview, isLoading: isPreviewLoading } = useOrderPreview( - quote?.draft_order_id!, - {}, - { enabled: !!quote?.draft_order_id } +```ts title="src/modules/hello/loaders/hello-world.ts" +export default async function helloWorldLoader() { + console.log( + "[HELLO MODULE] Just started the Medusa application!" ) +} +``` - if (isLoading || !quote) { - return <> - } +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). - if (isPreviewLoading) { - return <> - } +### Resolve Resources in Loader - if (!isPreviewLoading && !preview) { - throw "preview not found" - } +To resolve resources in a loader, use the `container` property of its first parameter: - // TODO render content -} +```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -export default QuoteDetails -``` +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -The `QuoteDetails` component will render the content of the quote's details page. So far, you retrieve the quote and its preview using the hooks you created earlier. You also render empty components or an error message if the data is still loading or not found. + logger.info("[helloWorldLoader]: Hello, World!") +} +``` -To add the rendered content, replace the `TODO` with the following: +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). -```tsx title="src/admin/routes/quote/[id]/page.tsx" -return ( -
-
-
- {quote.status === "accepted" && ( - -
- - - Quote accepted by customer. Order is ready for processing. - +### Access Module Options - -
-
- )} +To access a module's options in its loader, use the `options` property of its first parameter: - -
- Quote Summary -
- - -
-
- - Original Total - - - {formatAmount(quote.draft_order.total, quote.draft_order.currency_code)} - -
- -
- - Quote Total - - - {formatAmount(preview!.summary.current_order_total, quote.draft_order.currency_code)} - -
-
+```ts highlights={[["11", "options"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" - {/* TODO add actions later */} -
+// recommended to define type in another file +type ModuleOptions = { + apiKey?: boolean +} -
+export default async function helloWorldLoader({ + options, +}: LoaderOptions) { + + console.log( + "[HELLO MODULE] Just started the Medusa application!", + options + ) +} +``` -
- -
- Customer -
+Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md). -
- - Email - +### Register Resources in the Module's Container - e.stopPropagation()} - > - {quote.draft_order?.customer?.email} - -
-
-
-
+To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method: - -
-) +```ts highlights={[["9", "registerAdd"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { asValue } from "awilix" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + container.registerAdd( + "custom_data", + asValue({ + test: true, + }) + ) +} ``` -You first check if the quote has been accepted by the customer, and show a banner to view the created order if so. +Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register. -Next, you use the `QuoteItems` and `TotalsBreakdown` components that you created to show the quote's items and totals. You also show the original and current totals of the quote, where the original total is the total of the draft order before any changes are made to its items. +*** -Finally, you show the customer's email and a link to view their details. +## Admin Customizations -### Test Quote Details UI Route +You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes. -To test the quote details UI route, start the Medusa application: +For a list of components to use in the admin dashboard, refere to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/index.html.md). -```bash npm2yarn -npm run dev -``` +### Create Widget -Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. +A widget is a React component that can be injected into an existing page in the admin dashboard. -Next, click on Quotes in the sidebar, which will open the list of quotes UI route you created earlier. Click on one of the quotes to view its details page. +To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content: -On the quote's details page, you can see the quote's items, its totals, and the customer's details. In the next steps, you'll add management features to the page. +```tsx title="src/admin/widgets/products-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -![Quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png) +const ProductWidget = () => { + return ( + +
+ Product Widget +
+
+ ) +} -*** +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) -## Step 10: Add Merchant Reject Quote Feature +export default ProductWidget +``` -After the merchant or admin views the quote, they can choose to either reject it, send the quote back to the customer to review it, or make changes to the quote's prices and quantities. +Learn more about widgets in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md). -In this step, you'll implement the functionality to reject a quote from the quote's details page. This will include: +### Receive Details Props in Widgets -1. Implementing the workflow to reject a quote. -2. Adding the API route to reject a quote that uses the workflow. -3. Add a hook in admin customizations that sends a request to the reject quote API route. -4. Add a button to reject the quote in the quote's details page. +Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product): -### Implement Merchant Reject Quote Workflow +```tsx highlights={[["10", "data"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" -To reject a quote, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps: +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. -- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. -- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`merchant\_rejected\`. +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -As mentioned before, the `useQueryGraphStep` is provided by Medusa's `@medusajs/medusa/core-flows` package. So, you'll only implement the remaining steps. +export default ProductWidget +``` -#### validateQuoteNotAccepted +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets#detail-widget-props/index.html.md). -The second step of the merchant rejection workflow ensures that a quote isn't already accepted, as it can't be rejected afterwards. +### Create a UI Route -To create the step, create the file `src/workflows/steps/validate-quote-not-accepted.ts` with the following content: +A UI route is a React Component that adds a new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page. -![Diagram showcasing the directory structure after adding the validate quote rejection step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159537/Medusa%20Resources/quote-34_mtcxwa.jpg) +To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content: -```ts title="src/workflows/steps/validate-quote-not-accepted.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Quote, QuoteStatus } from "../../modules/quote/models/quote" +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container, Heading } from "@medusajs/ui" -type StepInput = { - quote: InferTypeOf +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) } -export const validateQuoteNotAccepted = createStep( - "validate-quote-not-accepted", - async function ({ quote }: StepInput) { - if (quote.status === QuoteStatus.ACCEPTED) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Quote is already accepted by customer` - ) - } - } -) -``` - -You create a step that accepts a quote as an input and throws an error if the quote's status is `accepted`, as you can't reject a quote that has been accepted by the customer. - -#### updateQuoteStatusStep - -In the last step of the workflow, you'll change the workflow's status to `merchant_rejected`. So, you'll create a step that can be used to update a quote's status. +export const config = defineRouteConfig({ + label: "Custom Route", + icon: ChatBubbleLeftRight, +}) -Create the file `src/workflows/steps/update-quotes.ts` with the following content: +export default CustomPage +``` -![Diagram showcasing the directory structure after adding the update quotes step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159754/Medusa%20Resources/quote-35_moaulz.jpg) +This adds a new page at `localhost:9000/app/custom`. -```ts title="src/workflows/steps/update-quotes.ts" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" -import { QUOTE_MODULE } from "../../modules/quote" -import { QuoteStatus } from "../../modules/quote/models/quote" -import QuoteModuleService from "../../modules/quote/service" +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). -type StepInput = { - id: string; - status?: QuoteStatus; -}[] +### Create Settings Page -export const updateQuotesStep = createStep( - "update-quotes", - async (data: StepInput, { container }) => { - const quoteModuleService: QuoteModuleService = container.resolve( - QUOTE_MODULE - ) +To create a settings page, create a UI route under the `src/admin/routes/settings` directory. - const dataBeforeUpdate = await quoteModuleService.listQuotes( - { id: data.map((d) => d.id) } - ) +For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content: - const updatedQuotes = await quoteModuleService.updateQuotes(data) +```tsx title="src/admin/routes/settings/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" - return new StepResponse(updatedQuotes, { - dataBeforeUpdate, - }) - }, - async (revertInput, { container }) => { - if (!revertInput) { - return - } +const CustomSettingPage = () => { + return ( + +
+ Custom Setting Page +
+
+ ) +} - const quoteModuleService: QuoteModuleService = container.resolve( - QUOTE_MODULE - ) +export const config = defineRouteConfig({ + label: "Custom", +}) - await quoteModuleService.updateQuotes( - revertInput.dataBeforeUpdate - ) - } -) +export default CustomSettingPage ``` -This step accepts an array of quotes to update their status. In the step function, you resolve the Quote Module's service. Then, you retrieve the quotes' original data so that you can pass them to the compensation function. Finally, you update the quotes' data and return the updated quotes. - -In the compensation function, you resolve the Quote Module's service and update the quotes with their original data. +This adds a setting page at `localhost:9000/app/settings/custom`. -#### Implement Workflow +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md) -You can now implement the merchant-rejection workflow. Create the file `src/workflows/merchant-reject-quote.ts` with the following content: +### Accept Path Parameters in UI Routes -![Diagram showcasing the directory structure after adding the merchant reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159969/Medusa%20Resources/quote-36_l1ffxm.jpg) +To accept a path parameter in a UI route, name one of the directories in its path in the format `[param]`. -```ts title="src/workflows/merchant-reject-quote.ts" highlights={merchantRejectionWorkflowHighlights} -import { useQueryGraphStep } from "@medusajs/core-flows" -import { createWorkflow } from "@medusajs/workflows-sdk" -import { QuoteStatus } from "../modules/quote/models/quote" -import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" -import { updateQuotesStep } from "./steps/update-quotes" +For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: -type WorkflowInput = { - quote_id: string; -} +```tsx title="src/admin/routes/custom/[id]/page.tsx" +import { useParams } from "react-router-dom" +import { Container } from "@medusajs/ui" -export const merchantRejectQuoteWorkflow = createWorkflow( - "merchant-reject-quote-workflow", - (input: WorkflowInput) => { - const { data: quotes } = useQueryGraphStep({ - entity: "quote", - fields: ["id", "status"], - filters: { id: input.quote_id }, - options: { - throwIfKeyNotFound: true, - }, - }) +const CustomPage = () => { + const { id } = useParams() - validateQuoteNotAccepted({ - // @ts-ignore - quote: quotes[0], - }) + return ( + +
+ Passed ID: {id} +
+
+ ) +} - updateQuotesStep([ - { - id: input.quote_id, - status: QuoteStatus.MERCHANT_REJECTED, - }, - ]) - } -) +export default CustomPage ``` -You create a workflow that accepts the ID of a quote to reject. In the workflow, you: - -1. Use the `useQueryGraphStep` to retrieve the quote's details. -2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted`. -3. Update the quote's status to `merchant_rejected` using the `updateQuotesStep`. +This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter. -You'll use this workflow next in an API route that allows a merchant to reject a quote. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#path-parameters/index.html.md) -### Add Admin Reject Quote API Route +### Send Request to API Route -You'll now add the API route that allows a merchant to reject a quote. The route will use the `merchantRejectQuoteWorkflow` you created in the previous step. +To send a request to custom API routes from the admin dashboard, use the Fetch API. -Create the file `src/api/admin/quotes/[id]/reject/route.ts` with the following content: +For example: -![Diagram showcasing the directory structure after adding the reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741160251/Medusa%20Resources/quote-37_jwlfcw.jpg) +```tsx +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "@medusajs/ui" +import { useEffect, useState } from "react" -```ts title="src/api/admin/quotes/[id]/reject/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -import { merchantRejectQuoteWorkflow } from "../../../../../workflows/merchant-reject-quote" +const ProductWidget = () => { + const [productsCount, setProductsCount] = useState(0) + const [loading, setLoading] = useState(true) -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { id } = req.params + useEffect(() => { + if (!loading) { + return + } - await merchantRejectQuoteWorkflow(req.scope).run({ - input: { - quote_id: id, - }, - }) + fetch(`/admin/products`, { + credentials: "include", + }) + .then((res) => res.json()) + .then(({ count }) => { + setProductsCount(count) + setLoading(false) + }) + }, [loading]) - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, - }, - { throwIfKeyNotFound: true } + return ( + + {loading && Loading...} + {!loading && You have {productsCount} Product(s).} + ) - - res.json({ quote }) } -``` -You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/reject`. In the route handler, you run the `merchantRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. - -Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`. +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) -### Add Reject Quote Hook +export default ProductWidget +``` -Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the route to reject a quote. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) -In `src/admin/hooks/quotes.tsx` add the following new hook: +### Add Link to Another Page -```tsx title="src/admin/hooks/quotes.tsx" -// other imports... -import { - useMutation, - UseMutationOptions, -} from "@tanstack/react-query" +To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component: -// ... +```tsx +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "@medusajs/ui" +import { Link } from "react-router-dom" -export const useRejectQuote = ( - id: string, - options?: UseMutationOptions -) => { - const queryClient = useQueryClient() +// The widget +const ProductWidget = () => { + return ( + + View Orders + + ) +} - const rejectQuote = async (id: string) => - sdk.client.fetch(`/admin/quotes/${id}/reject`, { - method: "POST", - }) +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) - return useMutation({ - mutationFn: () => rejectQuote(id), - onSuccess: (data: AdminQuoteResponse, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [orderPreviewQueryKey, id], - }) +export default ProductWidget +``` - queryClient.invalidateQueries({ - queryKey: ["quote", id], - }) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#routing-functionalities/index.html.md). - queryClient.invalidateQueries({ - queryKey: ["quote", "list"], - }) +*** - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} -``` +## Integration Tests -You add a `useRejectQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the reject quote API route. +Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations. -When the mutation is invoked, the hook sends a request to the API route to reject the quote, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data. +For details on setting up your project for integration tests, refer to [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/index.html.md). -### Add Reject Quote Button +### Test Custom API Route -Finally, you can add a button to the quote's details page that allows a merchant to reject the quote. +To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: -In `src/admin/routes/quote/[id]/page.tsx`, add the following imports: +```ts title="integration-tests/http/custom-routes.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -```tsx title="src/admin/routes/quote/[id]/page.tsx" -import { - toast, - usePrompt, -} from "@medusajs/ui" -import { useEffect, useState } from "react" -import { - useRejectQuote, -} from "../../../hooks/quotes" +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/custom` + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) ``` -Then, in the `QuoteDetails` component, add the following after the `useOrderPreview` hook usage: +Then, run the test with the following command: -```tsx title="src/admin/routes/quote/[id]/page.tsx" -const prompt = usePrompt() -const { mutateAsync: rejectQuote, isPending: isRejectingQuote } = - useRejectQuote(id!) -const [showRejectQuote, setShowRejectQuote] = useState(false) +```bash npm2yarn +npm run test:integration +``` -useEffect(() => { - if ( - ["customer_rejected", "merchant_rejected", "accepted"].includes( - quote?.status! - ) - ) { - setShowRejectQuote(false) - } else { - setShowRejectQuote(true) - } -}, [quote]) +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/index.html.md). -const handleRejectQuote = async () => { - const res = await prompt({ - title: "Reject quote?", - description: - "You are about to reject this customer's quote. Do you want to continue?", - confirmText: "Continue", - cancelText: "Cancel", - variant: "confirmation", - }) +### Test Workflow - if (res) { - await rejectQuote(void 0, { - onSuccess: () => - toast.success("Successfully rejected customer's quote"), - onError: (e) => toast.error(e.message), - }) - } -} -``` +To create a test for a workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: -First, you initialize the following variables: +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -1. `prompt`: A function that you'll use to show a confirmation pop-up when the merchant tries to reject the quote. The `usePrompt` hook is available from the Medusa UI package. -2. `rejectQuote` and `isRejectingQuote`: both are returned by the `useRejectQuote` hook. The `rejectQuote` function invokes the mutation, rejecting the quote; `isRejectingQuote` is a boolean that indicates if the mutation is in progress. -3. `showRejectQuote`: A boolean that indicates whether the "Reject Quote" button should be shown. The button is shown if the quote's status is not `customer_rejected`, `merchant_rejected`, or `accepted`. This state variable is changed based on the quote's status in the `useEffect` hook. +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() -You also define a `handleRejectQuote` function that will be called when the merchant clicks the reject quote button. The function shows a confirmation pop-up using the `prompt` function. If the user confirms the action, the function calls the `rejectQuote` function to reject the quote. + expect(result).toEqual("Hello, World!") + }) + }) + }, +}) +``` -Finally, find the `TODO` in the `return` statement and replace it with the following: +Then, run the test with the following command: -```tsx title="src/admin/routes/quote/[id]/page.tsx" -
- {showRejectQuote && ( - - )} -
+```bash npm2yarn +npm run test:integration ``` -In this code snippet, you show the reject quote button if the `showRejectQuote` state is `true`. When the button is clicked, you call the `handleRejectQuote` function to reject the quote. +Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/workflows/index.html.md). -### Test Reject Quote Feature +### Test Module's Service -To test the reject quote feature, start the Medusa application: +To create a test for a module's service, create the test under the `__tests__` directory of the module. -```bash npm2yarn -npm run dev -``` +For example, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: -Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. +```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" -Next, open a quote's details page. You'll find a new "Reject Quote" button. If you click on it and confirm rejecting the quote, the quote will be rejected, and a success message will be shown. +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./modules/blog", + testSuite: ({ service }) => { + describe("BlogModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() -![Quote details page with reject quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741161544/Medusa%20Resources/Screenshot_2025-03-05_at_9.58.41_AM_xzdv6k.png) + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) +``` -*** +Then, run the test with the following command: -## Step 11: Add Merchant Send Quote Feature +```bash npm2yarn +npm run test:modules +``` -Another action that a merchant can take on a quote is to send the quote back to the customer for review. The customer can then reject or accept the quote, which would convert it to an order. +*** -In this step, you'll implement the functionality to send a quote back to the customer for review. This will include: +## Commerce Modules -1. Implementing the workflow to send a quote back to the customer. -2. Adding the API route to send a quote back to the customer that uses the workflow. -3. Add a hook in admin customizations that sends a request to the send quote API route. -4. Add a button to send the quote back to the customer in the quote's details page. +Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules. -### Implement Merchant Send Quote Workflow +Refer to the [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) documentation for concepts and reference of every module's main service. -You'll implement the logic of sending the quote in a workflow. The workflow has the following steps: +### Create an Actor Type to Authenticate -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. -- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. -- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`pending\_customer\`. +To create an actor type that can authenticate to the Medusa application, such as a `manager`: -All the steps are available for use, so you can implement the workflow directly. +1. Create the data model in a module: -Create the file `src/workflows/merchant-send-quote.ts` with the following content: +```ts +import { model } from "@medusajs/framework/utils" -![Directory structure after adding the merchant send quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162342/Medusa%20Resources/quote-38_n4ksr0.jpg) +const Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), +}) -```ts title="src/workflows/merchant-send-quote.ts" highlights={sendQuoteHighlights} -import { useQueryGraphStep } from "@medusajs/core-flows" -import { createWorkflow } from "@medusajs/workflows-sdk" -import { QuoteStatus } from "../modules/quote/models/quote" -import { updateQuotesStep } from "./steps/update-quotes" -import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" +export default Manager +``` -type WorkflowInput = { - quote_id: string; -} +2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager: -export const merchantSendQuoteWorkflow = createWorkflow( - "merchant-send-quote-workflow", - (input: WorkflowInput) => { - const { data: quotes } = useQueryGraphStep({ - entity: "quote", - fields: ["id", "status"], - filters: { id: input.quote_id }, - options: { - throwIfKeyNotFound: true, - }, +```ts +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, +} from "@medusajs/medusa/core-flows" +// other imports... + +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, }) - validateQuoteNotAccepted({ - // @ts-ignore - quote: quotes[0], + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, }) - updateQuotesStep([ - { - id: input.quote_id, - status: QuoteStatus.PENDING_CUSTOMER, - }, - ]) + return new WorkflowResponse(manager) } ) ``` -You create a workflow that accepts the ID of a quote to send back to the customer. In the workflow, you: - -1. Use the `useQueryGraphStep` to retrieve the quote's details. -2. Validate that the quote can be sent back to the customer using the `validateQuoteNotAccepted` step. -3. Update the quote's status to `pending_customer` using the `updateQuotesStep`. +3. Use the workflow in an API route that creates a user (manager) of the actor type: -You'll use this workflow next in an API route that allows a merchant to send a quote back to the customer. +```ts +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import createManagerWorkflow from "../../workflows/create-manager" -### Add Send Quote API Route +type RequestBody = { + first_name: string + last_name: string + email: string +} -You'll now add the API route that allows a merchant to send a quote back to the customer. The route will use the `merchantSendQuoteWorkflow` you created in the previous step. +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." + ) + } -Create the file `src/api/admin/quotes/[id]/send/route.ts` with the following content: + const { result } = await createManagerWorkflow(req.scope) + .run({ + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, + }) + + res.status(200).json({ manager: result }) +} +``` -![Directory structure after adding the send quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162497/Medusa%20Resources/quote-39_us1jbh.jpg) +4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`: -```ts title="src/api/admin/quotes/[id]/send/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" import { - merchantSendQuoteWorkflow, -} from "../../../../../workflows/merchant-send-quote" - -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { id } = req.params + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" - await merchantSendQuoteWorkflow(req.scope).run({ - input: { - quote_id: id, +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], }, - }) - - const { - data: [quote], - } = await query.graph( { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], }, - { throwIfKeyNotFound: true } - ) - - res.json({ quote }) -} + ], +}) ``` -You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/send`. In the route handler, you run the `merchantSendQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. +Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers. -Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`. +Find an elaborate example and learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). -### Add Send Quote Hook +### Apply Promotion on Cart Items and Shipping -Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the quote send API route. +To apply a promotion on a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules: -In `src/admin/hooks/quotes.tsx` add the new hook: +```ts +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -```tsx title="src/admin/hooks/quotes.tsx" -export const useSendQuote = ( - id: string, - options?: UseMutationOptions -) => { - const queryClient = useQueryClient() +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) - const sendQuote = async (id: string) => - sdk.client.fetch(`/admin/quotes/${id}/send`, { - method: "POST", +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, }) + } +}) - return useMutation({ - mutationFn: () => sendQuote(id), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [orderPreviewQueryKey, id], - }) +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.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, + }) + } +}) - queryClient.invalidateQueries({ - queryKey: ["quote", id], - }) +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) - queryClient.invalidateQueries({ - queryKey: ["quote", "list"], - }) +// set the adjustments on the line item +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) -} +// set the adjustments on the shipping method +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) ``` -You add a `useSendQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the send quote API route. - -When the mutation is invoked, the hook sends a request to the send quote API route, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data. +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md). -### Add Send Quote Button +### Retrieve Tax Lines of a Cart's Items and Shipping -Finally, you can add a button to the quote's details page that allows a merchant to send the quote back to the customer for review. +To retrieve the tax lines of a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Tax](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md) modules: -First, add the following import to the `src/admin/routes/quote/[id]/page.tsx` file: +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) -```tsx title="src/admin/routes/quote/[id]/page.tsx" -import { - useSendQuote, -} from "../../../hooks/quotes" -``` +// 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, after the `useRejectQuote` hook usage, add the following: +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) -```tsx title="src/admin/routes/quote/[id]/page.tsx" -const { mutateAsync: sendQuote, isPending: isSendingQuote } = useSendQuote( - id! +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) ) -const [showSendQuote, setShowSendQuote] = useState(false) ``` -You initialize the following variables: +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md) -1. `sendQuote` and `isSendingQuote`: Data returned by the `useSendQuote` hook. The `sendQuote` function invokes the mutation, sending the quote back to the customer; `isSendingQuote` is a boolean that indicates if the mutation is in progress. -2. `showSendQuote`: A boolean that indicates whether the "Send Quote" button should be shown. +### Apply Promotion on an Order's Items and Shipping -Next, update the existing `useEffect` hook to change `showSendQuote` based on the quote's status: +To apply a promotion on an order's items and shipping methods using the [Order](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules: -```tsx title="src/admin/routes/quote/[id]/page.tsx" -useEffect(() => { - if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) { - setShowSendQuote(true) - } else { - setShowSendQuote(false) +```ts +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} 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, + }) } +}) - if ( - ["customer_rejected", "merchant_rejected", "accepted"].includes( - quote?.status! - ) - ) { - setShowRejectQuote(false) - } else { - setShowRejectQuote(true) +//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, + }) } -}, [quote]) +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + // TODO infer from cart or region + currency_code: "usd", + } +) + +// set the adjustments on the line items +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +// set the adjustments on the shipping methods +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) ``` -The `useEffect` hook now updates both the `showSendQuote` and `showRejectQuote` states based on the quote's status. The "Send Quote" button is hidden if the quote's status is not `pending_merchant` or `customer_rejected`. +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md) -Then, after the `handleRejectQuote` function, add the following `handleSendQuote` function: +### Accept Payment using Module -```tsx title="src/admin/routes/quote/[id]/page.tsx" -const handleSendQuote = async () => { - const res = await prompt({ - title: "Send quote?", - description: - "You are about to send this quote to the customer. Do you want to continue?", - confirmText: "Continue", - cancelText: "Cancel", - variant: "confirmation", - }) +To accept payment using the Payment Module's main service: - if (res) { - await sendQuote( - void 0, - { - onSuccess: () => toast.success("Successfully sent quote to customer"), - onError: (e) => toast.error(e.message), - } - ) - } -} -``` +1. Create a payment collection and link it to the cart: -You define a `handleSendQuote` function that will be called when the merchant clicks the "Send Quote" button. The function shows a confirmation pop-up using the `prompt` hook. If the user confirms the action, the function calls the `sendQuote` function to send the quote back to the customer. +```ts +import { + ContainerRegistrationKeys, + Modules, +} from "@medusajs/framework/utils" -Finally, add the following after the reject quote button in the `return` statement: +// ... + +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + region_id: "reg_123", + currency_code: "usd", + amount: 5000, + }) + +// resolve Link +const link = container.resolve( + ContainerRegistrationKeys.LINK +) -```tsx title="src/admin/routes/quote/[id]/page.tsx" -{showSendQuote && ( - -)} +// create a link between the cart and payment collection +link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: paymentCollection.id, + }, +}) ``` -In this code snippet, you show the "Send Quote" button if the `showSendQuote` state is `true`. When the button is clicked, you call the `handleSendQuote` function to send the quote back to the customer. +2. Create a payment session in the collection: -### Test Send Quote Feature +```ts +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, + { + provider_id: "stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider + }, + } + ) +``` -To test the send quote feature, start the Medusa application: +3. Authorize the payment session: -```bash npm2yarn -npm run dev +```ts +const payment = + await paymentModuleService.authorizePaymentSession( + paymentSession.id, + {} + ) ``` -Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). -Next, open a quote's details page. You'll find a new "Send Quote" button. If you click on it and confirm sending the quote, the quote will be sent back to the customer, and a success message will be shown. +### Get Variant's Prices for Region and Currency -You'll later add the feature to update the quote item's details before sending the quote back to the customer. +To get prices of a product variant for a region and currency using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md): -![Quote details page with send quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162950/Medusa%20Resources/Screenshot_2025-03-05_at_10.22.11_AM_sjuipg.png) +```ts +import { QueryContext } from "@medusajs/framework/utils" -*** +// ... -## Step 12: Add Customer Preview Order API Route +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", + }), + }, + }, +}) +``` -When the merchant sends back the quote to the customer, you want to show the customer the details of the quote and the order that would be created if they accept the quote. This helps the customer decide whether to accept or reject the quote (which you'll implement next). +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price#retrieve-calculated-price-for-a-context/index.html.md). -In this step, you'll add the API route that allows a customer to preview a quote's order. +### Get All Variant's Prices -To create the API route, create the file `src/api/store/customers/me/quotes/[id]/preview/route.ts` with the following content: +To get all prices of a product variant using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md): -![Directory structure after adding the customer preview order API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741163145/Medusa%20Resources/quote-40_lmcgve.jpg) +```ts +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", + ], + filters: { + id: [ + "prod_123", + ], + }, +}) +``` -```ts title="src/api/store/customers/me/quotes/[id]/preview/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const { id } = req.params - const query = req.scope.resolve( - ContainerRegistrationKeys.QUERY - ) +### Get Variant Prices with Taxes - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, - }, - { throwIfKeyNotFound: true } - ) +To get a variant's prices with taxes using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) and the [Tax Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md) - const orderModuleService = req.scope.resolve( - Modules.ORDER - ) +```ts +import { + HttpTypes, + TaxableItemDTO, + ItemTaxLineDTO, +} from "@medusajs/framework/types" +import { + QueryContext, + calculateAmountsWithTax, +} from "@medusajs/framework/utils" +// other imports... - const preview = await orderModuleService.previewOrderChange( - quote.draft_order_id - ) +// ... +const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { + return product.variants + ?.map((variant) => { + if (!variant.calculated_price) { + return + } - res.status(200).json({ - quote: { - ...quote, - order_preview: preview, - }, - }) + return { + id: variant.id, + product_id: product.id, + product_name: product.title, + product_categories: product.categories?.map((c) => c.name), + product_category_id: product.categories?.[0]?.id, + product_sku: variant.sku, + product_type: product.type, + product_type_id: product.type_id, + quantity: 1, + unit_price: variant.calculated_price.calculated_amount, + currency_code: variant.calculated_price.currency_code, + } + }) + .filter((v) => !!v) as unknown as TaxableItemDTO[] } -``` - -You create a `GET` route handler, which will expose a `GET` API route at `/store/customers/me/quotes/:id/preview`. In the route handler, you retrieve the quote's details using Query, then preview the order that would be created from the quote using the `previewOrderChange` method from the Order Module's service. Finally, you return the quote and its order preview in the response. -Notice that you're using the `req.queryConfig.fields` object in the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "region_123", + currency_code: "usd", + }), + }, + }, +}) -### Test Customer Preview Order API Route +const taxLines = (await taxModuleService.getTaxLines( + products.map(asTaxItem).flat(), + { + // example of context properties. You can pass other ones. + address: { + country_code, + }, + } +)) as unknown as ItemTaxLineDTO[] -To test the customer preview order API route, start the Medusa application: +const taxLinesMap = new Map() +taxLines.forEach((taxLine) => { + const variantId = taxLine.line_item_id + if (!taxLinesMap.has(variantId)) { + taxLinesMap.set(variantId, []) + } -```bash npm2yarn -npm run dev -``` + taxLinesMap.get(variantId)?.push(taxLine) +}) -Then, grab the ID of a quote placed by a customer that you have their [authentication token](#retrieve-customer-authentication-token). You can find the quote ID in the URL when viewing the quote's details page in the Medusa Admin dashboard. +products.forEach((product) => { + product.variants?.forEach((variant) => { + if (!variant.calculated_price) { + return + } -Finally, send the following request to get a preview of the customer's quote and order: + const taxLinesForVariant = taxLinesMap.get(variant.id) || [] + const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ + taxLines: taxLinesForVariant, + amount: variant.calculated_price!.calculated_amount!, + includesTax: + variant.calculated_price!.is_calculated_price_tax_inclusive!, + }) -```bash -curl 'http://localhost:9000/store/customers/me/quotes/{quote_id}/preview' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ --H 'Authorization: Bearer {token}' + // do something with prices... + }) +}) ``` -Make sure to replace: +Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/index.html.md). -- `{quote_id}` with the ID of the quote you want to preview. -- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). -- `{token}` with the customer's authentication token. +### Invite Users -You'll receive in the response the quote's details with the order preview. You can show the customer these details in the storefront. +To invite a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md): -*** +```ts +const invite = await userModuleService.createInvites({ + email: "user@example.com", +}) +``` -## Step 13: Add Customer Reject Quote Feature +### Accept User Invite -After the customer previews the quote and its order, they can choose to reject the quote. When the customer rejects the quote, the quote's status is changed to `customer_rejected`. The merchant will still be able to update the quote and send it back to the customer for review. +To accept an invite and create a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md): -In this step, you'll implement the functionality to reject a quote from the customer's perspective. This will include: +```ts +const invite = + await userModuleService.validateInviteToken(inviteToken) -1. Implementing the workflow to reject a quote as a customer. -2. Adding the API route to allow customers to reject a quote using the workflow. +await userModuleService.updateInvites({ + id: invite.id, + accepted: true, +}) -### Implement Customer Reject Quote Workflow +const user = await userModuleService.createUsers({ + email: invite.email, +}) +``` -To reject a quote from the customer's perspective, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps: -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. -- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer. -- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`customer\_rejected\`. +# How-to & Tutorials -All the steps are available for use, so you can implement the workflow directly. +In this section of the documentation, you'll find how-to guides and tutorials that will help you customize the Medusa server and admin. These guides are useful after you've learned Medusa's main concepts in the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) section of the documentation. -Create the file `src/workflows/customer-reject-quote.ts` with the following content: +You can follow these guides to learn how to customize the Medusa server and admin to fit your business requirements. This section of the documentation also includes deployment guides to help you deploy your Medusa server and admin to different platforms. -![Directory structure after adding the customer reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164371/Medusa%20Resources/quote-41_fgpqhz.jpg) +## Example Snippets -```ts title="src/workflows/customer-reject-quote.ts" highlights={customerRejectQuoteHighlights} -import { useQueryGraphStep } from "@medusajs/core-flows" -import { createWorkflow } from "@medusajs/workflows-sdk" -import { QuoteStatus } from "../modules/quote/models/quote" -import { updateQuotesStep } from "./steps/update-quotes" -import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted" +For a quick access to code snippets of the different concepts you learned about, such as API routes and workflows, refer to the [Examples Snippets](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/index.html.md) documentation. -type WorkflowInput = { - quote_id: string; - customer_id: string; -} +*** -export const customerRejectQuoteWorkflow = createWorkflow( - "customer-reject-quote-workflow", - (input: WorkflowInput) => { - const { data: quotes } = useQueryGraphStep({ - entity: "quote", - fields: ["id", "status"], - filters: { id: input.quote_id, customer_id: input.customer_id }, - options: { - throwIfKeyNotFound: true, - }, - }) +*** - validateQuoteNotAccepted({ - // @ts-ignore - quote: quotes[0], - }) +## Deployment Guides - updateQuotesStep([ - { - id: input.quote_id, - status: QuoteStatus.CUSTOMER_REJECTED, - }, - ]) - } -) -``` +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. -You create a workflow that accepts the IDs of the quote to reject and the customer rejecting it. In the workflow, you: -1. Use the `useQueryGraphStep` to retrieve the quote's details. Notice that you pass the IDs of the quote and the customer as filters to ensure that the quote belongs to the customer. -2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted` step. -3. Update the quote's status to `customer_rejected` using the `updateQuotesStep`. +# Send Abandoned Cart Notifications in Medusa -You'll use this workflow next in an API route that allows a customer to reject a quote. +In this tutorial, you will learn how to send notifications to customers who have abandoned their carts. -### Add Customer Reject Quote API Route +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 cart-management capabilities. -You'll now add the API route that allows a customer to reject a quote. The route will use the `customerRejectQuoteWorkflow` you created in the previous step. +Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications. -Create the file `src/api/store/customers/me/quotes/[id]/reject/route.ts` with the following content: +In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers. -![Directory structure after adding the customer reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164538/Medusa%20Resources/quote-42_bryo2z.jpg) +## Summary -```ts title="src/api/store/customers/me/quotes/[id]/reject/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -import { - customerRejectQuoteWorkflow, -} from "../../../../../../../workflows/customer-reject-quote" +By following this tutorial, you will: -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const { id } = req.params - const query = req.scope.resolve( - ContainerRegistrationKeys.QUERY - ) +- Install and set up Medusa. +- Create the logic to send an email to customers who have abandoned their carts. +- Run the above logic once a day. +- Add a route to the storefront to recover the cart. - await customerRejectQuoteWorkflow(req.scope).run({ - input: { - quote_id: id, - customer_id: req.auth_context.actor_id, - }, - }) +![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg) - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, - }, - { throwIfKeyNotFound: true } - ) +[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial. - return res.json({ quote }) -} -``` +*** -You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/reject`. In the route handler, you run the `customerRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. +## Step 1: Install a Medusa Application -Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. +### Prerequisites -### Test Customer Reject Quote Feature +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) -To test the customer reject quote feature, start the Medusa application: +Start by installing the Medusa application on your machine with the following command: -```bash npm2yarn -npm run dev +```bash +npx create-medusa-app@latest ``` -Then, send a request to reject a quote for the authenticated customer: +You will 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." -```bash -curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/reject' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ --H 'Authorization: Bearer {token}' -``` +Afterwards, 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. -Make sure to replace: +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). -- `{quote_id}` with the ID of the quote you want to reject. -- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). -- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token). +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. Afterwards, you can log in with the new user and explore the dashboard. -After sending the request, the quote will be rejected, and the updated quote will be returned in the response. You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. +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 14: Add Customer Accept Quote Feature +## Step 2: Set up SendGrid -The customer alternatively can choose to accept a quote after previewing it. When the customer accepts a quote, the quote's draft order should become an order whose payment can be processed and items fulfilled. No further changes can be made on the quote after it's accepted. +### Prerequisites -In this step, you'll implement the functionality to allow a customer to accept a quote. This will include: +- [SendGrid account](https://sendgrid.com) +- [Verified Sender Identity](https://mc.sendgrid.com/senders) +- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) -1. Implementing the workflow to accept a quote as a customer. -2. Adding the API route to allow customers to accept a quote using the workflow. +Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice. -### Implement Customer Accept Quote Workflow +To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails. -You'll implement the quote acceptance logic in a workflow. The workflow has the following steps: +Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details. -- [validateQuoteCanAcceptStep](#validateQuoteCanAcceptStep): Validate that the quote can be accepted. -- [updateQuotesStep](#updateQuotesStep): Update the quote's status to \`accepted\`. -- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md): Confirm the changes made on the draft order, such as changes to item quantities and prices. -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md): Update the draft order to change its status and convert it into an order. +To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`: -You only need to implement the `validateQuoteCanAcceptStep` step before implementing the workflow, as the other steps are already available for use. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@medusajs/medusa/notification-sendgrid", + id: "sendgrid", + options: { + channels: ["email"], + api_key: process.env.SENDGRID_API_KEY, + from: process.env.SENDGRID_FROM, + }, + }, + ], + }, + }, + ], +}) +``` -#### validateQuoteCanAcceptStep +In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options: -In the `validateQuoteCanAcceptStep`, you'll validate whether the customer can accept the quote. The customer can only accept a quote if the quote's status is `pending_customer`, meaning the merchant sent the quote back to the customer for review. +- `channels`: The channels that the provider supports. In this case, it is only email. +- `api_key`: Your SendGrid API key. +- `from`: The email address that the emails will be sent from. -Create the file `src/workflows/steps/validate-quote-can-accept.ts` with the following content: +Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project: -![Directory structure after adding the validate quote can accept step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741165829/Medusa%20Resources/quote-43_cxc3qi.jpg) +```plain +SENDGRID_API_KEY=your-sendgrid-api-key +SENDGRID_FROM=test@gmail.com +``` -```ts title="src/workflows/steps/validate-quote-can-accept.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Quote, QuoteStatus } from "../../modules/quote/models/quote" +You can now use SendGrid to send emails in Medusa. -type StepInput = { - quote: InferTypeOf -} +*** -export const validateQuoteCanAcceptStep = createStep( - "validate-quote-can-accept", - async function ({ quote }: StepInput) { - if (quote.status !== QuoteStatus.PENDING_CUSTOMER) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Cannot accept quote when quote status is ${quote.status}` - ) - } - } -) -``` +## Step 3: Send Abandoned Cart Notification Flow -You create a step that accepts a quote as input. In the step function, you throw an error if the quote's status is not `pending_customer`. +You will now implement the sending logic for the abandoned cart notifications. -#### Implement Workflow +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 is 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 a scheduled job. -You can now implement the workflow that accepts a quote for a customer. Create the file `src/workflows/customer-accept-quote.ts` with the following content: +In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day. -![Directory structure after adding the customer accept quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166025/Medusa%20Resources/quote-44_c09ts9.jpg) +The workflow will receive the list of abandoned carts as an input. The workflow has the following steps: -```ts title="src/workflows/customer-accept-quote.ts" highlights={customerAcceptQuoteHighlights} -import { - confirmOrderEditRequestWorkflow, - updateOrderWorkflow, - useQueryGraphStep, -} from "@medusajs/core-flows" -import { OrderStatus } from "@medusajs/framework/utils" -import { createWorkflow } from "@medusajs/workflows-sdk" -import { validateQuoteCanAcceptStep } from "./steps/validate-quote-can-accept" -import { QuoteStatus } from "../modules/quote/models/quote" -import { updateQuotesStep } from "./steps/update-quotes" +- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date. -type WorkflowInput = { - quote_id: string; - customer_id: string; -}; +Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one. -export const customerAcceptQuoteWorkflow = createWorkflow( - "customer-accept-quote-workflow", - (input: WorkflowInput) => { - const { data: quotes } = useQueryGraphStep({ - entity: "quote", - fields: ["id", "draft_order_id", "status"], - filters: { id: input.quote_id, customer_id: input.customer_id }, - options: { - throwIfKeyNotFound: true, - }, - }) +### sendAbandonedNotificationsStep - validateQuoteCanAcceptStep({ - // @ts-ignore - quote: quotes[0], - }) +The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input. - updateQuotesStep([{ - id: input.quote_id, - status: QuoteStatus.ACCEPTED, - }]) +To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content: - confirmOrderEditRequestWorkflow.runAsStep({ - input: { - order_id: quotes[0].draft_order_id, - confirmed_by: input.customer_id, - }, - }) +```ts title="src/workflows/steps/send-abandoned-notifications.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { CartDTO, CustomerDTO } from "@medusajs/framework/types" - updateOrderWorkflow.runAsStep({ - input:{ - id: quotes[0].draft_order_id, - // @ts-ignore - status: OrderStatus.PENDING, - is_draft_order: false, +type SendAbandonedNotificationsStepInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] +} + +export const sendAbandonedNotificationsStep = createStep( + "send-abandoned-notifications", + async (input: SendAbandonedNotificationsStepInput, { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const notificationData = input.carts.map((cart) => ({ + to: cart.email!, + channel: "email", + template: process.env.ABANDONED_CART_TEMPLATE_ID || "", + data: { + customer: { + first_name: cart.customer?.first_name || cart.shipping_address?.first_name, + last_name: cart.customer?.last_name || cart.shipping_address?.last_name, + }, + cart_id: cart.id, + items: cart.items?.map((item) => ({ + product_title: item.title, + quantity: item.quantity, + unit_price: item.unit_price, + thumbnail: item.thumbnail, + })), }, + })) + + const notifications = await notificationModuleService.createNotifications( + notificationData + ) + + return new StepResponse({ + notifications, }) } ) ``` -You create a workflow that accepts the IDs of the quote to accept and the customer accepting it. In the workflow, you: +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: -1. Use the `useQueryGraphStep` to retrieve the quote's details. You pass the IDs of the quotes and the customer as filters to ensure that the quote belongs to the customer. -2. Validate that the quote can be accepted using the `validateQuoteCanAcceptStep`. -3. Update the quote's status to `accepted` using the `updateQuotesStep`. -4. Confirm the changes made on the draft order using the `confirmOrderEditRequestWorkflow` executed as a step. This is useful when you soon add the admin functionality to edit the quote items. Any changes that the admin has made will be applied on the draft order using this step. -5. Update the draft order to change its status and convert it into an order using the `updateOrderWorkflow` executed as a step. +1. The step's unique name, which is `create-review`. +2. An async function that receives two parameters: + - The step's input, which is in this case an object with the review's properties. + - 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. -You'll use this workflow next in an API route that allows a customer to accept a quote. +In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method. -### Add Customer Accept Quote API Route +Notice that each notification is an object with the following properties: -You'll now add the API route that allows a customer to accept a quote. The route will use the `customerAcceptQuoteWorkflow` you created in the previous step. +- `to`: The email address of the customer. +- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel. +- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it. +- `data`: The data to pass to the template to render the email's dynamic content. -Create the file `src/api/store/customers/me/quotes/[id]/accept/route.ts` with the following content: +Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object. -![Directory structure after adding the customer accept quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166543/Medusa%20Resources/quote-45_y8zprn.jpg) +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications. -```ts title="src/api/store/customers/me/quotes/[id]/accept/route.ts" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -import { - customerAcceptQuoteWorkflow, -} from "../../../../../../../workflows/customer-accept-quote" +### Create Workflow -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { id } = req.params +You can now create the workflow that uses the step you just created to send the abandoned cart notifications. - await customerAcceptQuoteWorkflow(req.scope).run({ - input: { - quote_id: id, - customer_id: req.auth_context.actor_id, - }, - }) +Create the file `src/workflows/send-abandoned-carts.ts` with the following content: - const { - data: [quote], - } = await query.graph( - { - entity: "quote", - filters: { id }, - fields: req.queryConfig.fields, - }, - { throwIfKeyNotFound: true } - ) +```ts title="src/workflows/send-abandoned-carts.ts" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + sendAbandonedNotificationsStep, +} from "./steps/send-abandoned-notifications" +import { updateCartsStep } from "@medusajs/medusa/core-flows" +import { CartDTO } from "@medusajs/framework/types" +import { CustomerDTO } from "@medusajs/framework/types" - return res.json({ quote }) +export type SendAbandonedCartsWorkflowInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] } + +export const sendAbandonedCartsWorkflow = createWorkflow( + "send-abandoned-carts", + function (input: SendAbandonedCartsWorkflowInput) { + sendAbandonedNotificationsStep(input) + + const updateCartsData = transform( + input, + (data) => { + return data.carts.map((cart) => ({ + id: cart.id, + metadata: { + ...cart.metadata, + abandoned_notification: new Date().toISOString(), + }, + })) + } + ) + + const updatedCarts = updateCartsStep(updateCartsData) + + return new WorkflowResponse(updatedCarts) + } +) ``` -You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/accept`. In the route handler, you run the `customerAcceptQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response. +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. -Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`. +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 arra of carts. -### Test Customer Accept Quote Feature +In the workflow's constructor function, you: -To test the customer accept quote feature, start the Medusa application: +- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers. +- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date. -```bash npm2yarn -npm run dev -``` +Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). -Then, send a request to accept a quote for the authenticated customer: +Your workflow is now ready for use. You will learn how to execute it in the next section. -```bash -curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/accept' \ --H 'x-publishable-api-key: {your_publishable_api_key}' \ --H 'Authorization: Bearer {token}' -``` +### Setup Email Template -Make sure to replace: +Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step. -- `{quote_id}` with the ID of the quote you want to accept. -- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key). -- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token). +To create an email template in SendGrid: -After sending the request, the quote will be accepted, and the updated quote will be returned in the response. +- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard. +- Click on the "Create Dynamic Template" button. -You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. The quote will also have an order, which you can view in the Orders page or using the "View Order" button on the quote's details page. +![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png) -![View order button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166844/Medusa%20Resources/Screenshot_2025-03-05_at_11.27.02_AM_s90rqh.png) +- In the side window that opens, enter a name for the template, then click on the Create button. +- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it. -*** +![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png) -## Step 15: Edit Quote Items UI Route +In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template. -The last feature you'll add is allowing merchants or admin users to make changes to the quote's items. This includes updating the item's quantity and price. +You can also use the following template as an example: -Since you're using an [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to manage edits to the quote's draft orders, you don't need to implement customizations on the server side, such as adding workflows or API routes. Instead, you'll only add a new UI route in the Medusa Admin that uses the [Order Edit API routes](https://docs.medusajs.com/api/admin#order-edits) to provide the functionality to edit the quote's items. +```html title="Abandoned Cart Email Template" + + + + + + Complete Your Purchase + + + +
+
Hi {{customer.first_name}}, your cart is waiting! 🛍️
+

You left some great items in your cart. Complete your purchase before they're gone!

+ + {{#each items}} +
+ {{product_title}} +
+ {{product_title}} +

{{subtitle}}

+

Quantity: {{quantity}}

+

Price: $ {{unit_price}}

+
+
+ {{/each}} + + Return to Cart & Checkout + +
+ + +``` -Order changes also allow you to add or remove items from the quote. However, for simplicity, this guide only covers how to update the item's quantity and price. Refer to the [Order Change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) documentation to learn more. +This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout. -![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png) +You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart. -In this step, you'll add a new UI route to manage the quote's items. This will include: +Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project: -1. Adding hooks to send requests to [Medusa's Order Edits API routes](https://docs.medusajs.com/api/admin#order-edits). -2. Implement the components you'll use within the UI route. -3. Add the new UI route to the Medusa Admin. +```plain +ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id +``` -### Intermission: Order Editing Overview +*** -Before you start implementing the customizations, here's a quick overview of how order editing works in Medusa. +## Step 4: Schedule Cart Abandonment Notifications -When the admin wants to edit an order's items, Medusa creates an order change. You've already implemented this part on quote creation. +The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers. -Then, when the admin makes an edit to an item, Medusa saves that edit but without applying it to the order or finalizing the edit. This allows the admin to make multiple edits before finalizing the changes. +To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. -Once the admin is finished editing, they can confirm the order edit, which finalizes it to later be applied on the order. You've already implemented applying the order edit on the order when the customer accepts the quote. +You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content: -So, you still need two implement two aspects: updating the quote items, and confirming the order edit. You'll implement these in the next steps. +```ts title="src/jobs/send-abandoned-cart-notification.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { + sendAbandonedCartsWorkflow, + SendAbandonedCartsWorkflowInput, +} from "../workflows/send-abandoned-carts" -### Add Hooks +export default async function abandonedCartJob( + container: MedusaContainer +) { + const logger = container.resolve("logger") + const query = container.resolve("query") -To implement the edit quote items functionality, you'll need two hooks: + const oneDayAgo = new Date() + oneDayAgo.setDate(oneDayAgo.getDate() - 1) + const limit = 100 + const offset = 0 + const totalCount = 0 + const abandonedCartsCount = 0 -1. A hook that updates a quote item's quantity and price using the Order Edits API routes. -2. A hook that confirms the edit of the items using the Order Edits API routes. + do { + // TODO retrieve paginated abandoned carts + } while (offset < totalCount) -#### Update Quote Item Hook + logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`) +} -The first hook updates an item's quantity and price using the Order Edits API routes. You'll use this whenever an admin updates an item's quantity or price. +export const config = { + name: "abandoned-cart-notification", + schedule: "0 0 * * *", // Run at midnight every day +} +``` -In `src/admin/hooks/quotes.tsx`, add the following hook: +In a scheduled job's file, you must export: -```tsx title="src/admin/hooks/quotes.tsx" -// other imports... -import { HttpTypes } from "@medusajs/framework/types" +1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. +2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs. -// ... +In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. -export const useUpdateQuoteItem = ( - id: string, - options?: UseMutationOptions< - HttpTypes.AdminOrderEditPreviewResponse, - FetchError, - UpdateQuoteItemParams - > -) => { - const queryClient = useQueryClient() +You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts. - return useMutation({ - mutationFn: ({ - itemId, - ...payload - }: UpdateQuoteItemParams) => { - return sdk.admin.orderEdit.updateOriginalItem(id, itemId, payload) - }, - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [orderPreviewQueryKey, id], - }) +Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following: - options?.onSuccess?.(data, variables, context) +```ts title="src/jobs/send-abandoned-cart-notification.ts" +const { + data: abandonedCarts, + metadata, +} = await query.graph({ + entity: "cart", + fields: [ + "id", + "email", + "items.*", + "metadata", + "customer.*", + ], + filters: { + updated_at: { + $lt: oneDayAgo, }, - ...options, + email: { + $ne: null, + }, + completed_at: null, + }, + pagination: { + skip: offset, + take: limit, + }, +}) + +totalCount = metadata?.count ?? 0 +const cartsWithItems = abandonedCarts.filter((cart) => + cart.items?.length > 0 && !cart.metadata?.abandoned_notification +) + +try { + await sendAbandonedCartsWorkflow(container).run({ + input: { + carts: cartsWithItems, + } as unknown as SendAbandonedCartsWorkflowInput, }) + abandonedCartsCount += cartsWithItems.length + +} catch (error) { + logger.error( + `Failed to send abandoned cart notification: ${error.message}` + ) } + +offset += limit ``` -You create a `useUpdateQuoteItem` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that updates an item's quantity and price using the `sdk.admin.orderEdit.updateOriginalItem` method. +In the do-while loop, you use Query to retrieve carts matching the following criteria: -When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data. +- The cart was last updated more than a day ago. +- The cart has an email address. +- The cart has not been completed. -#### Confirm Order Edit Hook +You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification. -Next, you'll add a hook that confirms the order edit. This hook will be used when the admin is done editing the quote's items. As mentioned earlier, confirming the order edit doesn't apply the changes to the order but finalizes the edit. +Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts. -In `src/admin/hooks/quotes.tsx`, add the following hook: +### Test it Out -```tsx title="src/admin/hooks/quotes.tsx" -export const useConfirmQuote = ( - id: string, - options?: UseMutationOptions< - HttpTypes.AdminOrderEditPreviewResponse, - FetchError, - void - > -) => { - const queryClient = useQueryClient() +To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing: - return useMutation({ - mutationFn: () => sdk.admin.orderEdit.request(id), - onSuccess: (data: any, variables: any, context: any) => { - queryClient.invalidateQueries({ - queryKey: [orderPreviewQueryKey, id], - }) +```ts title="src/jobs/send-abandoned-cart-notification.ts" +oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing +``` - options?.onSuccess?.(data, variables, context) - }, - ...options, - }) +And to change the job's schedule in `config` to run every minute: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +export const config = { + // ... + schedule: "* * * * *", // Run every minute for testing } ``` -You create a `useConfirmQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that confirms the order edit using the `sdk.admin.orderEdit.request` method. +Finally, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command: -When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data. +```bash npm2yarn +npm run dev +``` -Now that you have the necessary hooks, you can use them in the UI route and its components. +Open the storefront at `localhost:8000`. You can either: -### Add ManageItem Component +- Create an account and add items to the cart, then leave the cart for a minute. +- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute. -The UI route will show the list of items to the admin user and allows them to update the item's quantity and price. So, you'll create a component that allows the admin to manage a single item's details. You'll later use this component for each item in the quote. +Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal: -![Screenshot of the manage item component in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186495/Medusa%20Resources/manage-item-highlight_ouffnu.png) +```bash +info: Sent 1 abandoned cart notifications +``` -Create the file `src/admin/components/manage-item.tsx` with the following content: +Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule. -![Directory structure after adding the manage item component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168152/Medusa%20Resources/quote-46_yxanj7.jpg) +*** -```tsx -import { AdminOrder, AdminOrderPreview } from "@medusajs/framework/types" -import { - Badge, - CurrencyInput, - Hint, - Input, - Label, - Text, - toast, -} from "@medusajs/ui" -import { useMemo } from "react" -import { - useUpdateQuoteItem, -} from "../hooks/quotes" -import { Amount } from "./amount" +## Step 5: Recover Cart in Storefront -type ManageItemProps = { - originalItem: AdminOrder["items"][0]; - item: AdminOrderPreview["items"][0]; - currencyCode: string; - orderId: string; -}; +In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page. -export function ManageItem({ - originalItem, - item, - currencyCode, - orderId, -}: ManageItemProps) { - const { mutateAsync: updateItem } = useUpdateQuoteItem(orderId) +To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content: - const isItemUpdated = useMemo( - () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"), - [item] - ) +```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue" +import { NextRequest } from "next/server" +import { retrieveCart } from "../../../../../../lib/data/cart" +import { setCartId } from "../../../../../../lib/data/cookies" +import { notFound, redirect } from "next/navigation" +type Params = Promise<{ + id: string +}> - const onUpdate = async ({ - quantity, - unit_price, - }: { - quantity?: number; - unit_price?: number; - }) => { - if ( - typeof quantity === "number" && - quantity <= item.detail.fulfilled_quantity - ) { - toast.error("Quantity should be greater than the fulfilled quantity") - return - } +export async function GET(req: NextRequest, { params }: { params: Params }) { + const { id } = await params + const cart = await retrieveCart(id) - try { - await updateItem({ - quantity, - unit_price, - itemId: item.id, - }) - } catch (e) { - toast.error((e as any).message) - } + if (!cart) { + return notFound() } - - // TODO render the item's details and input fields + + setCartId(id) + + const countryCode = cart.shipping_address?.country_code || + cart.region?.countries?.[0]?.iso_2 + + redirect( + `/${countryCode ? `${countryCode}/` : ""}cart` + ) } ``` -You define a `ManageItem` component that accepts the following props: +You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you: -- `originalItem`: The original item details from the quote. This is the item's details before any edits. -- `item`: The item's details from the quote's order preview. This is the item's details which may have been edited. -- `currencyCode`: The currency code of the quote's draft order. -- `orderId`: The ID of the quote's draft order. +- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response. +- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront. +- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region. -In the component, you define the following variables: +### Test it Out -- `updateItem`: The `mutateAsync` function returned by the `useUpdateQuoteItem` hook. This function updates the item's quantity and price using Medusa's Order Edits API routes. -- `isItemUpdated`: A boolean that indicates whether the item has been updated. +To test it out, start the Medusa application: -You also define an `onUpdate` function that will be called when the admin updates the item's quantity or price. The function sends a request to update the item's quantity and price using the `updateItem` function. If the quantity is less than or equal to the fulfilled quantity, you show an error message. +```bash npm2yarn +npm run dev +``` -Next, you'll add a return statement to show the item's details and allow the admin to update the item's quantity and price. Replace the `TODO` with the following: +And in the Next.js Starter Storefront's directory, start the storefront: -```tsx title="src/admin/components/manage-item.tsx" -return ( -
-
-
-
+```bash npm2yarn +npm run dev +``` -
-
- - {item.title}{" "} - +Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart. - {item.variant_sku && ({item.variant_sku})} -
- - {item.product_title} - -
-
+![Cart page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1742459552/Medusa%20Resources/Screenshot_2025-03-20_at_10.32.17_AM_frmbup.png) - {isItemUpdated && ( - - Modified - - )} -
+*** -
-
- { - const val = e.target.value - const quantity = val === "" ? null : Number(val) +## Next Steps - if (quantity) { - onUpdate({ quantity }) - } - }} - /> - - Quantity - -
+You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as: -
- -
-
-
+- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md). +- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md). +- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md). -
-
- - - Override the unit price of this product - -
+If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have used in this guide and more. -
-
- { - onUpdate({ - unit_price: parseFloat(e.target.value), - quantity: item.quantity, - }) - }} - className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover" - /> -
-
-
-
-) -``` +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). -You show the item's title, product title, and variant SKU. If the item has been updated, you show a "Modified" badge. -You also show input fields for the quantity and price of the item, allowing the admin to update the item's quantity and price. Once the admin updates the quantity or price, the `onUpdate` function is called to send a request to update the item's details. +# Implement First-Purchase Discount in Medusa -### Add ManageQuoteForm Component +In this tutorial, you'll learn how to implement first-purchase discounts in Medusa. -Next, you'll add the form component that shows the list of items in the quote and allows the admin to manage each item. You'll use the `ManageItem` component you created in the previous step for each item in the quote. +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 promotion and cart management features. -![Screenshot of the manage quote form in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186643/Medusa%20Resources/manage-quote-form-highlight_pfyee5.png) +The first-purchase discount feature encourages customers to sign up and make their first purchase by offering them a discount. In this tutorial, you'll learn how to implement this feature in Medusa. -Create the file `src/admin/components/manage-quote-form.tsx` with the following content: +## Summary -![Directory structure after adding the manage quote form component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168581/Medusa%20Resources/quote-47_f5kamq.jpg) +By following this tutorial, you'll learn how to: -```tsx title="src/admin/components/manage-quote-form.tsx" -import { AdminOrder } from "@medusajs/framework/types" -import { Button, Heading, toast } from "@medusajs/ui" -import { useConfirmQuote } from "../hooks/quotes" -import { formatAmount } from "../utils/format-amount" -import { useOrderPreview } from "../hooks/order-preview" -import { useNavigate, useParams } from "react-router-dom" -import { useMemo } from "react" -import { ManageItem } from "./manage-item" +- Install and set up Medusa. +- Apply a first-purchase discount to a customer's cart if they are a first-time customer. +- Add custom validation to ensure the discount is only used by first-time customers. +- Customize the Next.js Starter Storefront to display a pop-up encouraging first-time customers to sign up and receive a discount. -type ReturnCreateFormProps = { - order: AdminOrder; -}; +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. -export const ManageQuoteForm = ({ order }: ReturnCreateFormProps) => { - const { order: preview } = useOrderPreview(order.id) - const navigate = useNavigate() - const { id: quoteId } = useParams() +![Diagram showcasing the flow of first-time purchase discounts](https://res.cloudinary.com/dza7lstvk/image/upload/v1750846212/Medusa%20Resources/first-purchase-promo-overview_jbiwa9.jpg) - const { mutateAsync: confirmQuote, isPending: isRequesting } = - useConfirmQuote(order.id) +[View on Github](https://github.com/medusajs/examples/tree/main/first-purchase-discount): Find the full code for this tutorial. - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - try { - await confirmQuote() - navigate(`/quotes/${quoteId}`) +*** - toast.success("Successfully updated quote") - } catch (e) { - toast.error("Error", { - description: (e as any).message, - }) - } - } - - const originalItemsMap = useMemo(() => { - return new Map(order.items.map((item) => [item.id, item])) - }, [order]) +## Step 1: Install a Medusa Application - if (!preview) { - return <> - } +### Prerequisites - // TODO render form -} -``` +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) -You define a `ManageQuoteForm` component that accepts the quote's draft order as a prop. In the component, you retrieve the preview of that order. The preview holds any edits made on the order's items. +Start by installing the Medusa application on your machine with the following command: -You also define the `confirmQuote` function using the `useConfirmQuote` hook. This function confirms the order edit, finalizing the changes made on the order's items. +```bash +npx create-medusa-app@latest +``` -Then, you define the `handleSubmit` function that will be called when the admin submits the form. The function confirms the order edit using the `confirmQuote` function and navigates the admin back to the quote's details page. +First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes." -Next, you'll add a return statement to show the edit form for the quote's items. Replace the `TODO` with the following: +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 named `{project-name}-storefront`. -```tsx title="src/admin/components/manage-quote-form.tsx" -return ( -
-
-
- Items -
+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). - {preview.items.map((item) => ( - - ))} -
+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. -
-
- - Current Total - +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. - - {formatAmount(order.total, order.currency_code)} - -
+*** -
- - New Total - +## Step 2: Create a First-Purchase Promotion - - {formatAmount(preview.total, order.currency_code)} - -
-
+Before you apply the first-purchase discount or promotion to a customer's cart, you need to create the promotion that will be applied. -
-
- -
-
-
-) +Start your Medusa application with the following command: + +```bash npm2yarn +npm run dev ``` -You use the `ManageItem` component to show each item in the quote and allow the admin to update the item's quantity and price. You also show the updated total amount of the quote and a button to confirm the order edit. +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in with the user you created in the previous step. -You'll use this component next in the UI route that allows the admin to edit the quote's items. +Next, click on the "Promotions" tab in the left sidebar, then click on the "Create Promotion" button to create a new promotion. -### Implement UI Route +You can customize the promotion based on your use case. For example, it can be a `10%` off the entire order, or a fixed amount off specific items. -Finally, you'll add the UI route that allows the admin to edit the quote's items. The route will use the `ManageQuoteForm` component you created in the previous step. +Make sure to set the promotion's code to `FIRST_PURCHASE`, as you'll be using this code in your Medusa customization. If you want to use a different code, make sure to update the code in the next steps accordingly. -Create the file `src/admin/routes/quotes/[id]/manage/page.tsx` with the following content: +Refer to the [Create Promotions User Guide](https://docs.medusajs.com/user-guide/promotions/create/index.html.md) to learn how to create promotions in Medusa. -![Directory structure after adding the edit quote items UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168993/Medusa%20Resources/quote-48_roangs.jpg) +Once you create and publish the promotion, you can proceed to the next steps. -```tsx title="src/admin/routes/quotes/[id]/manage/page.tsx" -import { useParams } from "react-router-dom" -import { useQuote } from "../../../../hooks/quotes" -import { Container, Heading, Toaster } from "@medusajs/ui" -import { ManageQuoteForm } from "../../../../components/manage-quote-form" +![First-purchase promotion in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1750846696/Medusa%20Resources/CleanShot_2025-06-25_at_13.18.00_2x_j46emw.png) -const QuoteManage = () => { - const { id } = useParams() - const { quote, isLoading } = useQuote(id!, { - fields: - "*draft_order.customer", - }) +*** - if (isLoading) { - return <> - } +## Step 3: Apply the First-Purchase Discount to Cart - if (!quote) { - throw "quote not found" - } +In this step, you'll customize the Medusa application to automatically apply the first-purchase promotion to a cart. - return ( - <> - - - Manage Quote - +To build this feature, you need to: - - - - - ) -} +- Create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that implements the logic to apply the first-purchase promotion to a cart. +- Execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that is triggered when a cart is created, or when it's transferred to a customer. -export default QuoteManage +### a. Store the First-Purchase Promotion Code + +Since you'll refer to the first-purchase promotion code in multiple places, it's a good idea to store it as a constant in your Medusa application. + +So, create the file `src/constants.ts` with the following content: + +```ts title="src/constants.ts" +export const FIRST_PURCHASE_PROMOTION_CODE = "FIRST_PURCHASE" ``` -You define a `QuoteManage` component that will show the form to manage the quote's items in the Medusa Admin dashboard. +You'll reference this constant in the next steps. -In the component, you first retrieve the quote's details using the `useQuote` hook. Then, you show the `ManageQuoteForm` component, passing the quote's draft order as a prop. +### b. Create the Workflow -### Add Manage Button to Quote Details Page +Next, you'll create the workflow that implements the logic to apply the first-purchase promotion to a cart. -To allow the admin to access the manage page you just added, you'll add a new button on the quote's details page that links to the manage page. +A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task with rollback and retry mechanisms. In Medusa, you build commerce features in workflows, then execute them in other customizations, such as subscribers, scheduled jobs, and API routes. -In `src/admin/routes/quotes/[id]/page.tsx`, add the following variable definition after the `showSendQuote` variable: +The workflow you'll build will have the following steps: -```tsx title="src/admin/routes/quotes/[id]/page.tsx" -const [showManageQuote, setShowManageQuote] = useState(false) -``` +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details, including its promotions and customer. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the first-purchase promotion. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details, including its promotions. -This variable will be used to show or hide the manage quote button. +Medusa provides all these steps in its `@medusajs/medusa/core-flows` package, so you can implement the workflow right away. -Then, update the existing `useEffect` hook to the following: +To create the workflow, create the file `src/workflows/apply-first-purchase-promo.ts` with the following content: -```tsx title="src/admin/routes/quotes/[id]/page.tsx" -useEffect(() => { - if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) { - setShowSendQuote(true) - } else { - setShowSendQuote(false) - } +```ts title="src/workflows/apply-first-purchase-promo.ts" highlights={workflowHighlights} +import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { updateCartPromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { FIRST_PURCHASE_PROMOTION_CODE } from "../constants" +import { PromotionActions } from "@medusajs/framework/utils" - if ( - ["customer_rejected", "merchant_rejected", "accepted"].includes( - quote?.status! - ) - ) { - setShowRejectQuote(false) - } else { - setShowRejectQuote(true) - } +type WorkflowInput = { + cart_id: string +} - if (![ - "pending_merchant", - "customer_rejected", - "merchant_rejected", - ].includes(quote?.status!)) { - setShowManageQuote(false) - } else { - setShowManageQuote(true) +export const applyFirstPurchasePromoWorkflow = createWorkflow( + "apply-first-purchase-promo", + (input: WorkflowInput) => { + const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: ["promotions.*", "customer.*", "customer.orders.*"], + filters: { + id: input.cart_id, + }, + }) + + const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: ["code"], + filters: { + code: FIRST_PURCHASE_PROMOTION_CODE, + }, + }).config({ name: "retrieve-promotions" }) + + when({ + carts, + promotions, + }, (data) => { + return data.promotions.length > 0 && + !data.carts[0].promotions?.some((promo) => promo?.id === data.promotions[0].id) && + data.carts[0].customer !== null && + data.carts[0].customer.orders?.length === 0 + }) + .then(() => { + updateCartPromotionsStep({ + id: carts[0].id, + promo_codes: [promotions[0].code!], + action: PromotionActions.ADD, + }) + }) + + // retrieve updated cart + const { data: updatedCarts } = useQueryGraphStep({ + entity: "cart", + fields: ["*", "promotions.*"], + filters: { + id: input.cart_id, + }, + }).config({ name: "retrieve-updated-cart" }) + + return new WorkflowResponse(updatedCarts[0]) } -}, [quote]) +) ``` -The `showManageQuote` variable is now updated based on the quote's status, where you only show it if the quote is pending the merchant's action, or if it has been rejected by either the customer or merchant. - -Finally, add the following button component after the `Send Quote` button: +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. -```tsx title="src/admin/routes/quotes/[id]/page.tsx" -{showManageQuote && ( - -)} -``` +`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. The function accepts as an input an object with the ID of the cart to apply the first-purchase promotion to. -The Manage Quote button is now shown if the `showManageQuote` variable is `true`. When clicked, it navigates the admin to the manage quote page. +In the workflow's constructor function, you: -### Test Edit Quote Items UI Route +- Retrieve the cart's details, including its promotions and customer, using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md). +- Retrieve the details of the first-purchase promotion using the `useQueryGraphStep`. + - You pass the `FIRST_PURCHASE_PROMOTION_CODE` constant to the `filters` option to retrieve the promotion. +- Use the [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) utility to only apply the promotion if the first-purchase promotion exists, the cart doesn't have the promotion, and the customer doesn't have any orders. `when` receives two parameters: + - An object to use in the condition function. + - A condition function that receives the first parameter object and returns a boolean indicating whether to execute the steps in the `then` block. +- Retrieve the updated cart's details, including its promotions, using the `useQueryGraphStep` again. -To test the edit quote items UI route, start the Medusa application: +Finally, you return a `WorkflowResponse` with the updated cart's details. -```bash npm2yarn -npm run dev -``` +You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that require accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. -Then, open the Medusa Admin dashboard at `http://localhost:9000/admin`. Open a quote's details page whose status is either `pending_merchant`, `merchant_rejected` or `customer_rejected`. You'll find a new "Manage Quote" button. +### c. Create the Subscriber -![Manage Quote button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169567/Medusa%20Resources/Screenshot_2025-03-05_at_12.12.21_PM_c5fhsp.png) +Next, you'll create a subscriber that executes the workflow when a cart is created or transferred to a customer. -Click on the button, and you'll be taken to the manage quote page where you can update the quote's items. Try to update the items' quantities or price. Then, once you're done, click the "Confirm Edit" button to finalize the changes. +A cart can be transferred to a customer when they sign up or log in, or in B2B use cases. -![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png) +A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to events to perform a task. In this case, you'll create a subscriber that listens to the `cart.created` and `cart.customer_transferred` events to execute the workflow. -The changes can now be previewed from the quote's details page. The customer can also see these changes using the preview API route you created earlier. Once the customer accepts the quote, the changes will be applied to the order. +To create the subscriber, create the file `src/subscribers/apply-first-purchase.ts` with the following content: -*** +```ts title="src/subscribers/apply-first-purchase.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { applyFirstPurchasePromoWorkflow } from "../workflows/apply-first-purchase-promo" -## Next Steps +export default async function cartCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ + id: string +}>) { + await applyFirstPurchasePromoWorkflow(container) + .run({ + input: { + cart_id: data.id, + }, + }) +} -You've now implemented quote management features in Medusa. There's still more that you can implement to enhance the quote management experience: +export const config: SubscriberConfig = { + event: ["cart.created", "cart.customer_transferred"], +} +``` -- Refer to the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) for more quote-management related features, including how to add or remove items from a quote, and how to allow messages between the customer and the merchant. -- To build a storefront, refer to the [Storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md). You can also add to the storefront features related to quote-management using the APIs you implemented in this guide. +A subscriber file must export: -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. +1. An asynchronous function, which is the subscriber that is executed when the event is emitted. +2. A configuration object that holds the names of the events the subscriber listens to, which are `cart.created` and `cart.customer_transferred` in this case. -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). +The subscriber function receives an object as a parameter that has a `container` property, which is the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container holds Framework and commerce tools that you can resolve and use in your customizations. +In the subscriber function, you execute the `applyFirstPurchasePromoWorkflow` by invoking it, passing it the Medusa container, then calling its `run` method. You pass the `cart_id` from the event payload as an input to the workflow. -# Medusa Examples +### Test it Out -This documentation page has examples of customizations useful for your custom development in the Medusa application. +You can now test the automatic application of the first-purchase promotion to a cart. To do that, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) you installed in the first step. -Each section links to the associated documentation page to learn more about it. +The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. -## API Routes +So, if your Medusa application's directory is `medusa-first-promo`, you can find the storefront by going back to the parent directory and changing to the `medusa-first-promo-storefront` directory: -An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. +```bash +cd ../medusa-first-promo-storefront # change based on your project name +``` -### Create API Route +First, start the Medusa application with the following command: -Create the file `src/api/hello-world/route.ts` with the following content: +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm run dev +``` -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +Then, start the Next.js Starter Storefront with the following command: -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev ``` -This creates a `GET` API route at `/hello-world`. - -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). +The storefront will run at `http://localhost:8000`. Open it in your browser and click on Account at the top right to register. -### Resolve Resources in API Route +After you register, add a product to the cart, then go to the cart page. You'll find that the `FIRST_PURCHASE` promotion has been applied to the cart automatically. -To resolve resources from the Medusa container in an API route: +![Cart page with first-purchase promotion applied](https://res.cloudinary.com/dza7lstvk/image/upload/v1750842319/Medusa%20Resources/CleanShot_2025-06-25_at_12.02.17_2x_bbu8vt.png) -```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" +*** -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const productModuleService = req.scope.resolve( - Modules.PRODUCT - ) +## Step 4: Validate First-Purchase Discount Usage - const [, count] = await productModuleService - .listAndCountProducts() +You now automatically apply the first-purchase promotion to a cart, but any customer can use the promotion code at the moment. - res.json({ - count, - }) -} -``` +So, you need to add custom validation to ensure that the first-purchase promotion is only used by first-time customers. -This resolves the Product Module's main service. +In this step, you'll customize Medusa's existing workflows to validate the first-purchase promotion usage. You can do that by consuming the [workflows' 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. -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +You'll consume the hooks of the following workflows: -### Use Path Parameters +- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): This workflow is used to add or remove promotions from a cart. You'll check that the customer is a first-time customer before allowing the promotion to be added. +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): This workflow is used to complete a cart and place an order. You'll validate that the first-purchase promotion is only used by first-time customers before allowing the order to be placed. -API routes can accept path parameters. +### a. Consume `updateCartPromotionsWorkflow.validate` Hook -To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content: +You'll start by consuming the `validate` hook of the `updateCartPromotionsWorkflow`. This hook is called before any operations are performed in the workflow. -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +To consume the hook, create the file `src/workflows/hooks/validate-promotion.ts` with the following content: -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` +```ts title="src/workflows/hooks/validate-promotion.ts" highlights={validatePromotionHighlights} +import { + updateCartPromotionsWorkflow, +} from "@medusajs/medusa/core-flows" +import { FIRST_PURCHASE_PROMOTION_CODE } from "../../constants" +import { MedusaError } from "@medusajs/framework/utils" -Learn more about path parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#path-parameters/index.html.md). +updateCartPromotionsWorkflow.hooks.validate( + (async ({ input, cart }, { container }) => { + const hasFirstPurchasePromo = input.promo_codes?.some( + (code) => code === FIRST_PURCHASE_PROMOTION_CODE + ) -### Use Query Parameters + if (!hasFirstPurchasePromo) { + return + } -API routes can accept query parameters: + if (!cart.customer_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "First purchase discount can only be applied to carts with a customer" + ) + } + const query = container.resolve("query") -```ts highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" + const { data: [customer] } = await query.graph({ + entity: "customer", + fields: ["orders.*", "has_account"], + filters: { + id: cart.customer_id, + }, + }) -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, + if (!customer.has_account || (customer?.orders?.length || 0) > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "First purchase discount can only be applied to carts with no previous orders" + ) + } }) -} +) ``` -Learn more about query parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#query-parameters/index.html.md). +You consume a workflow's hook by calling the `hooks` property of the workflow, then calling the hook you want to consume. In this case, you call the `validate` hook of the `updateCartPromotionsWorkflow`. -### Use Body Parameters +The `validate` hook receives a step function as a parameter. The function receives two parameters: -API routes can accept request body parameters: +- The hook's input, which differs based on the workflow. In this case, it receives the following properties: + - `input`: The input of the `updateCartPromotionsWorkflow`, which includes the `promo_codes` to add or remove from the cart. + - `cart`: The cart being updated. +- The hook or step context object. Most notably, it has a `container` property, which is the Medusa container. -```ts highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +In the step function, you check if the `FIRST_PURCHASE_PROMOTION_CODE` is being applied to the cart. If so, you validate that: -type HelloWorldReq = { - name: string -} +- The cart is associated with a customer. +- The customer has an account. +- The customer has no previous orders. -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, - }) -} -``` +If any of these validations fail, you throw a `MedusaError` with the appropriate error message. This will prevent the promotion from being applied to the cart. -Learn more about request body parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#request-body-parameters/index.html.md). +To retrieve the customer's details, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules in your Medusa application. -### Set Response Code +### b. Consume `completeCartWorkflow.validate` Hook -You can change the response code of an API route: +Next, you'll consume the `validate` hook of the `completeCartWorkflow`. This workflow is used to complete a cart and place an order. You'll validate that the first-purchase promotion is only used by first-time customers before allowing the order to be placed. -```ts highlights={[["7", "status", "Change the response's status."]]} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +In the same `src/workflows/hooks/validate-promotion.ts` file, add the following import at the top of the file: -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} +```ts title="src/workflows/hooks/validate-promotion.ts" +import { + completeCartWorkflow, +} from "@medusajs/medusa/core-flows" ``` -Learn more about setting the response code in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#set-response-status-code/index.html.md). +Then, consume the hook at the end of the file: -### Execute a Workflow in an API Route +```ts title="src/workflows/hooks/validate-promotion.ts" highlights={validateCartCompletionHighlights} +completeCartWorkflow.hooks.validate( + (async ({ input, cart }, { container }) => { + const hasFirstPurchasePromo = cart.promotions?.some( + (promo) => promo?.code === FIRST_PURCHASE_PROMOTION_CODE + ) -To execute a workflow in an API route: + if (!hasFirstPurchasePromo) { + return + } -```ts -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../workflows/hello-world" + if (!cart.customer_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "First purchase discount can only be applied to carts with a customer" + ) + } -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run({ - input: { - name: req.query.name as string, + const query = container.resolve("query") + + const { data: [customer] } = await query.graph({ + entity: "customer", + fields: ["orders.*", "has_account"], + filters: { + id: cart.customer_id, }, }) - res.send(result) -} + if (!customer.has_account || (customer?.orders?.length || 0) > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "First purchase discount can only be applied to carts with no previous orders" + ) + } + }) +) ``` -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md). +You consume the `validate` hook of the `completeCartWorkflow` in the same way as the previous hook. The step function receives the cart being completed as an input. -### Change Response Content Type +In the step function, you check if the `FIRST_PURCHASE_PROMOTION_CODE` is applied to the cart. If so, you validate that: -By default, an API route's response has the content type `application/json`. +- The cart is associated with a customer. +- The customer has an account. +- The customer has no previous orders. -To change it to another content type, use the `writeHead` method of `MedusaResponse`: +If any of these validations fail, you throw a `MedusaError` with the appropriate error message. This will prevent the order from being placed if the first-purchase promotion is used by a customer who is not a first-time customer. -```ts highlights={responseContentTypeHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +### Test it Out -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) +To test the custom validation, start the Medusa application and the Next.js Starter Storefront as you did in the previous steps. - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) +Then, register a new customer in the storefront, and place an order. The first-purchase promotion will be applied to the cart automatically and the order will be placed successfully. - req.on("end", () => { - clearInterval(interval) - res.end() - }) -} -``` +Try to place another order with the same customer. The first-purchase promotion will not be automatically applied to the cart. If you also try to apply the first-purchase promotion manually, you'll receive an error message indicating that the promotion can only be applied to first-time customers. -This changes the response type to return an event stream. +*** -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#change-response-content-type/index.html.md). +## Step 5: Show Discount Pop-Up in Storefront -### Create Middleware +The first-time purchase promotion is now fully functional. However, you need to inform first-time customers about the discount and encourage them to sign up. -A middleware is a function executed when a request is sent to an API Route. +To do that, you'll customize the Next.js Starter Storefront to show a pop-up when a first-time customer visits the storefront. -Create the file `src/api/middlewares.ts` with the following content: +### a. Create the Pop-Up Component -```ts title="src/api/middlewares.ts" -import type { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +You'll first create the pop-up component that will be displayed to first-time customers. -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +Create the file `src/modules/common/components/discount-popup/index.tsx` with the following content: - next() - }, - ], - }, - { - matcher: "/custom/:id", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("With Path Parameter") +```tsx title="src/modules/common/components/discount-popup/index.tsx" badgeLabel="Storefront" badgeColor="blue" +"use client" - next() - }, - ], - }, - ], -}) -``` +import { Button, Heading, Text } from "@medusajs/ui" +import Modal from "@modules/common/components/modal" +import useToggleState from "@lib/hooks/use-toggle-state" +import { useEffect } from "react" +import LocalizedClientLink from "@modules/common/components/localized-client-link" -Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). +const DISCOUNT_POPUP_KEY = "discount_popup_shown" -### Restrict HTTP Methods in Middleware +const DiscountPopup = () => { + const { state, open, close } = useToggleState(false) -To restrict a middleware to an HTTP method: + useEffect(() => { + // Check if the popup has been shown before + const hasBeenShown = localStorage.getItem(DISCOUNT_POPUP_KEY) + + if (!hasBeenShown) { + open() + // Mark as shown + localStorage.setItem(DISCOUNT_POPUP_KEY, "true") + } + }, [open]) -```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights} -import type { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" + return ( + +
+ {/* Decorative elements */} +
+
+ +
+ {/* Sale tag */} +
+ SAVE 10% +
-export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - method: ["POST", "PUT"], - middlewares: [ - // ... - ], - }, - ], -}) + + Limited Time Offer! + + +
+
+
10%
+
OFF YOUR FIRST ORDER
+
+ % +
+
+
+
+
+ + +
+ + Sign up now to receive an exclusive 10% discount on your first purchase. Join our community of satisfied customers! + + +
+ + + + + +
+ +
+ *Discount applies to your first order only +
+
+
+
+ ) +} + +export default DiscountPopup ``` -### Add Validation for Custom Routes +This component uses the `Modal` component that is already available in the Next.js Starter Storefront. It displays a pop-up with a discount offer and two buttons: one to register and save the discount, and another to close the pop-up. + +The pop-up will only be shown to first-time customers. Once the pop-up is shown, a `discount_popup_shown` key is stored in the local storage to prevent it from being shown again. -1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`: +### b. Add the Pop-Up to Layout -```ts title="src/api/custom/validators.ts" -import { z } from "zod" +To ensure that the pop-up is displayed when the customer visits the storefront, you need to add the `DiscountPopup` component to the main layout of the Next.js Starter Storefront. -export const PostStoreCustomSchema = z.object({ - a: z.number(), - b: z.number(), -}) -``` +In `src/app/[countryCode]/(main)/layout.tsx`, add the following import at the top of the file: -2. Add a validation middleware to the custom route in `src/api/middlewares.ts`: +```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue" +import DiscountPopup from "@modules/common/components/discount-popup" +``` -```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]} -import { - validateAndTransformBody, - defineMiddlewares, -} from "@medusajs/framework/http" -import { PostStoreCustomSchema } from "./custom/validators" +Then, in the return statement of the `PageLayout` component, add the `DiscountPopup` component before rendering `props.children`: -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - method: "POST", - middlewares: [ - validateAndTransformBody(PostStoreCustomSchema), - ], - }, - ], -}) +```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue" +<> + {/* ... */} + {!customer && } + {props.children} + {/* ... */} + ``` -3. Use the validated body in the `/custom` API route: +Notice that you only display the pop-up if the customer is not logged in. This way, the pop-up will only be shown to first-time customers. -```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { z } from "zod" -import { PostStoreCustomSchema } from "./validators" +### c. Show Registration Form Before Login -type PostStoreCustomSchemaType = z.infer< - typeof PostStoreCustomSchema -> +If you go to the `/account` page in the Next.js Starter Storefront as a guest customer, you'll see the login form. However, in this case, you want to show the registration form first instead. -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - sum: req.validatedBody.a + req.validatedBody.b, - }) -} -``` +To change this behavior, in `src/modules/account/templates/login-template.tsx`, change the default value of `currentView` to `"register"`: -Learn more about request body validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md). +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +const [currentView, setCurrentView] = useState("register") +``` -### Pass Additional Data to API Route +This way, when a guest customer visits the `/account` page, they will see the registration form instead of the login form. -In this example, you'll pass additional data to the Create Product API route, then consume its hook: +### Test it Out -Find this example in details in [this documentation](https://docs.medusajs.com/docs/learn/customization/extend-features/extend-create-product/index.html.md). +To test the pop-up, start the Medusa application and the Next.js Starter Storefront as you did in the previous steps. -1. Create the file `src/api/middlewares.ts` with the following content: +Then, open the storefront in your browser. If you're a first-time customer, you'll see the discount pop-up encouraging you to sign up and receive the first-purchase discount. -```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]} -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +If you don't see the pop-up, make sure that you're logged out. -export default defineMiddlewares({ - routes: [ - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) -``` +![Discount pop-up in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1750844087/Medusa%20Resources/CleanShot_2025-06-25_at_12.34.35_2x_f1f5jh.png) -Learn more about additional data in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/additional-data/index.html.md). +*** -2. Create the file `src/workflows/hooks/created-product.ts` with the following content: +## Next Steps -```ts -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" +You've now implemented the first-purchase discount feature in Medusa. You can add more features to build customer loyalty, such as a [loyalty points system](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/index.html.md) or [product reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md). -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data.brand_id) { - return new StepResponse([], []) - } +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 understanding of all the concepts you've used in this guide and more. - // TODO perform custom action - }), - (async (links, { container }) => { - // TODO undo the action in the compensation - }) +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). -) -``` +### Troubleshooting -Learn more about workflow hooks in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). +If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md). -### Restrict an API Route to Admin Users +### Getting Help -You can protect API routes by restricting access to authenticated admin users only. +If you encounter issues not covered in the troubleshooting guides: -Add the following middleware in `src/api/middlewares.ts`: +1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions. +2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members. -```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate( - "user", - ["session", "bearer", "api-key"] - ), - ], - }, - ], -}) -``` +# Add Gift Message to Line Items in Medusa -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). +In this tutorial, you will learn how to add a gift message to items in carts and orders in Medusa. -### Restrict an API Route to Logged-In Customers +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 cart and order management capabilities. -You can protect API routes by restricting access to authenticated customers only. +You can customize the Medusa application and storefront to add a gift message to items in the cart. This feature allows customers to add a personalized message to their gifts, enhancing the shopping experience. -Add the following middleware in `src/api/middlewares.ts`: +## Summary -```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +By following this tutorial, you will learn how to: -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/customer*", - middlewares: [ - authenticate("customer", ["session", "bearer"]), - ], - }, - ], -}) -``` +- Install and set up Medusa and the Next.js Starter Storefront. +- Customize the storefront to support gift messages on cart items during checkout. +- Customize the Medusa Admin to show gift items with messages in an order. -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. -### Retrieve Logged-In Admin User +[View on Github](https://github.com/medusajs/examples/tree/main/order-gift-message): Find the full code for this tutorial. -To retrieve the currently logged-in user in an API route: +*** -Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users). +## Step 1: Install a Medusa Application -```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" +### Prerequisites -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const userModuleService = req.scope.resolve( - Modules.USER - ) +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) - const user = await userModuleService.retrieveUser( - req.auth_context.actor_id - ) +Start by installing the Medusa application on your machine with the following command: - // ... -} +```bash +npx create-medusa-app@latest ``` -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details/index.html.md). +First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes." -### Retrieve Logged-In Customer +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 named `{project-name}-storefront`. -To retrieve the currently logged-in customer in an API route: +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). -Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers). +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. -```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" +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. -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - if (req.auth_context?.actor_id) { - // retrieve customer - const customerModuleService = req.scope.resolve( - Modules.CUSTOMER - ) +*** - const customer = await customerModuleService.retrieveCustomer( - req.auth_context.actor_id - ) - } +## Step 2: Add Gift Inputs to Cart Item - // ... -} -``` +In this step, you'll customize the Next.js Starter Storefront to allow customers to specify that an item is a gift and add a gift message to it. -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details/index.html.md). +You'll store the gift option and message in the cart item's `metadata` property, which is a key-value `jsonb` object that can hold any additional information about the item. When the customer places the order, the `metadata` is copied to the `metadata` of the order's line items. -### Throw Errors in API Route +So, you only need to customize the storefront to add the gift message input and update the cart item metadata. -To throw errors in an API route, use `MedusaError` from the Medusa Framework: +### a. Changes to Update Item Function -```ts highlights={[["9", "MedusaError"]]} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" +The Next.js Starter Storefront has an `updateLineItem` function that sends a request to the Medusa server to update the cart item. However, it doesn't support updating the `metadata` property. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - if (!req.query.q) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The `q` query parameter is required." - ) - } +So, in `src/lib/data/cart.ts`, find the `updateLineItem` function and add a `metadata` property to its object parameter: +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["8"]]} +export async function updateLineItem({ + lineId, + quantity, + metadata, +}: { + lineId: string + quantity: number + metadata?: Record +}) { // ... } ``` -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors/index.html.md). +Next, change the usage of `await sdk.store.cart.updateLineItem` in the function to pass the `metadata` property: -### Override Error Handler of API Routes +```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" +const updateData: any = { quantity } +if (metadata) { + updateData.metadata = metadata +} -To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content: +await sdk.store.cart + .updateLineItem(cartId, lineId, updateData, {}, headers) +// ... +``` -```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]} -import { - defineMiddlewares, - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" +You pass the `metadata` property to the Medusa server, which will update the cart item with the new metadata. -export default defineMiddlewares({ - errorHandler: ( - error: MedusaError | any, - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - res.status(400).json({ - error: "Something happened.", - }) - }, -}) -``` +### b. Add Gift Inputs -Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors#override-error-handler/index.html.md), +Next, you'll modify the cart item component that's shown in the cart and checkout pages to show two inputs: one to specify that the item is a gift and another to add a gift message. -### Setting up CORS for Custom API Routes +In `src/modules/cart/components/item/index.tsx`, add the following imports at the top of the file: -By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`. +```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { Checkbox, Textarea, Button, Label } from "@medusajs/ui" +``` -To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content: +You import components from the [Medusa UI library](https://docs.medusajs.com/ui/index.html.md) that will be useful for the gift inputs. -```ts title="src/api/middlewares.ts" -import type { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" -import { ConfigModule } from "@medusajs/framework/types" -import { parseCorsOrigins } from "@medusajs/framework/utils" -import cors from "cors" +Next, in the `Item` component, add the following variables before the `changeQuantity` function: -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - const configModule: ConfigModule = - req.scope.resolve("configModule") +```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={giftVarsHighlights} +const [giftUpdating, setGiftUpdating] = useState(false) +const [newGiftMessage, setNewGiftMessage] = useState( + item.metadata?.gift_message as string || "" +) +const [isEditingGiftMessage, setIsEditingGiftMessage] = useState(false) - return cors({ - origin: parseCorsOrigins( - configModule.projectConfig.http.storeCors - ), - credentials: true, - })(req, res, next) - }, - ], - }, - ], -}) +const isGift = item.metadata?.is_gift === "true" +const giftMessage = item.metadata?.gift_message as string ``` -### Parse Webhook Body - -By default, the Medusa application parses a request's body using JSON. +You define the following variables: -To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content: +- `giftUpdating`: A state variable to track whether the gift message is being updated. This will be useful to handle loading and disabled states. +- `newGiftMessage`: A state variable to hold the new gift message input value. +- `isEditingGiftMessage`: A state variable to track whether the gift message input is being edited. This will be useful to show or hide the input field. +- `isGift`: A boolean indicating whether the item is a gift based on the `metadata.is_gift` property. +- `giftMessage`: The current gift message from the item's `metadata.gift_message` property. -```ts title="src/api/middlewares.ts" highlights={[["9"]]} -import { - defineMiddlewares, -} from "@medusajs/framework/http" +Next, add the following functions before the `return` statement to handle updates to the gift inputs: -export default defineMiddlewares({ - routes: [ - { - matcher: "/webhooks/*", - bodyParser: { preserveRawBody: true }, - method: ["POST"], - }, - ], -}) -``` +```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={giftFunctionsHighlights} +const handleGiftToggle = async (checked: boolean) => { + setGiftUpdating(true) + + try { + const newMetadata = { + is_gift: checked.toString(), + gift_message: checked ? newGiftMessage : "", + } + + await updateLineItem({ + lineId: item.id, + quantity: item.quantity, + metadata: newMetadata, + }) + } catch (error) { + console.error("Error updating gift status:", error) + } finally { + setGiftUpdating(false) + } +} -To access the raw body data in your route, use the `req.rawBody` property: +const handleSaveGiftMessage = async () => { + setGiftUpdating(true) + + try { + const newMetadata = { + is_gift: "true", + gift_message: newGiftMessage, + } + + await updateLineItem({ + lineId: item.id, + quantity: item.quantity, + metadata: newMetadata, + }) + setIsEditingGiftMessage(false) + } catch (error) { + console.error("Error updating gift message:", error) + } finally { + setGiftUpdating(false) + } +} -```ts title="src/api/webhooks/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +const handleStartEdit = () => { + setIsEditingGiftMessage(true) +} -export const POST = ( - req: MedusaRequest, - res: MedusaResponse -) => { - console.log(req.rawBody) +const handleCancelEdit = () => { + setNewGiftMessage(giftMessage || "") + setIsEditingGiftMessage(false) } ``` -*** +You define the following functions: -## Modules +- `handleGiftToggle`: Used when the gift checkbox is toggled. It updates the cart item's metadata to set the `is_gift` and `gift_message` properties based on the checkbox state. +- `handleSaveGiftMessage`: Used to save the gift message when the customer clicks the "Save" button. It updates the cart item's metadata with the new gift message. +- `handleStartEdit`: Used to start editing the gift message input by setting the `isEditingGiftMessage` state to `true`. +- `handleCancelEdit`: Used to cancel the gift message editing and reset the input value to the current gift message. -A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database. +Finally, you'll change the `return` statement to include the gift inputs. Replace the existing return statement with the following: -### Create Module +```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+
+ {/* Product Image */} +
+ + + +
-Find this example explained in details in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + {/* Product Details */} +
+
+
+ + {item.product_title} + + +
+
-1. Create the directory `src/modules/blog`. -2. Create the file `src/modules/blog/models/post.ts` with the following data model: + {/* Gift Options */} +
+
+ + +
+ + {isGift && ( +
+ {isEditingGiftMessage ? ( +
+
+ + Gift Message: + + (optional) +
+