feat(medusa): Attach or update cart sales channel (#1873)
What Allow to create a cart with a sales channel, otherwise the default one is attached. Also allow to update the sales channel on an existing cart and in that case the line items that does not belongs to the new sales channel attached are removed How Updating existing end points and service method to integrate the new requirements Tests Add new integration tests Fixes CORE-270 Fixes CORE-272 Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
8dd85e5f03
commit
df66378535
17 files changed
+595
-169
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds support for:
|
||||||
|
- Attaching Sales Channel to cart as part of creation
|
||||||
|
- Updating Sales Channel on a cart and removing inapplicable line items
|
||||||
@@ -110,18 +110,6 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`sales channels GET /store/cart/:id with saleschannel returns cart with sales channel for single cart 1`] = `
|
|
||||||
Object {
|
|
||||||
"created_at": Any<String>,
|
|
||||||
"deleted_at": null,
|
|
||||||
"description": "test description",
|
|
||||||
"id": Any<String>,
|
|
||||||
"is_disabled": false,
|
|
||||||
"name": "test name",
|
|
||||||
"updated_at": Any<String>,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = `
|
exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"sales_channel": ObjectContaining {
|
"sales_channel": ObjectContaining {
|
||||||
|
|||||||
@@ -311,11 +311,8 @@ describe("sales channels", () => {
|
|||||||
await simpleSalesChannelFactory(dbConnection, {
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
name: "Default channel",
|
name: "Default channel",
|
||||||
id: "test-channel",
|
id: "test-channel",
|
||||||
|
is_default: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
await dbConnection.manager.query(
|
|
||||||
`UPDATE store SET default_sales_channel_id = 'test-channel'`
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@@ -620,45 +617,6 @@ describe("sales channels", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("GET /store/cart/:id with saleschannel", () => {
|
|
||||||
let cart
|
|
||||||
beforeEach(async () => {
|
|
||||||
try {
|
|
||||||
await adminSeeder(dbConnection)
|
|
||||||
|
|
||||||
cart = await simpleCartFactory(dbConnection, {
|
|
||||||
sales_channel: {
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
const db = useDb()
|
|
||||||
await db.teardown()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns cart with sales channel for single cart", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
|
||||||
|
|
||||||
expect(response.data.cart.sales_channel).toBeTruthy()
|
|
||||||
expect(response.data.cart.sales_channel).toMatchSnapshot({
|
|
||||||
id: expect.any(String),
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
is_disabled: false,
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
describe("DELETE /admin/sales-channels/:id/products/batch", () => {
|
||||||
let salesChannel
|
let salesChannel
|
||||||
let product
|
let product
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`sales channels GET /store/cart/:id returns cart with sales channel for single cart 1`] = `
|
||||||
|
Object {
|
||||||
|
"created_at": Any<String>,
|
||||||
|
"deleted_at": null,
|
||||||
|
"description": "test description",
|
||||||
|
"id": Any<String>,
|
||||||
|
"is_disabled": false,
|
||||||
|
"name": "test name",
|
||||||
|
"updated_at": Any<String>,
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -83,7 +83,7 @@ Object {
|
|||||||
"cart": Object {
|
"cart": Object {
|
||||||
"billing_address_id": "test-billing-address",
|
"billing_address_id": "test-billing-address",
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"context": null,
|
"context": Object {},
|
||||||
"created_at": Any<String>,
|
"created_at": Any<String>,
|
||||||
"customer_id": "test-customer",
|
"customer_id": "test-customer",
|
||||||
"deleted_at": null,
|
"deleted_at": null,
|
||||||
@@ -259,7 +259,7 @@ Object {
|
|||||||
"cart": Object {
|
"cart": Object {
|
||||||
"billing_address_id": "test-billing-address",
|
"billing_address_id": "test-billing-address",
|
||||||
"completed_at": null,
|
"completed_at": null,
|
||||||
"context": null,
|
"context": Object {},
|
||||||
"created_at": Any<String>,
|
"created_at": Any<String>,
|
||||||
"customer_id": "test-customer",
|
"customer_id": "test-customer",
|
||||||
"deleted_at": null,
|
"deleted_at": null,
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ const {
|
|||||||
ShippingProfile,
|
ShippingProfile,
|
||||||
Product,
|
Product,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
MoneyAmount,
|
|
||||||
LineItem,
|
LineItem,
|
||||||
Payment,
|
|
||||||
Cart,
|
|
||||||
ShippingMethod,
|
|
||||||
Swap,
|
|
||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../helpers/setup-server")
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
|
const { useDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
|
const {
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
simpleCartFactory, simpleRegionFactory, simpleProductFactory,
|
||||||
|
} = require("../../factories")
|
||||||
|
|
||||||
|
const startServerWithEnvironment =
|
||||||
|
require("../../../helpers/start-server-with-environment").default
|
||||||
|
|
||||||
|
const adminReqConfig = {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
describe("sales channels", () => {
|
||||||
|
let medusaProcess
|
||||||
|
let dbConnection
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
const [process, connection] = await startServerWithEnvironment({
|
||||||
|
cwd,
|
||||||
|
env: { MEDUSA_FF_SALES_CHANNELS: true },
|
||||||
|
verbose: false,
|
||||||
|
})
|
||||||
|
dbConnection = connection
|
||||||
|
medusaProcess = process
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
|
||||||
|
medusaProcess.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/cart/", () => {
|
||||||
|
let salesChannel
|
||||||
|
let disabledSalesChannel
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
await simpleRegionFactory(dbConnection, {
|
||||||
|
name: "Test region",
|
||||||
|
tax_rate: 0,
|
||||||
|
})
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
description: "Created by Medusa",
|
||||||
|
is_default: true
|
||||||
|
})
|
||||||
|
disabledSalesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "disabled cart sales channel",
|
||||||
|
description: "disabled cart sales channel description",
|
||||||
|
is_disabled: true,
|
||||||
|
})
|
||||||
|
salesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "cart sales channel",
|
||||||
|
description: "cart sales channel description",
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a cart with the default sales channel", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post(`/store/carts`, {}, adminReqConfig)
|
||||||
|
|
||||||
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
|
expect(response.data.cart.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
description: "Created by Medusa",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a cart with the given sales channel", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post(`/store/carts`, { sales_channel_id: salesChannel.id }, adminReqConfig)
|
||||||
|
|
||||||
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
|
expect(response.data.cart.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: salesChannel.name,
|
||||||
|
description: salesChannel.description,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throw if the given sales channel is disabled", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const err = await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{ sales_channel_id: disabledSalesChannel.id },
|
||||||
|
adminReqConfig
|
||||||
|
).catch(err => err)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(400)
|
||||||
|
expect(err.response.data.message).toBe(`Unable to assign the cart to a disabled Sales Channel "disabled cart sales channel"`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/cart/:id", () => {
|
||||||
|
let salesChannel1, salesChannel2, disabledSalesChannel
|
||||||
|
let product1, product2
|
||||||
|
let cart
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
await simpleRegionFactory(dbConnection, {
|
||||||
|
name: "Test region",
|
||||||
|
currency_code: "usd",
|
||||||
|
tax_rate: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "salesChannel1",
|
||||||
|
description: "salesChannel1",
|
||||||
|
})
|
||||||
|
salesChannel2 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "salesChannel2",
|
||||||
|
description: "salesChannel2",
|
||||||
|
})
|
||||||
|
disabledSalesChannel = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "disabled cart sales channel",
|
||||||
|
description: "disabled cart sales channel description",
|
||||||
|
is_disabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
product1 = await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
title: "prod 1",
|
||||||
|
sales_channels: [salesChannel1],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "test-variant",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 50,
|
||||||
|
currency: "usd",
|
||||||
|
variant_id: "test-variant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
product2 = await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
sales_channels: [salesChannel2],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "test-variant-2",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 100,
|
||||||
|
currency: "usd",
|
||||||
|
variant_id: "test-variant-2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cart = await simpleCartFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
sales_channel: salesChannel1,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
variant_id: "test-variant",
|
||||||
|
unit_price: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it(
|
||||||
|
"updates a cart sales channels should remove the items that does not belongs to the new sales channel",
|
||||||
|
async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
let response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
||||||
|
|
||||||
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
|
expect(response.data.cart.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: salesChannel1.name,
|
||||||
|
description: salesChannel1.description,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(response.data.cart.items.length).toBe(1)
|
||||||
|
expect(response.data.cart.items[0].variant.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
title: product1.title,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await api.post(`/store/carts/${cart.id}`, { sales_channel_id: salesChannel2.id }, adminReqConfig)
|
||||||
|
|
||||||
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
|
expect(response.data.cart.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: salesChannel2.name,
|
||||||
|
description: salesChannel2.description,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(response.data.cart.items.length).toBe(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it("throw if the given sales channel is disabled", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const err = await api.post(
|
||||||
|
`/store/carts/${cart.id}`,
|
||||||
|
{ sales_channel_id: disabledSalesChannel.id },
|
||||||
|
adminReqConfig
|
||||||
|
).catch(err => err)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(400)
|
||||||
|
expect(err.response.data.message).toBe("Unable to assign the cart to a disabled Sales Channel \"disabled cart sales channel\"")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /store/cart/:id", () => {
|
||||||
|
let cart
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
cart = await simpleCartFactory(dbConnection, {
|
||||||
|
sales_channel: {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns cart with sales channel for single cart", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
||||||
|
|
||||||
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
|
expect(response.data.cart.sales_channel).toMatchSnapshot({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
is_disabled: false,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -34,7 +34,7 @@ export type CartFactoryData = {
|
|||||||
export const simpleCartFactory = async (
|
export const simpleCartFactory = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
data: CartFactoryData = {},
|
data: CartFactoryData = {},
|
||||||
seed: number
|
seed?: number
|
||||||
): Promise<Cart> => {
|
): Promise<Cart> => {
|
||||||
if (typeof seed !== "undefined") {
|
if (typeof seed !== "undefined") {
|
||||||
faker.seed(seed)
|
faker.seed(seed)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SalesChannelFactoryData = {
|
|||||||
is_disabled?: boolean
|
is_disabled?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
products?: Product[],
|
products?: Product[],
|
||||||
|
is_default?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleSalesChannelFactory = async (
|
export const simpleSalesChannelFactory = async (
|
||||||
@@ -36,12 +37,19 @@ export const simpleSalesChannelFactory = async (
|
|||||||
for (const product of data.products) {
|
for (const product of data.products) {
|
||||||
promises.push(
|
promises.push(
|
||||||
manager.query(`
|
manager.query(`
|
||||||
INSERT INTO product_sales_channel (product_id, sales_channel_id) VALUES ('${product.id}', '${salesChannel.id}');
|
INSERT INTO product_sales_channel (product_id, sales_channel_id)
|
||||||
|
VALUES ('${product.id}', '${salesChannel.id}');
|
||||||
`)
|
`)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.is_default) {
|
||||||
|
await manager.query(
|
||||||
|
`UPDATE store SET default_sales_channel_id = '${salesChannel.id}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return salesChannel
|
return salesChannel
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,12 @@ describe("POST /store/carts", () => {
|
|||||||
it("calls CartService create", () => {
|
it("calls CartService create", () => {
|
||||||
expect(CartServiceMock.create).toHaveBeenCalledTimes(1)
|
expect(CartServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(CartServiceMock.create).toHaveBeenCalledWith({
|
expect(CartServiceMock.create).toHaveBeenCalledWith({
|
||||||
region_id: IdMap.getId("testRegion"),
|
|
||||||
context: {
|
context: {
|
||||||
ip: "::ffff:127.0.0.1",
|
ip: "::ffff:127.0.0.1",
|
||||||
user_agent: "node-superagent/3.8.3",
|
user_agent: "node-superagent/3.8.3",
|
||||||
clientId: "test",
|
clientId: "test",
|
||||||
},
|
},
|
||||||
|
region_id: IdMap.getId("testRegion"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import { MedusaError } from "medusa-core-utils"
|
|||||||
import reqIp from "request-ip"
|
import reqIp from "request-ip"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
import { defaultStoreCartFields, defaultStoreCartRelations, } from "."
|
||||||
import { CartService, LineItemService } from "../../../../services"
|
import { CartService, LineItemService, RegionService } from "../../../../services"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { AddressPayload } from "../../../../types/common"
|
|
||||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||||
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /carts
|
* @oas [post] /carts
|
||||||
@@ -33,6 +33,9 @@ import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
|||||||
* region_id:
|
* region_id:
|
||||||
* type: string
|
* type: string
|
||||||
* description: The id of the Region to create the Cart in.
|
* description: The id of the Region to create the Cart in.
|
||||||
|
* sales_channel_id:
|
||||||
|
* type: string
|
||||||
|
* description: [EXPERIMENTAL] The id of the Sales channel to create the Cart in.
|
||||||
* country_code:
|
* country_code:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The 2 character ISO country code to create the Cart in."
|
* description: "The 2 character ISO country code to create the Cart in."
|
||||||
@@ -63,7 +66,7 @@ import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
|||||||
* $ref: "#/components/schemas/cart"
|
* $ref: "#/components/schemas/cart"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validated = await validator(StorePostCartReq, req.body)
|
const validated = req.validatedBody as StorePostCartReq
|
||||||
|
|
||||||
const reqContext = {
|
const reqContext = {
|
||||||
ip: reqIp.getClientIp(req),
|
ip: reqIp.getClientIp(req),
|
||||||
@@ -72,18 +75,17 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
|
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
|
||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
|
const regionService: RegionService = req.scope.resolve("regionService")
|
||||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
await entityManager.transaction(async (manager) => {
|
await entityManager.transaction(async (manager) => {
|
||||||
// Add a default region if no region has been specified
|
|
||||||
let regionId: string
|
let regionId: string
|
||||||
|
|
||||||
if (typeof validated.region_id !== "undefined") {
|
if (typeof validated.region_id !== "undefined") {
|
||||||
regionId = validated.region_id
|
regionId = validated.region_id
|
||||||
} else {
|
} else {
|
||||||
const regionService = req.scope.resolve("regionService")
|
const regions = await regionService
|
||||||
const regions = await regionService.withTransaction(manager).list({})
|
.withTransaction(manager)
|
||||||
|
.list({})
|
||||||
|
|
||||||
if (!regions?.length) {
|
if (!regions?.length) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
@@ -95,36 +97,15 @@ export default async (req, res) => {
|
|||||||
regionId = regions[0].id
|
regionId = regions[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCreate: {
|
let cart = await cartService.withTransaction(manager).create({
|
||||||
region_id: string
|
...validated,
|
||||||
context: object
|
|
||||||
customer_id?: string
|
|
||||||
email?: string
|
|
||||||
shipping_address?: Partial<AddressPayload>
|
|
||||||
} = {
|
|
||||||
region_id: regionId,
|
|
||||||
context: {
|
context: {
|
||||||
...reqContext,
|
...reqContext,
|
||||||
...validated.context,
|
...validated.context,
|
||||||
},
|
},
|
||||||
}
|
region_id: regionId,
|
||||||
|
})
|
||||||
|
|
||||||
if (req.user && req.user.customer_id) {
|
|
||||||
const customerService = req.scope.resolve("customerService")
|
|
||||||
const customer = await customerService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.retrieve(req.user.customer_id)
|
|
||||||
toCreate["customer_id"] = customer.id
|
|
||||||
toCreate["email"] = customer.email
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validated.country_code) {
|
|
||||||
toCreate["shipping_address"] = {
|
|
||||||
country_code: validated.country_code.toLowerCase(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cart = await cartService.withTransaction(manager).create(toCreate)
|
|
||||||
if (validated.items) {
|
if (validated.items) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
validated.items.map(async (i) => {
|
validated.items.map(async (i) => {
|
||||||
@@ -160,6 +141,7 @@ export class Item {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorePostCartReq {
|
export class StorePostCartReq {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -177,4 +159,10 @@ export class StorePostCartReq {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
context?: object
|
context?: object
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [
|
||||||
|
IsString(),
|
||||||
|
IsOptional(),
|
||||||
|
])
|
||||||
|
sales_channel_id?: string
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import { Router } from "express"
|
|||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Cart, Order, Swap } from "../../../../"
|
import { Cart, Order, Swap } from "../../../../"
|
||||||
import { DeleteResponse, EmptyQueryParams } from "../../../../types/common"
|
import { DeleteResponse, EmptyQueryParams } from "../../../../types/common"
|
||||||
import middlewares, { transformQuery } from "../../../middlewares"
|
import middlewares, { transformBody, transformQuery } from "../../../middlewares"
|
||||||
|
import { StorePostCartsCartReq } from "./update-cart";
|
||||||
|
import { StorePostCartReq } from "./create-cart";
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app, container) => {
|
export default (app, container) => {
|
||||||
@@ -11,9 +13,8 @@ export default (app, container) => {
|
|||||||
|
|
||||||
app.use("/carts", route)
|
app.use("/carts", route)
|
||||||
|
|
||||||
const relations = [...defaultStoreCartRelations]
|
|
||||||
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
||||||
relations.push("sales_channel")
|
defaultStoreCartRelations.push("sales_channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject plugin routes
|
// Inject plugin routes
|
||||||
@@ -25,7 +26,7 @@ export default (app, container) => {
|
|||||||
route.get(
|
route.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
transformQuery(EmptyQueryParams, {
|
transformQuery(EmptyQueryParams, {
|
||||||
defaultRelations: relations,
|
defaultRelations: defaultStoreCartRelations,
|
||||||
defaultFields: defaultStoreCartFields,
|
defaultFields: defaultStoreCartFields,
|
||||||
isList: false,
|
isList: false,
|
||||||
}),
|
}),
|
||||||
@@ -35,10 +36,15 @@ export default (app, container) => {
|
|||||||
route.post(
|
route.post(
|
||||||
"/",
|
"/",
|
||||||
middlewareService.usePreCartCreation(),
|
middlewareService.usePreCartCreation(),
|
||||||
|
transformBody(StorePostCartReq),
|
||||||
middlewares.wrap(require("./create-cart").default)
|
middlewares.wrap(require("./create-cart").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
route.post("/:id", middlewares.wrap(require("./update-cart").default))
|
route.post(
|
||||||
|
"/:id",
|
||||||
|
transformBody(StorePostCartsCartReq),
|
||||||
|
middlewares.wrap(require("./update-cart").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
"/:id/complete",
|
"/:id/complete",
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import {
|
|||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
||||||
import { CartService } from "../../../../services"
|
import { CartService } from "../../../../services"
|
||||||
import { CartUpdateProps } from "../../../../types/cart"
|
|
||||||
import { AddressPayload } from "../../../../types/common"
|
import { AddressPayload } from "../../../../types/common"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { IsType } from "../../../../utils/validators/is-type"
|
import { IsType } from "../../../../utils/validators/is-type"
|
||||||
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
||||||
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators";
|
||||||
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /store/carts/{id}
|
* @oas [post] /store/carts/{id}
|
||||||
@@ -35,6 +35,9 @@ import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
|||||||
* email:
|
* email:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "An email to be used on the Cart."
|
* description: "An email to be used on the Cart."
|
||||||
|
* sales_channel_id:
|
||||||
|
* type: string
|
||||||
|
* description: The id of the Sales channel to update the Cart with.
|
||||||
* billing_address:
|
* billing_address:
|
||||||
* description: "The Address to be used for billing purposes."
|
* description: "The Address to be used for billing purposes."
|
||||||
* anyOf:
|
* anyOf:
|
||||||
@@ -83,30 +86,11 @@ import { decorateLineItemsWithTotals } from "./decorate-line-items-with-totals"
|
|||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
const validated = req.validatedBody as StorePostCartsCartReq
|
||||||
const validated = await validator(StorePostCartsCartReq, req.body)
|
|
||||||
|
|
||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
|
await cartService.update(id, validated)
|
||||||
|
|
||||||
// Update the cart
|
|
||||||
const { shipping_address, billing_address, ...rest } = validated
|
|
||||||
|
|
||||||
const cartDataToUpdate: CartUpdateProps = { ...rest }
|
|
||||||
if (typeof shipping_address === "string") {
|
|
||||||
cartDataToUpdate.shipping_address_id = shipping_address
|
|
||||||
} else {
|
|
||||||
cartDataToUpdate.shipping_address = shipping_address
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof billing_address === "string") {
|
|
||||||
cartDataToUpdate.billing_address_id = billing_address
|
|
||||||
} else {
|
|
||||||
cartDataToUpdate.billing_address = billing_address
|
|
||||||
}
|
|
||||||
|
|
||||||
await cartService.update(id, cartDataToUpdate)
|
|
||||||
|
|
||||||
// If the cart has payment sessions update these
|
|
||||||
const updated = await cartService.retrieve(id, {
|
const updated = await cartService.retrieve(id, {
|
||||||
relations: ["payment_sessions", "shipping_methods"],
|
relations: ["payment_sessions", "shipping_methods"],
|
||||||
})
|
})
|
||||||
@@ -173,4 +157,10 @@ export class StorePostCartsCartReq {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
context?: object
|
context?: object
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [
|
||||||
|
IsString(),
|
||||||
|
IsOptional(),
|
||||||
|
])
|
||||||
|
sales_channel_id?: string
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
|||||||
import CartService from "../cart"
|
import CartService from "../cart"
|
||||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||||
|
import { FlagRouter } from "../../utils/flag-router";
|
||||||
|
|
||||||
const eventBusService = {
|
const eventBusService = {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
@@ -46,6 +47,7 @@ describe("CartService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
result = await cartService.retrieve(IdMap.getId("emptyCart"))
|
result = await cartService.retrieve(IdMap.getId("emptyCart"))
|
||||||
})
|
})
|
||||||
@@ -76,6 +78,7 @@ describe("CartService", () => {
|
|||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -136,6 +139,7 @@ describe("CartService", () => {
|
|||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -239,6 +243,7 @@ describe("CartService", () => {
|
|||||||
customerService,
|
customerService,
|
||||||
regionService,
|
regionService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -271,6 +276,7 @@ describe("CartService", () => {
|
|||||||
customer_id: IdMap.getId("customer"),
|
customer_id: IdMap.getId("customer"),
|
||||||
email: "email@test.com",
|
email: "email@test.com",
|
||||||
customer: expect.any(Object),
|
customer: expect.any(Object),
|
||||||
|
context: expect.any(Object),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
||||||
@@ -315,6 +321,7 @@ describe("CartService", () => {
|
|||||||
|
|
||||||
expect(cartRepository.create).toHaveBeenCalledTimes(1)
|
expect(cartRepository.create).toHaveBeenCalledTimes(1)
|
||||||
expect(cartRepository.create).toHaveBeenCalledWith({
|
expect(cartRepository.create).toHaveBeenCalledWith({
|
||||||
|
context: {},
|
||||||
region_id: IdMap.getId("testRegion"),
|
region_id: IdMap.getId("testRegion"),
|
||||||
shipping_address: {
|
shipping_address: {
|
||||||
first_name: "LeBron",
|
first_name: "LeBron",
|
||||||
@@ -400,6 +407,7 @@ describe("CartService", () => {
|
|||||||
shippingOptionService,
|
shippingOptionService,
|
||||||
inventoryService,
|
inventoryService,
|
||||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -614,6 +622,7 @@ describe("CartService", () => {
|
|||||||
shippingOptionService,
|
shippingOptionService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -719,6 +728,7 @@ describe("CartService", () => {
|
|||||||
cartRepository,
|
cartRepository,
|
||||||
totalsService,
|
totalsService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -806,6 +816,7 @@ describe("CartService", () => {
|
|||||||
eventBusService,
|
eventBusService,
|
||||||
inventoryService,
|
inventoryService,
|
||||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -887,6 +898,7 @@ describe("CartService", () => {
|
|||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
customerService,
|
customerService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -967,6 +979,7 @@ describe("CartService", () => {
|
|||||||
cartRepository,
|
cartRepository,
|
||||||
addressRepository,
|
addressRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1028,6 +1041,7 @@ describe("CartService", () => {
|
|||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1182,6 +1196,7 @@ describe("CartService", () => {
|
|||||||
eventBusService,
|
eventBusService,
|
||||||
paymentSessionRepository: MockRepository(),
|
paymentSessionRepository: MockRepository(),
|
||||||
priceSelectionStrategy: priceSelectionStrat,
|
priceSelectionStrategy: priceSelectionStrat,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1269,6 +1284,7 @@ describe("CartService", () => {
|
|||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1383,6 +1399,7 @@ describe("CartService", () => {
|
|||||||
cartRepository,
|
cartRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1573,6 +1590,7 @@ describe("CartService", () => {
|
|||||||
lineItemService,
|
lineItemService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
customShippingOptionService,
|
customShippingOptionService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1927,6 +1945,7 @@ describe("CartService", () => {
|
|||||||
discountService,
|
discountService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -2214,6 +2233,7 @@ describe("CartService", () => {
|
|||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ import { MedusaError, Validator } from "medusa-core-utils"
|
|||||||
import { DeepPartial, EntityManager, In } from "typeorm"
|
import { DeepPartial, EntityManager, In } from "typeorm"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||||
import { DiscountRuleType } from "../models"
|
import {
|
||||||
import { Address } from "../models/address"
|
DiscountRuleType,
|
||||||
import { Cart } from "../models/cart"
|
Address,
|
||||||
import { CustomShippingOption } from "../models/custom-shipping-option"
|
Cart,
|
||||||
import { Customer } from "../models/customer"
|
CustomShippingOption,
|
||||||
import { Discount } from "../models/discount"
|
Customer,
|
||||||
import { LineItem } from "../models/line-item"
|
Discount,
|
||||||
import { ShippingMethod } from "../models/shipping-method"
|
LineItem,
|
||||||
|
ShippingMethod,
|
||||||
|
User,
|
||||||
|
SalesChannel,
|
||||||
|
} from "../models"
|
||||||
import { AddressRepository } from "../repositories/address"
|
import { AddressRepository } from "../repositories/address"
|
||||||
import { CartRepository } from "../repositories/cart"
|
import { CartRepository } from "../repositories/cart"
|
||||||
import { LineItemRepository } from "../repositories/line-item"
|
import { LineItemRepository } from "../repositories/line-item"
|
||||||
@@ -39,6 +43,10 @@ import RegionService from "./region"
|
|||||||
import ShippingOptionService from "./shipping-option"
|
import ShippingOptionService from "./shipping-option"
|
||||||
import TaxProviderService from "./tax-provider"
|
import TaxProviderService from "./tax-provider"
|
||||||
import TotalsService from "./totals"
|
import TotalsService from "./totals"
|
||||||
|
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||||
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
|
import SalesChannelService from "./sales-channel"
|
||||||
|
import StoreService from "./store"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -48,9 +56,12 @@ type InjectedDependencies = {
|
|||||||
paymentSessionRepository: typeof PaymentSessionRepository
|
paymentSessionRepository: typeof PaymentSessionRepository
|
||||||
lineItemRepository: typeof LineItemRepository
|
lineItemRepository: typeof LineItemRepository
|
||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
|
salesChannelService: SalesChannelService
|
||||||
taxProviderService: TaxProviderService
|
taxProviderService: TaxProviderService
|
||||||
paymentProviderService: PaymentProviderService
|
paymentProviderService: PaymentProviderService
|
||||||
productService: ProductService
|
productService: ProductService
|
||||||
|
storeService: StoreService
|
||||||
|
featureFlagRouter: FlagRouter
|
||||||
productVariantService: ProductVariantService
|
productVariantService: ProductVariantService
|
||||||
regionService: RegionService
|
regionService: RegionService
|
||||||
lineItemService: LineItemService
|
lineItemService: LineItemService
|
||||||
@@ -90,6 +101,9 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
protected readonly eventBus_: EventBusService
|
protected readonly eventBus_: EventBusService
|
||||||
protected readonly productVariantService_: ProductVariantService
|
protected readonly productVariantService_: ProductVariantService
|
||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
protected readonly storeService_: StoreService
|
||||||
|
protected readonly salesChannelService_: SalesChannelService
|
||||||
protected readonly regionService_: RegionService
|
protected readonly regionService_: RegionService
|
||||||
protected readonly lineItemService_: LineItemService
|
protected readonly lineItemService_: LineItemService
|
||||||
protected readonly paymentProviderService_: PaymentProviderService
|
protected readonly paymentProviderService_: PaymentProviderService
|
||||||
@@ -127,6 +141,9 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
customShippingOptionService,
|
customShippingOptionService,
|
||||||
lineItemAdjustmentService,
|
lineItemAdjustmentService,
|
||||||
priceSelectionStrategy,
|
priceSelectionStrategy,
|
||||||
|
salesChannelService,
|
||||||
|
featureFlagRouter,
|
||||||
|
storeService,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -153,6 +170,9 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
this.taxProviderService_ = taxProviderService
|
this.taxProviderService_ = taxProviderService
|
||||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
this.priceSelectionStrategy_ = priceSelectionStrategy
|
||||||
|
this.salesChannelService_ = salesChannelService
|
||||||
|
this.featureFlagRouter_ = featureFlagRouter
|
||||||
|
this.storeService_ = storeService
|
||||||
}
|
}
|
||||||
|
|
||||||
protected transformQueryForTotals_(
|
protected transformQueryForTotals_(
|
||||||
@@ -331,15 +351,17 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
this.addressRepository_
|
this.addressRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const { region_id } = data
|
const rawCart: DeepPartial<Cart> = {
|
||||||
if (!region_id) {
|
context: data.context ?? {},
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`A region_id must be provided when creating a cart`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawCart: DeepPartial<Cart> = {}
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||||
|
) {
|
||||||
|
rawCart.sales_channel_id = (
|
||||||
|
await this.getValidatedSalesChannel(data.sales_channel_id)
|
||||||
|
).id
|
||||||
|
}
|
||||||
|
|
||||||
if (data.email) {
|
if (data.email) {
|
||||||
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
||||||
@@ -348,15 +370,21 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
rawCart.email = customer.email
|
rawCart.email = customer.email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.region_id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`A region_id must be provided when creating a cart`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCart.region_id = data.region_id
|
||||||
const region = await this.regionService_
|
const region = await this.regionService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.retrieve(region_id, {
|
.retrieve(data.region_id, {
|
||||||
relations: ["countries"],
|
relations: ["countries"],
|
||||||
})
|
})
|
||||||
const regCountries = region.countries.map(({ iso_2 }) => iso_2)
|
const regCountries = region.countries.map(({ iso_2 }) => iso_2)
|
||||||
|
|
||||||
rawCart.region_id = region.id
|
|
||||||
|
|
||||||
if (!data.shipping_address && !data.shipping_address_id) {
|
if (!data.shipping_address && !data.shipping_address_id) {
|
||||||
if (region.countries.length === 1) {
|
if (region.countries.length === 1) {
|
||||||
rawCart.shipping_address = addressRepo.create({
|
rawCart.shipping_address = addressRepo.create({
|
||||||
@@ -399,10 +427,8 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
typeof data[remainingField] !== "undefined" &&
|
typeof data[remainingField] !== "undefined" &&
|
||||||
remainingField !== "object"
|
remainingField !== "object"
|
||||||
) {
|
) {
|
||||||
/* TODO: See how to fix the error TS2590 properly while keeping the DeepPartial type */
|
const key = remainingField as string
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
rawCart[key] = data[remainingField]
|
||||||
// @ts-ignore
|
|
||||||
rawCart[remainingField] = data[remainingField]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +444,32 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getValidatedSalesChannel(
|
||||||
|
salesChannelId?: string
|
||||||
|
): Promise<SalesChannel | never> {
|
||||||
|
let salesChannel: SalesChannel
|
||||||
|
if (typeof salesChannelId !== "undefined") {
|
||||||
|
salesChannel = await this.salesChannelService_
|
||||||
|
.withTransaction(this.manager_)
|
||||||
|
.retrieve(salesChannelId)
|
||||||
|
} else {
|
||||||
|
salesChannel = (
|
||||||
|
await this.storeService_.withTransaction(this.manager_).retrieve({
|
||||||
|
relations: ["default_sales_channel"],
|
||||||
|
})
|
||||||
|
).default_sales_channel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (salesChannel.is_disabled) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Unable to assign the cart to a disabled Sales Channel "${salesChannel.name}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return salesChannel
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a line item from the cart.
|
* Removes a line item from the cart.
|
||||||
* @param cartId - the id of the cart that we will remove from
|
* @param cartId - the id of the cart that we will remove from
|
||||||
@@ -721,6 +773,30 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
const cartRepo = transactionManager.getCustomRepository(
|
const cartRepo = transactionManager.getCustomRepository(
|
||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
|
const relations = [
|
||||||
|
"items",
|
||||||
|
"shipping_methods",
|
||||||
|
"shipping_address",
|
||||||
|
"billing_address",
|
||||||
|
"gift_cards",
|
||||||
|
"customer",
|
||||||
|
"region",
|
||||||
|
"payment_sessions",
|
||||||
|
"region.countries",
|
||||||
|
"discounts",
|
||||||
|
"discounts.rule",
|
||||||
|
"discounts.regions",
|
||||||
|
]
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
SalesChannelFeatureFlag.key
|
||||||
|
) &&
|
||||||
|
data.sales_channel_id
|
||||||
|
) {
|
||||||
|
relations.push("items.variant", "items.variant.product")
|
||||||
|
}
|
||||||
|
|
||||||
const cart = await this.retrieve(cartId, {
|
const cart = await this.retrieve(cartId, {
|
||||||
select: [
|
select: [
|
||||||
"subtotal",
|
"subtotal",
|
||||||
@@ -729,20 +805,7 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
"discount_total",
|
"discount_total",
|
||||||
"total",
|
"total",
|
||||||
],
|
],
|
||||||
relations: [
|
relations,
|
||||||
"items",
|
|
||||||
"shipping_methods",
|
|
||||||
"shipping_address",
|
|
||||||
"billing_address",
|
|
||||||
"gift_cards",
|
|
||||||
"customer",
|
|
||||||
"region",
|
|
||||||
"payment_sessions",
|
|
||||||
"region.countries",
|
|
||||||
"discounts",
|
|
||||||
"discounts.rule",
|
|
||||||
"discounts.regions",
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (data.customer_id) {
|
if (data.customer_id) {
|
||||||
@@ -764,8 +827,12 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.region_id !== "undefined") {
|
if (typeof data.region_id !== "undefined") {
|
||||||
|
const shippingAddress =
|
||||||
|
typeof data.shipping_address !== "string"
|
||||||
|
? data.shipping_address
|
||||||
|
: {}
|
||||||
const countryCode =
|
const countryCode =
|
||||||
(data.country_code || data.shipping_address?.country_code) ?? null
|
(data.country_code || shippingAddress?.country_code) ?? null
|
||||||
await this.setRegion_(cart, data.region_id, countryCode)
|
await this.setRegion_(cart, data.region_id, countryCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,6 +851,18 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
await this.updateShippingAddress_(cart, shippingAddress, addrRepo)
|
await this.updateShippingAddress_(cart, shippingAddress, addrRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof data.sales_channel_id !== "undefined" &&
|
||||||
|
data.sales_channel_id != cart.sales_channel_id
|
||||||
|
) {
|
||||||
|
await this.onSalesChannelChange(cart, data.sales_channel_id)
|
||||||
|
cart.sales_channel_id = data.sales_channel_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof data.discounts !== "undefined") {
|
if (typeof data.discounts !== "undefined") {
|
||||||
const previousDiscounts = [...cart.discounts]
|
const previousDiscounts = [...cart.discounts]
|
||||||
cart.discounts.length = 0
|
cart.discounts.length = 0
|
||||||
@@ -861,6 +940,42 @@ class CartService extends TransactionBaseService<CartService> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the cart line item that does not belongs to the newly assigned sales channel
|
||||||
|
* @param cart - The cart being updated
|
||||||
|
* @param newSalesChannelId - The new sales channel being assigned to the cart
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async onSalesChannelChange(
|
||||||
|
cart: Cart,
|
||||||
|
newSalesChannelId: string
|
||||||
|
): Promise<void> {
|
||||||
|
await this.getValidatedSalesChannel(newSalesChannelId)
|
||||||
|
|
||||||
|
const productIds = cart.items.map((item) => item.variant.product_id)
|
||||||
|
const productsToKeep = await this.productService_
|
||||||
|
.withTransaction(this.manager_)
|
||||||
|
.filterProductsBySalesChannel(productIds, newSalesChannelId, {
|
||||||
|
select: ["id", "sales_channels"],
|
||||||
|
take: productIds.length,
|
||||||
|
})
|
||||||
|
const productIdsToKeep = new Set<string>(
|
||||||
|
productsToKeep.map((product) => product.id)
|
||||||
|
)
|
||||||
|
const itemsToRemove = cart.items.filter((item) => {
|
||||||
|
return !productIdsToKeep.has(item.variant.product_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (itemsToRemove.length) {
|
||||||
|
const results = await Promise.all(
|
||||||
|
itemsToRemove.map((item) => {
|
||||||
|
return this.removeLineItem(cart.id, item.id)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
cart.items = results.pop()?.items ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the customer id of a cart
|
* Sets the customer id of a cart
|
||||||
* @param cart - the cart to add email to
|
* @param cart - the cart to add email to
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { MedusaError } from "medusa-core-utils"
|
|||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { SearchService } from "."
|
import { SearchService } from "."
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { Product, ProductTag, ProductType, ProductVariant } from "../models"
|
import {
|
||||||
|
Product,
|
||||||
|
ProductTag,
|
||||||
|
ProductType,
|
||||||
|
ProductVariant,
|
||||||
|
SalesChannel,
|
||||||
|
} from "../models"
|
||||||
import { ImageRepository } from "../repositories/image"
|
import { ImageRepository } from "../repositories/image"
|
||||||
import {
|
import {
|
||||||
FindWithoutRelationsOptions,
|
FindWithoutRelationsOptions,
|
||||||
@@ -284,6 +290,37 @@ class ProductService extends TransactionBaseService<ProductService> {
|
|||||||
return product.variants
|
return product.variants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async filterProductsBySalesChannel(
|
||||||
|
productIds: string[],
|
||||||
|
salesChannelId: string,
|
||||||
|
config: FindProductConfig = {
|
||||||
|
skip: 0,
|
||||||
|
take: 50,
|
||||||
|
}
|
||||||
|
): Promise<Product[]> {
|
||||||
|
const givenRelations = config.relations ?? []
|
||||||
|
const requiredRelations = ["sales_channels"]
|
||||||
|
const relationsSet = new Set([...givenRelations, ...requiredRelations])
|
||||||
|
|
||||||
|
const products = await this.list(
|
||||||
|
{
|
||||||
|
id: productIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
relations: [...relationsSet],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const productSalesChannelsMap = new Map<string, SalesChannel[]>(
|
||||||
|
products.map((product) => [product.id, product.sales_channels])
|
||||||
|
)
|
||||||
|
return products.filter((product) => {
|
||||||
|
return productSalesChannelsMap
|
||||||
|
.get(product.id)
|
||||||
|
?.some((sc) => sc.id === salesChannelId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async listTypes(): Promise<ProductType[]> {
|
async listTypes(): Promise<ProductType[]> {
|
||||||
const manager = this.manager_
|
const manager = this.manager_
|
||||||
const productTypeRepository = manager.getCustomRepository(
|
const productTypeRepository = manager.getCustomRepository(
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class Discount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CartCreateProps = {
|
export type CartCreateProps = {
|
||||||
region_id: string
|
region_id?: string
|
||||||
email?: string
|
email?: string
|
||||||
billing_address_id?: string
|
billing_address_id?: string
|
||||||
billing_address?: Partial<AddressPayload>
|
billing_address?: Partial<AddressPayload>
|
||||||
@@ -55,6 +55,8 @@ export type CartCreateProps = {
|
|||||||
type?: CartType
|
type?: CartType
|
||||||
context?: object
|
context?: object
|
||||||
metadata?: object
|
metadata?: object
|
||||||
|
sales_channel_id?: string
|
||||||
|
country_code?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CartUpdateProps = {
|
export type CartUpdateProps = {
|
||||||
@@ -63,8 +65,8 @@ export type CartUpdateProps = {
|
|||||||
email?: string
|
email?: string
|
||||||
shipping_address_id?: string
|
shipping_address_id?: string
|
||||||
billing_address_id?: string
|
billing_address_id?: string
|
||||||
billing_address?: AddressPayload
|
billing_address?: AddressPayload | string
|
||||||
shipping_address?: AddressPayload
|
shipping_address?: AddressPayload | string
|
||||||
completed_at?: Date
|
completed_at?: Date
|
||||||
payment_authorized_at?: Date
|
payment_authorized_at?: Date
|
||||||
gift_cards?: GiftCard[]
|
gift_cards?: GiftCard[]
|
||||||
@@ -72,4 +74,5 @@ export type CartUpdateProps = {
|
|||||||
customer_id?: string
|
customer_id?: string
|
||||||
context?: object
|
context?: object
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
sales_channel_id?: string
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user