Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core-flows, medusa): add shipping methods to cart API #7150

Merged
merged 11 commits into from Apr 29, 2024
6 changes: 6 additions & 0 deletions .changeset/thick-bats-rescue.md
@@ -0,0 +1,6 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
---

feat(core-flows, medusa): add shipping methods to cart API
141 changes: 104 additions & 37 deletions integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts
Expand Up @@ -26,7 +26,7 @@ import {
ISalesChannelModuleService,
IStockLocationServiceNext,
} from "@medusajs/types"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { ContainerRegistrationKeys, RuleOperator } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import adminSeeder from "../../../../helpers/admin-seeder"

Expand Down Expand Up @@ -1397,34 +1397,44 @@ medusaIntegrationTestRunner({
})
})
})

describe("AddShippingMethodToCartWorkflow", () => {
it("should add shipping method to cart", async () => {
let cart = await cartModuleService.create({
let cart
let shippingProfile
let fulfillmentSet
let priceSet

beforeEach(async () => {
cart = await cartModuleService.create({
currency_code: "usd",
shipping_address: {
country_code: "us",
province: "ny",
},
})

const shippingProfile =
await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})
shippingProfile = await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})

const fulfillmentSet = await fulfillmentModule.create({
fulfillmentSet = await fulfillmentModule.create({
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved
name: "Test",
type: "test-type",
service_zones: [
{
name: "Test",
geo_zones: [
{
type: "country",
country_code: "us",
},
],
geo_zones: [{ type: "country", country_code: "us" }],
},
],
})

priceSet = await pricingModule.create({
prices: [{ amount: 3000, currency_code: "usd" }],
})
})

it("should add shipping method to cart", async () => {
const shippingOption = await fulfillmentModule.createShippingOptions({
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
Expand All @@ -1436,41 +1446,26 @@ medusaIntegrationTestRunner({
description: "Test description",
code: "test-code",
},
})

const priceSet = await pricingModule.create({
prices: [
rules: [
{
amount: 3000,
currency_code: "usd",
operator: RuleOperator.EQ,
attribute: "shipping_address.province",
value: "ny",
},
],
})

await remoteLink.create([
{
[Modules.FULFILLMENT]: {
shipping_option_id: shippingOption.id,
},
[Modules.PRICING]: {
price_set_id: priceSet.id,
},
[Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id },
[Modules.PRICING]: { price_set_id: priceSet.id },
},
])

cart = await cartModuleService.retrieve(cart.id, {
select: ["id", "region_id", "currency_code"],
})

await addShippingMethodToWorkflow(appContainer).run({
input: {
options: [
{
id: shippingOption.id,
},
],
options: [{ id: shippingOption.id }],
cart_id: cart.id,
currency_code: cart.currency_code,
},
})

Expand All @@ -1491,6 +1486,78 @@ medusaIntegrationTestRunner({
})
)
})

it("should throw error when shipping option is not valid", async () => {
const shippingOption = await fulfillmentModule.createShippingOptions({
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
rules: [
{
operator: RuleOperator.EQ,
attribute: "shipping_address.city",
value: "sf",
},
],
})

await remoteLink.create([
{
[Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id },
[Modules.PRICING]: { price_set_id: priceSet.id },
},
])

const { errors } = await addShippingMethodToWorkflow(
appContainer
).run({
input: {
options: [{ id: shippingOption.id }],
cart_id: cart.id,
},
throwOnError: false,
})

// Rules are setup only for Germany, this should throw an error
expect(errors).toEqual([
expect.objectContaining({
error: expect.objectContaining({
message:
"Shipping Options (Test shipping option) are invalid for cart. Add a valid shipping option or remove existing invalid shipping options before continuing.",
type: "invalid_data",
}),
}),
])
})

it("should throw error when shipping option is not present in the db", async () => {
const { errors } = await addShippingMethodToWorkflow(
appContainer
).run({
input: {
options: [{ id: "does-not-exist" }],
cart_id: cart.id,
},
throwOnError: false,
})

// Rules are setup only for Berlin, this should throw an error
expect(errors).toEqual([
expect.objectContaining({
error: expect.objectContaining({
message: "Shipping Options (does-not-exist) not found",
type: "invalid_data",
}),
}),
])
})
})

describe("listShippingOptionsForCartWorkflow", () => {
Expand Down
90 changes: 89 additions & 1 deletion integration-tests/modules/__tests__/cart/store/carts.spec.ts
Expand Up @@ -7,14 +7,19 @@ import {
import {
ICartModuleService,
ICustomerModuleService,
IFulfillmentModuleService,
IPricingModuleService,
IProductModuleService,
IPromotionModuleService,
IRegionModuleService,
ISalesChannelModuleService,
ITaxModuleService,
} from "@medusajs/types"
import { PromotionRuleOperator, PromotionType } from "@medusajs/utils"
import {
PromotionRuleOperator,
PromotionType,
RuleOperator,
} from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import adminSeeder from "../../../../helpers/admin-seeder"
import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer"
Expand All @@ -38,6 +43,7 @@ medusaIntegrationTestRunner({
let remoteLink: RemoteLink
let promotionModule: IPromotionModuleService
let taxModule: ITaxModuleService
let fulfillmentModule: IFulfillmentModuleService

let defaultRegion

Expand All @@ -52,6 +58,9 @@ medusaIntegrationTestRunner({
remoteLink = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
promotionModule = appContainer.resolve(ModuleRegistrationName.PROMOTION)
taxModule = appContainer.resolve(ModuleRegistrationName.TAX)
fulfillmentModule = appContainer.resolve(
ModuleRegistrationName.FULFILLMENT
)
})

beforeEach(async () => {
Expand Down Expand Up @@ -1100,6 +1109,85 @@ medusaIntegrationTestRunner({
})
})
})

describe("POST /store/carts/:id/shipping-methods", () => {
it("should add a shipping methods to a cart", async () => {
const cart = await cartModule.create({
currency_code: "usd",
shipping_address: { country_code: "us" },
items: [],
})

const shippingProfile =
await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})

const fulfillmentSet = await fulfillmentModule.create({
name: "Test",
type: "test-type",
service_zones: [
{
name: "Test",
geo_zones: [{ type: "country", country_code: "us" }],
},
],
})

const priceSet = await pricingModule.create({
prices: [{ amount: 3000, currency_code: "usd" }],
})

const shippingOption = await fulfillmentModule.createShippingOptions({
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
rules: [
{
operator: RuleOperator.EQ,
attribute: "shipping_address.country_code",
value: "us",
},
],
})

await remoteLink.create([
{
[Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id },
[Modules.PRICING]: { price_set_id: priceSet.id },
},
])

let response = await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id }
)

expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: [
{
shipping_option_id: shippingOption.id,
amount: 3000,
id: expect.any(String),
tax_lines: [],
adjustments: [],
},
],
})
)
})
})
})
},
})
11 changes: 7 additions & 4 deletions packages/core-flows/src/common/steps/use-remote-query.ts
Expand Up @@ -5,21 +5,24 @@ interface StepInput {
entry_point: string
fields: string[]
variables?: Record<string, any>
list?: boolean
}

export const useRemoteQueryStepId = "use-remote-query"
export const useRemoteQueryStep = createStep(
useRemoteQueryStepId,
async (data: StepInput, { container }) => {
const { list = true, fields, variables, entry_point: entryPoint } = data
const query = container.resolve("remoteQuery")

const queryObject = remoteQueryObjectFromString({
entryPoint: data.entry_point,
fields: data.fields,
variables: data.variables,
entryPoint,
fields,
variables,
})

const result = await query(queryObject)
const entities = await query(queryObject)
const result = list ? entities : entities[0]

return new StepResponse(result)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core-flows/src/definition/cart/steps/index.ts
Expand Up @@ -20,5 +20,5 @@ export * from "./retrieve-cart-with-links"
export * from "./set-tax-lines-for-items"
export * from "./update-cart-promotions"
export * from "./update-carts"
export * from "./validate-cart-shipping-options"
export * from "./validate-variants-existence"